Поделиться через


оператор join (справочник по C#)

join Используйте предложение для связывания элементов из разных исходных последовательностей, которые не имеют прямой связи в объектной модели. Единственное требование заключается в том, что элементы в каждом источнике имеют некоторое значение, которое можно сравнить с равенством. Например, у распространителя продуктов питания может быть список поставщиков определенного продукта, а также список покупателей. Вы можете использовать join предложение для создания списка поставщиков и покупателей этого продукта, которые находятся в одном регионе.

Справочные документы по языку C# описывают последнюю выпущенную версию языка C#. Она также содержит начальную документацию по функциям в общедоступных предварительных версиях для предстоящего языкового выпуска.

Документация определяет любую функцию, впервые представленную в последних трех версиях языка или в текущих общедоступных предварительных версиях.

Подсказка

Чтобы узнать, когда функция впервые появилась в C#, ознакомьтесь со статьей об истории версий языка C#.

Предложение join принимает две исходные последовательности в качестве входных данных. Элементы в каждой последовательности должны быть или содержать свойство, которое можно сравнить с соответствующим свойством в другой последовательности. Предложение join использует специальное equals ключевое слово для сравнения указанных ключей для равенства. Все соединения, которые join выполняет предложение, являются эквивалентными. Форма выходных данных join предложения зависит от конкретного типа выполняемого соединения. В следующем списке показаны три наиболее распространенных типа соединения:

  • Внутреннее соединение
  • Присоединение к группе
  • Левое внешнее соединение

Внутреннее соединение

В следующем примере показано простое внутреннее эквисоединение. Этот запрос создает плоскую последовательность пар "имя продукта / категория". Одна строка категории отображается в нескольких элементах. Если элемент из categories не соответствует products, эта категория не отображается в результатах.

var innerJoinQuery =
    from category in categories
    join prod in products on category.ID equals prod.CategoryID
    select new { ProductName = prod.Name, Category = category.Name }; //produces flat sequence

Дополнительные сведения см. в разделе "Выполнение внутренних соединений".

Присоединение к группе

Предложение join с into выражением называется групповым объединением.

var innerGroupJoinQuery =
    from category in categories
    join prod in products on category.ID equals prod.CategoryID into prodGroup
    select new { CategoryName = category.Name, Products = prodGroup };

Соединение группы создает иерархическую последовательность результатов, которая связывает элементы в левой исходной последовательности с одним или несколькими соответствующими элементами в правой исходной последовательности. Соединение группы не имеет эквивалента в реляционных терминах; это по сути последовательность массивов объектов.

Если элементы из правой исходной последовательности не соответствуют элементу в левом источнике, join предложение создает пустой массив для этого элемента. Таким образом, соединение группы по-прежнему является внутренним эквивалентным соединением, за исключением того, что последовательность результатов организована в группы.

Если вы просто выберете результаты соединения группы, вы можете получить доступ к элементам, но не удается определить ключ, на который они совпадают. Поэтому, как правило, более полезно выбрать результаты объединения группы в новый тип, который также имеет имя ключа, как показано в предыдущем примере.

Вы также можете использовать результат объединения группы в качестве генератора другого подзапроса:

var innerGroupJoinQuery2 =
    from category in categories
    join prod in products on category.ID equals prod.CategoryID into prodGroup
    from prod2 in prodGroup
    where prod2.UnitPrice > 2.50M
    select prod2;

Дополнительные сведения см. в разделе "Выполнение группированных соединений".

Левое внешнее соединение

В левом внешнем соединении запрос возвращает все элементы в левой исходной последовательности, даже если соответствующие элементы не находятся в правой последовательности. Чтобы выполнить левое внешнее соединение в LINQ, используйте метод DefaultIfEmpty в сочетании с групповым соединением, чтобы указать элемент справа по умолчанию, формируемый в случае, если для левого элемента не найдены совпадения. Можно использовать null в качестве значения по умолчанию для любого ссылочного типа или указать определяемый пользователем тип по умолчанию. В следующем примере показан определяемый пользователем тип по умолчанию:

var leftOuterJoinQuery =
    from category in categories
    join prod in products on category.ID equals prod.CategoryID into prodGroup
    from item in prodGroup.DefaultIfEmpty(new Product { Name = String.Empty, CategoryID = 0 })
    select new { CatName = category.Name, ProdName = item.Name };

Дополнительные сведения см. в разделе "Выполнение левых внешних соединений".

Оператор равенства

Предложение join выполняет эквисоединение. Другими словами, можно основывать совпадения только на равенстве двух ключей. Другие типы сравнения, такие как "больше" или "не равно", не поддерживаются. Чтобы прояснить, что все соединения являются эквисоединениями, join предложение использует equals ключевое слово вместо == оператора. Ключевое equals слово может использоваться только в join предложении, и оно отличается от == оператора некоторыми важными способами. При сравнении строк equals имеет перегрузку для сравнения по значению, а оператор == использует сравнение ссылок. Если обе стороны сравнения имеют одинаковые строковые переменные и equals== достигают одного результата: true. Это связано с тем, что, когда программа объявляет две или более эквивалентные строковые переменные, компилятор сохраняет все из них в одном расположении. Это называется интернированием. Еще одно важное различие заключается в сравнении null: null equals null вычисляется как false с equals оператором, а не == оператором, который вычисляет его как true. Наконец, поведение области отличается: при использовании equals левый ключ обрабатывает внешнюю последовательность источника, а правый ключ обрабатывает внутренний источник. Внешний источник действует только в левой части equals, а внутренняя последовательность — только в правой.

неравные соединения

Вы можете выполнять несовпадающие соединения, перекрестные соединения и другие пользовательские операции соединения с помощью нескольких from предложений, чтобы независимо вводить новые последовательности в запрос. Дополнительные сведения см. в разделе "Выполнение пользовательских операций соединения".

Объединения в коллекциях объектов и реляционных таблицах

В выражении запроса LINQ выполняются операции соединения с коллекциями объектов. Невозможно объединить коллекции объектов точно так же, как и две реляционные таблицы. В LINQ требуются только явные join предложения, если две исходные последовательности не имеют никакой связи. При работе с LINQ to SQL объектная модель представляет таблицы внешнего ключа в качестве свойств первичной таблицы. Например, в базе данных Northwind у таблицы Customer есть связь внешнего ключа с таблицей Orders. При сопоставлении таблиц с объектной Orders моделью класс Customer имеет свойство, содержащее коллекцию Orders , связанную с этим клиентом. По сути, соединение уже сделано для вас.

Дополнительные сведения о запросах между связанными таблицами в контексте LINQ to SQL см. в разделе "Практическое руководство. Сопоставление связей базы данных".

Составные ключи

Вы можете проверить равенство нескольких значений с помощью составного ключа. Дополнительные сведения см. в разделе "Присоединение с помощью составных ключей". Вы также можете использовать составные ключи в предложении group .

Пример

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

class JoinDemonstration
{
    #region Data

    class Product
    {
        public required string Name { get; init; }
        public required int CategoryID { get; init; }
    }

    class Category
    {
        public required string Name { get; init; }
        public required int ID { get; init; }
    }

    // Specify the first data source.
    List<Category> categories =
    [
        new Category {Name="Beverages", ID=001},
        new Category {Name="Condiments", ID=002},
        new Category {Name="Vegetables", ID=003},
        new Category {Name="Grains", ID=004},
        new Category {Name="Fruit", ID=005}
    ];

    // Specify the second data source.
    List<Product> products =
    [
      new Product {Name="Cola",  CategoryID=001},
      new Product {Name="Tea",  CategoryID=001},
      new Product {Name="Mustard", CategoryID=002},
      new Product {Name="Pickles", CategoryID=002},
      new Product {Name="Carrots", CategoryID=003},
      new Product {Name="Bok Choy", CategoryID=003},
      new Product {Name="Peaches", CategoryID=005},
      new Product {Name="Melons", CategoryID=005},
    ];
    #endregion

    static void Main(string[] args)
    {
        JoinDemonstration app = new JoinDemonstration();

        app.InnerJoin();
        app.GroupJoin();
        app.GroupInnerJoin();
        app.GroupJoin3();
        app.LeftOuterJoin();
        app.LeftOuterJoin2();
    }

    void InnerJoin()
    {
        // Create the query that selects
        // a property from each element.
        var innerJoinQuery =
           from category in categories
           join prod in products on category.ID equals prod.CategoryID
           select new { Category = category.ID, Product = prod.Name };

        Console.WriteLine("InnerJoin:");
        // Execute the query. Access results
        // with a simple foreach statement.
        foreach (var item in innerJoinQuery)
        {
            Console.WriteLine("{0,-10}{1}", item.Product, item.Category);
        }
        Console.WriteLine($"InnerJoin: {innerJoinQuery.Count()} items in 1 group.");
        Console.WriteLine(System.Environment.NewLine);
    }

    void GroupJoin()
    {
        // This is a demonstration query to show the output
        // of a "raw" group join. A more typical group join
        // is shown in the GroupInnerJoin method.
        var groupJoinQuery =
           from category in categories
           join prod in products on category.ID equals prod.CategoryID into prodGroup
           select prodGroup;

        // Store the count of total items (for demonstration only).
        int totalItems = 0;

        Console.WriteLine("Simple GroupJoin:");

        // A nested foreach statement is required to access group items.
        foreach (var prodGrouping in groupJoinQuery)
        {
            Console.WriteLine("Group:");
            foreach (var item in prodGrouping)
            {
                totalItems++;
                Console.WriteLine("   {0,-10}{1}", item.Name, item.CategoryID);
            }
        }
        Console.WriteLine($"Unshaped GroupJoin: {totalItems} items in {groupJoinQuery.Count()} unnamed groups");
        Console.WriteLine(System.Environment.NewLine);
    }

    void GroupInnerJoin()
    {
        var groupJoinQuery2 =
            from category in categories
            orderby category.ID
            join prod in products on category.ID equals prod.CategoryID into prodGroup
            select new
            {
                Category = category.Name,
                Products = from prod2 in prodGroup
                           orderby prod2.Name
                           select prod2
            };

        //Console.WriteLine("GroupInnerJoin:");
        int totalItems = 0;

        Console.WriteLine("GroupInnerJoin:");
        foreach (var productGroup in groupJoinQuery2)
        {
            Console.WriteLine(productGroup.Category);
            foreach (var prodItem in productGroup.Products)
            {
                totalItems++;
                Console.WriteLine("  {0,-10} {1}", prodItem.Name, prodItem.CategoryID);
            }
        }
        Console.WriteLine($"GroupInnerJoin: {totalItems} items in {groupJoinQuery2.Count()} named groups");
        Console.WriteLine(System.Environment.NewLine);
    }

    void GroupJoin3()
    {

        var groupJoinQuery3 =
            from category in categories
            join product in products on category.ID equals product.CategoryID into prodGroup
            from prod in prodGroup
            orderby prod.CategoryID
            select new { Category = prod.CategoryID, ProductName = prod.Name };

        //Console.WriteLine("GroupInnerJoin:");
        int totalItems = 0;

        Console.WriteLine("GroupJoin3:");
        foreach (var item in groupJoinQuery3)
        {
            totalItems++;
            Console.WriteLine($"   {item.ProductName}:{item.Category}");
        }

        Console.WriteLine($"GroupJoin3: {totalItems} items in 1 group");
        Console.WriteLine(System.Environment.NewLine);
    }

    void LeftOuterJoin()
    {
        // Create the query.
        var leftOuterQuery =
           from category in categories
           join prod in products on category.ID equals prod.CategoryID into prodGroup
           select prodGroup.DefaultIfEmpty(new Product() { Name = "Nothing!", CategoryID = category.ID });

        // Store the count of total items (for demonstration only).
        int totalItems = 0;

        Console.WriteLine("Left Outer Join:");

        // A nested foreach statement  is required to access group items
        foreach (var prodGrouping in leftOuterQuery)
        {
            Console.WriteLine("Group:");
            foreach (var item in prodGrouping)
            {
                totalItems++;
                Console.WriteLine("  {0,-10}{1}", item.Name, item.CategoryID);
            }
        }
        Console.WriteLine($"LeftOuterJoin: {totalItems} items in {leftOuterQuery.Count()} groups");
        Console.WriteLine(System.Environment.NewLine);
    }

    void LeftOuterJoin2()
    {
        // Create the query.
        var leftOuterQuery2 =
           from category in categories
           join prod in products on category.ID equals prod.CategoryID into prodGroup
           from item in prodGroup.DefaultIfEmpty()
           select new { Name = item == null ? "Nothing!" : item.Name, CategoryID = category.ID };

        Console.WriteLine($"LeftOuterJoin2: {leftOuterQuery2.Count()} items in 1 group");
        // Store the count of total items
        int totalItems = 0;

        Console.WriteLine("Left Outer Join 2:");

        // Groups have been flattened.
        foreach (var item in leftOuterQuery2)
        {
            totalItems++;
            Console.WriteLine("{0,-10}{1}", item.Name, item.CategoryID);
        }
        Console.WriteLine($"LeftOuterJoin2: {totalItems} items in 1 group");
    }
}
/*Output:

InnerJoin:
Cola      1
Tea       1
Mustard   2
Pickles   2
Carrots   3
Bok Choy  3
Peaches   5
Melons    5
InnerJoin: 8 items in 1 group.


Unshaped GroupJoin:
Group:
    Cola      1
    Tea       1
Group:
    Mustard   2
    Pickles   2
Group:
    Carrots   3
    Bok Choy  3
Group:
Group:
    Peaches   5
    Melons    5
Unshaped GroupJoin: 8 items in 5 unnamed groups


GroupInnerJoin:
Beverages
    Cola       1
    Tea        1
Condiments
    Mustard    2
    Pickles    2
Vegetables
    Bok Choy   3
    Carrots    3
Grains
Fruit
    Melons     5
    Peaches    5
GroupInnerJoin: 8 items in 5 named groups


GroupJoin3:
    Cola:1
    Tea:1
    Mustard:2
    Pickles:2
    Carrots:3
    Bok Choy:3
    Peaches:5
    Melons:5
GroupJoin3: 8 items in 1 group


Left Outer Join:
Group:
    Cola      1
    Tea       1
Group:
    Mustard   2
    Pickles   2
Group:
    Carrots   3
    Bok Choy  3
Group:
    Nothing!  4
Group:
    Peaches   5
    Melons    5
LeftOuterJoin: 9 items in 5 groups


LeftOuterJoin2: 9 items in 1 group
Left Outer Join 2:
Cola      1
Tea       1
Mustard   2
Pickles   2
Carrots   3
Bok Choy  3
Nothing!  4
Peaches   5
Melons    5
LeftOuterJoin2: 9 items in 1 group
Press any key to exit.
*/

Замечания

Предложение join , за которым не следует into , преобразуется в Join вызов метода. Предложение join , за которым следует into , преобразуется в GroupJoin вызов метода.

См. также