Вложенные '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;