Вложенные 'froms' в LINQ
Я новичок в LINQ, и у меня проблема с вложенными froms:
using System;
using System.Linq;
class MultipleFroms
{
static void Main()
{
char[] chrs = { 'A', 'B', 'C'};
char[] chrs2 = { 'X', 'Y', 'Z' };
var pairs = from ch1 in chrs
from ch2 in chrs2
select ch1+" "+ ch2;
Console.WriteLine("For ABC and XYZ: ");
foreach (var p in pairs)
Console.WriteLine(p);
Console.WriteLine();
Console.WriteLine("For D and W: ");
chrs = new char[] { 'D' };
chrs2 = new char[] { 'W' };
foreach (var p in pairs)
Console.WriteLine(p);
}
}
На выходе у меня есть:
For ABC and XYZ:
A X
A Y
A Z
B X
B Y
B Z
C X
C Y
C Z
For D and W:
A W
B W
C W
Но я ожидал:
...
For D and W:
D W
Почему pairs
во втором случае используется "старый" chrs
, { 'A', 'B', 'C'}
вместо {'D'}
?
Ответы
Ответ 1
Этот вопрос получил несколько хороших ответов, в которых указано очевидное - вам нужно переназначить переменную pairs
. Однако меня больше интересует странное поведение - как, почему переназначение chrs2
влияет на результат перечисления, а переназначение chrs
- нет.
Если мы используем вложенные from
-s, выглядит как переназначение любой из используемых коллекций, за исключением FIRST, влияет на результат перечисления: http://ideone.com/X7f3eQ.
Теперь, как вы, вероятно, должны знать, LINQ "синтаксис запросов" - это просто синтаксический сахар для цепочки вызовов метода расширения из библиотеки System.Linq
. Пусть desugar ваш конкретный пример:
var pairs = from ch1 in chrs
from ch2 in chrs2
select ch1 + " "+ ch2;
становится
var pairs = chrs.SelectMany(ch1 => chrs2, (ch1, ch2) => ch1 + " " + ch2);
(или с синтаксисом не-расширения-метода, SelectMany(chrs, ch1 => chrs2, (ch1, ch2) => ch1 + " " + ch2)
)
(проверьте здесь: http://ideone.com/NjVeLD)
Итак, что происходит? SelectMany
принимает chrs
и два lambdas в качестве параметров и генерирует из них IEnumerable
, который позже можно перечислить, чтобы начать фактическую оценку.
Теперь, когда мы переназначаем chrs2
, он изменяется в лямбда, потому что это захваченная переменная. Однако это, очевидно, не будет работать с chrs
!
Ответ 2
Самый простой способ объяснить это, я могу придумать, это отметить, что этот
var pairs = from ch1 in chrs
from ch2 in chrs2
select ch1 + " " + ch2;
Является эквивалентным:
var pairs = chrs.SelectMany(ch1 => chrs2, (ch1, ch2) => ch1 + " " + ch2);
И что компилятор внутренне создает класс закрытия, подобный этому:
private sealed class Closure
{
public char[] chrs2;
internal IEnumerable<char> Method(char ch1)
{
return chrs2;
}
}
И затем изменяет ваш метод следующим образом:
static void Main()
{
Closure closure = new Closure();
char[] chrs = { 'A', 'B', 'C' };
closure.chrs2 = new[] { 'X', 'Y', 'Z' };
var pairs = chrs.SelectMany(ch1 => closure.chrs2, (ch1, ch2) => ch1 + " " + ch2);
Console.WriteLine("For ABC and XYZ: ");
foreach (var p in pairs)
Console.WriteLine(p);
Console.WriteLine();
Console.WriteLine("For D and W: ");
chrs = new[] { 'D' };
closure.chrs2 = new[] { 'W' };
foreach (var p in pairs)
Console.WriteLine(p);
}
Я надеюсь, что таким образом легко увидеть, как вы прибудете к вашему результату.
Примечание. Я сделал несколько упрощений во время объяснения выше, чтобы сделать poitn лучше.
Следующий вопрос может быть "почему компилятор делает это?". Ответ заключается в том, что функции лямбда могут передаваться и выполняться в другом контексте с тем, в каком они были созданы. Когда это происходит, часто желательно сохранить состояние:
public Action<string> PrintCounter()
{
int counter = 0;
return prefix =>
Console.WriteLine(prefix + " " + (counter++).ToString());
}
В приведенном выше примере вы можете передавать функцию вокруг столько, сколько хотите, но счетчик выполняется каждый раз, когда вы его вызываете. Обычно локальные переменные, такие как counter
, живут в стеке, поэтому их время жизни является вызовом функции, стек "раскручивается", когда функция завершает выполнение. Чтобы обойти это, создаются замыкания, как показано выше. В большинстве случаев они чрезвычайно полезны, потому что они позволяют писать код, который отделяет логические/управляющие структуры от деталей того, как они будут использоваться. Но в некоторых дегенеративных случаях вы видите результаты, подобные тем, которые вы испытали.
Ответ 3
Вы должны посмотреть запрос как вызов метода, в котором метод получает первый источник данных (chrs
) в качестве параметра. Проблема в том, что вы не можете переназначить объект, к которому вы уже вызвали метод после его настройки. Второй источник данных (chrs2
) похож на глобальную переменную, таким образом, когда вы обновляете его значение, результат запроса также изменяется.
Лучший подход заключается в переносе вашего запроса на метод:
public static IEnumerable<string> Pairs(char[] chrs,char[] chrs2)
{
return from ch1 in chrs
from ch2 in chrs2
select ch1+" "+ ch2;
}
Таким образом вы можете сделать что-то вроде этого:
static void Main(string[] args)
{
char[] chrs = { 'A', 'B', 'C' };
char[] chrs2 = { 'X', 'Y', 'Z' };
Console.WriteLine("For ABC and XYZ: ");
foreach (var p in Pairs(chrs,chrs2))
Console.WriteLine(p);
Console.WriteLine();
Console.WriteLine("For D and W: ");
chrs = new char[] { 'D' };
chrs2 = new char[] { 'W' };
foreach (var p in Pairs(chrs, chrs2))
Console.WriteLine(p);
}
Ответ 4
Вам нужно снова назначить переменную pairs
. После обновления chrs
и chrs2
снова введите следующие строки кода:
pairs = from ch1 in chrs
from ch2 in chrs2
select ch1+" "+ ch2;