Какая разница, часть Четвёртая: into и into

Ключевое слово «into» в выражениях-запросах означает две разных вещи, в зависимости от того, идёт ли оно после join или select/group. Если оно следует за join, то оно превращает объединение в групповое объединение. Если оно следует за select или group, то оно вводит продолжение запроса. Эти две вещи сильно отличаются, но их легко спутать.

Во-первых, групповое объединение. Предположим, у вас есть ключ – идентификатор покупателя – который используется в качестве первичного ключа коллекции покупателей, и в качестве внешнего ключа в коллекции номеров кредитных карточек. То есть, у вас есть класс Customer с полями Id, Name, Address, и так далее, и класс CreditCard с полями CustomerId, CardType, Number, и так далее. Пусть у покупателя Боба есть Виза и Дискавер, а у покупателя Алисы есть Виза и Мастеркард. Так что у нас есть данные о покупателях:

101, Bob
102, Alice

и данные кредиток:

101, Visa
101, Discover
102, Visa
102, MasterCard

Если мы построим запрос

from customer in customers
join card in cards on customer.Id equals card.CustomerId
select new {customer.Name, card.Kind}

То результатом будет

Bob, Visa
Bob, Discover
Alice, Visa
Alice, Mastercard

Верно? Это всего лишь прямолинейное объединение. Мы заканчиваем списком из четырёх элементов. Но это, вероятно, не то, что вы на самом деле хотите в этом случае. Предположим, что вы хотели список покупателей, и для каждого покупателя в списке, список их кредиток. Вы можете использовать групповое объединение:

from customer in customers
join card in cards on customer.Id equals card.CustomerId into cardList
select new {customer.Name, Cards = cardList}

Результатом этого запроса были бы две записи, а не четыре:

Bob, { Visa, Discover }
Alice, { Visa, Mastercard }

В основном, «into» в групповом объединении – это порция выражения-запроса, которая логически собирает результаты всех объединённых записей, объединяет их в последовательность, и запихивает их во временную переменную cardList.

Продолжение запроса означает совсем другое. Смысл продолжения запроса в упрощении «передачи» результатов одного запроса в следующий запрос. Например, предположим, что вы хотите найти всех кареглазых детей, у кого есть хотя бы один голубоглазый брат или сестра. Очевидный способ сделать это – что-то вроде

from parent in parents
from child in parent.Children
where child.EyeColor == "Brown"
where parent.Children.Any(c=>c.EyeColor == Blue)
select child

но предположим, что мы не хотим так делать. Предположим, что там много больших семей, где все дети с карими глазами; наивный поиск будет довольно-таки неэффективным. Вы можете захотеть сначала сузить его другим способом. То есть, возможно, было бы быстрее сначала найти всех родителей, у кого есть голубоглазый ребёнок, а затем из этого короткого списка родителей извлечь всех кареглазых детей. Проще всего это сделать двумя запросами. Сначала найти родителей, а затем из этого построить второй запрос с проекцией детей:

var parentsWithABlueEyedChild =
    from parent in parents
    where parent.Children.Any(c=>c.EyeColor == Blue)
    select parent;
var brownEyedChildren =
    from p in parentsWithABlueEyedChild
    from child in p.Children
    where child.EyeColor == Brown
    select child;

Теперь мы можем достаточно легко скомбинировать это в один большой запрос:

var brownEyedChildren =
    from p in (
        from parent in parents
        where parent.Children.Any(c=>c.EyeColor == Blue)
        select parent)
    from child in p.Children
    where child.EyeColor == Brown
    select child;

Но... Представьте чуть больше уровней вложенности. Это превратится в бардак. Заметьте, как мы ввели переменную диапазона «p» вначале, а потом нам пришлось пробраться через весь второй запрос прежде, чем снова её употребить. Здесь мы вводим переменные в «обратном» порядке. Продолжение запроса просто позволяет вам изменить этот порядок обратно на «прямой», перемещая переменную диапазона p в конец первоначального запроса:

var brownEyedChildren =
    from parent in parents
    where parent.Children.Any(c=>c.EyeColor == Blue)
    select parent into p
    from child in p.Children
    where child.EyeColor == Brown
    select child;

Заметьте, что в случае группового объединения, можно считать идентификатор справа от «into» логически представляющим последовательность, полученную в результате группировки соответствующих присоединённых элементов. Но в случае продолжения запроса, «into»-идентификатор не соответствует последовательности из первого запроса – сам запрос и есть объект, представляющий первую последовательность! Вместо этого, «p» представляет переменную диапазона, которая соответствует одному элементу коллекции за раз. Помните, «into» в продолжении запроса – всего лишь модный способ сказать from p in (blah); «p» - это переменная диапазона, которая по очереди проходит элементы (blah), но не сама последовательность элементов (blah).