JavaRush /Курсы /C# SELF /Объединение коллекций с join...

Объединение коллекций с join ( Join)

C# SELF
33 уровень , 0 лекция
Открыта

1. Введение

Иногда нам приходится работать с двумя разными коллекциями, которые связаны между собой каким-либо общим признаком. Например, у нас есть список заказов и отдельная коллекция покупателей. Как определить, какой заказ принадлежит какому покупателю? Для этого нужно, чтобы обе коллекции были связаны по общему идентификатору — например, по userId.

В реляционных базах данных такую задачу решает операция JOIN, которая объединяет строки из разных таблиц по совпадающим ключам. В LINQ есть аналогичный оператор — он тоже называется Join.

Представьте две таблицы: в первой — список читателей библиотеки с их идентификаторами, а во второй — список заказов книг, где у каждого заказа указан id читателя. С помощью join мы "склеиваем" эти таблицы по id, и на выходе получаем пары «читатель + его заказ».

Кстати, если вы думали, что join — это только для "баз данных", то вы много потеряли! В программировании объединять коллекции приходится часто, особенно когда работаешь с внешними системами или структурированными файлами (JSON, XML и прочими табличками Excel из бухгалтерии).

2. Сигнатура метода Join и принцип работы

Вот как выглядит метод Join:


public static IEnumerable<TResult> Join<TOuter, TInner, TKey, TResult>(
    this IEnumerable<TOuter> outer,                 // первая коллекция (внешняя)
    IEnumerable<TInner> inner,                      // вторая коллекция (внутренняя)
    Func<TOuter, TKey> outerKeySelector,            // как получить ключ из элемента внешней коллекции
    Func<TInner, TKey> innerKeySelector,            // как получить ключ из внутренней
    Func<TOuter, TInner, TResult> resultSelector)   // функция генерации результата (новый элемент)

На первый взгляд может показаться громоздким, но сейчас всё разложим по полочкам.
Как это работает:
Для каждого элемента из первой коллекции (outer) LINQ ищет подходящие (по ключу) элементы из второй (inner). Когда ключи совпали, вызывается resultSelector, и результат сохраняется в итоговую коллекцию.

3. Пример: Объединяем покупателей и их заказы

Заведём пару классов и коллекций. Эта идея — логичное продолжение нашего учебного приложения (пусть это будет магазинчик, который мы разрабатываем по ходу курса).


public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
}

public class Order
{
    public int Id { get; set; }
    public int CustomerId { get; set; }
    public string Product { get; set; } = "";
}

// Коллекция покупателей
var customers = new List<Customer>
{
    new Customer { Id = 1, Name = "Вася" },
    new Customer { Id = 2, Name = "Петя" },
    new Customer { Id = 3, Name = "Маша" },
};

// Коллекция заказов
var orders = new List<Order>
{
    new Order { Id = 101, CustomerId = 2, Product = "Книга" },
    new Order { Id = 102, CustomerId = 1, Product = "Ручка" },
    new Order { Id = 103, CustomerId = 2, Product = "Тетрадь" },
    new Order { Id = 104, CustomerId = 3, Product = "Ластик" },
};

Теперь мы хотим получить всех покупателей с их заказами. Например, вывести: "Петя заказал Книга", "Петя заказал Тетрадь" и т.п.

Применяем Join


// Объединяем покупателей с заказами по customer.Id и order.CustomerId
var query = customers.Join(
    orders,
    customer => customer.Id,          // Как получить ключ из покупателя
    order => order.CustomerId,        // Как получить ключ из заказа
    (customer, order) => new          // Что делать с совпавшими парами (создаём новый объект)
    {
        CustomerName = customer.Name,
        Product = order.Product
    }
);

// Выводим результат
foreach (var item in query)
{
    Console.WriteLine($"{item.CustomerName} заказал {item.Product}");
}

Что будет выведено:

Вася заказал Ручка
Петя заказал Книга
Петя заказал Тетрадь
Маша заказала Ластик

Обратите внимание: если у покупателя несколько заказов, он появится в списке несколько раз — по одному на каждый заказ. Именно так и должно быть!

4. Таблица: Сравнение Join и GroupBy + SelectMany

Операция Результат Сценарий использования
Join
Плоский список пар Классический SQL JOIN. Каждая пара (совпадение) — отдельная строка.
GroupBy + SelectMany
Группы с подколлекциями Хотите получить "покупатель + все его заказы" в виде структуры "один — ко многим".

Новички часто используют Join там, где нужна структура «покупатель → список заказов». Но Join работает построчно и не группирует данные. Для этого есть GroupJoin — о нём в следующей лекции. Или же использовать GroupBy.

5. Join в Query Syntax (SQL-стиле LINQ)

LINQ поддерживает два синтаксиса: метод-цепочки (Method Syntax, например Join(...)) и так называемый SQL-стиль (Query Syntax), который визуально напоминает обычные SQL-запросы.

Для некоторых разработчиков — особенно тех, кто раньше работал с базами данных — такой синтаксис бывает более наглядным:


var query2 =
    from customer in customers
    join order in orders
        on customer.Id equals order.CustomerId
    select new
    {
        CustomerName = customer.Name,
        Product = order.Product
    };

foreach (var item in query2)
{
    Console.WriteLine($"{item.CustomerName} заказал {item.Product}");
}

Обратите внимание:
В Query Syntax используется ключевое слово equals — оператор == здесь не работает!
Это частый подвох на собеседованиях, особенно для начинающих 😉

6. Важные детали и подводные камни

Иногда возникает ситуация, когда не все элементы из коллекций попадают в итоговый результат. Это связано с тем, что метод Join реализует так называемый «inner join», то есть он объединяет только те элементы, у которых совпадают ключи. Если у какого-то покупателя нет заказов, его просто не будет в итоговом списке. Аналогично, если есть заказ с CustomerId, которого нет среди покупателей, такой заказ тоже не попадёт в результат.

Но что делать, если нужно получить всех покупателей, даже тех, у кого заказов нет? Для таких случаев существует «левый» join, который в LINQ реализуется с помощью комбинации GroupJoin и SelectMany. Об этом мы поговорим в следующей лекции. В классическом же Join обязательно, чтобы обе стороны были представлены — иначе совпадения не произойдёт, и элемент выпадет из результата.

7. Работа с несколькими ключами (composite key)

Иногда объединять коллекции нужно не по одному, а по нескольким полям. Например: соединить товары и их продажи по "Код-товара + Год-продажи".

В LINQ это решается с помощью создания анонимных объектов в качестве ключей:


var sales = ...; // продажи
var products = ...; // товары

var query = products.Join(
    sales,
    prod => new { prod.Code, prod.Year },
    sale => new { sale.ProductCode, sale.Year },
    (prod, sale) => new { prod.Name, sale.Amount }
);

Важно, чтобы типы и имена свойств в этих анонимных объектах совпадали. Если они отличаются, совпадений найдено не будет — даже если значения по смыслу одинаковы.

8. Как выглядит объединённая коллекция — схема

Вот схема, иллюстрирующая работу join (очень упрощённо):

flowchart LR
    subgraph Customers
        A1["Вася (Id=1)"]
        A2["Петя (Id=2)"]
        A3["Маша (Id=3)"]
    end
    subgraph Orders
        B1["Ручка (CustomerId=1)"]
        B2["Книга (CustomerId=2)"]
        B3["Тетрадь (CustomerId=2)"]
        B4["Ластик (CustomerId=3)"]
    end

    A1-->|Id=1|B1
    A2-->|Id=2|B2
    A2-->|Id=2|B3
    A3-->|Id=3|B4
    
Схема соединения покупателей и заказов по ключу

Из схемы видно, что каждый покупатель соединяется со всеми своими заказами — при условии совпадения ключей.

9. Кратко об ошибках и особенностях

Одна из самых частых ошибок — перепутать порядок параметров, особенно если типы ключей в обеих коллекциях совпадают. При этом компилятор или LINQ не выдадут ошибку, но результат может оказаться пустым или неожиданным.

Ещё одна распространённая ловушка — попытка с помощью Join сделать «левое» объединение, чтобы в результате остались все покупатели, даже если у них нет заказов. Однако классический Join работает только с совпадающими парами и не включает элементы без соответствий.

2
Задача
C# SELF, 33 уровень, 0 лекция
Недоступна
Простое объединение списков покупателей и заказов
Простое объединение списков покупателей и заказов
Комментарии (2)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Ra Уровень 35 Student
11 декабря 2025
Мягко говоря, странная задача. Пришлось подсовывать string[] messages = {"", "", "а"};
Ra Уровень 35 Student
11 декабря 2025
IMO лучше писать так:

var query = customers.Join(
    inner: orders,
    outerKeySelector: customer => customer.Id,    // Как получить ключ из покупателя
    innerKeySelector: order => order.CustomerId, // Как получить ключ из заказа
    resultSelector: (customer, order) => new          // Что делать с совпавшими парами (создаём новый объект)
    {
        CustomerName = customer.Name,
        Product = order.Product
    }
);