如何:执行自定义联接操作(C# 编程指南)

此示例演示如何执行无法使用 join 子句执行的联接操作。 在查询表达式中,join 子句仅适用于同等联接(这是迄今为止最常见的联接操作类型),并针对同等联接进行了优化。 执行同等联接时,一般总是可以通过使用 join 子句获得最佳性能。

但是,在下面一些情况中,无法使用 join 子句:

  • 联接是在不等式(非同等联接)上断言的。

  • 联接是在多个等式或不等式上断言的。

  • 必须在联接操作之前为右侧(内部)序列引入一个临时范围变量。

若要执行非同等联接,可以使用多个 from 子句单独引入每个数据源。 然后,在 where 子句中将谓词表达式应用于每个源的范围变量。 该表达式还可以采用方法调用的形式。

备注

不要将这种自定义联接操作与使用多个 from 子句访问内部集合相混淆。有关更多信息,请参见 join 子句(C# 参考)

示例

下面示例中的第一个方法演示了一个简单的交叉联接。 必须慎用交叉联接,因为它们可能产生非常大的结果集。 但在某些方案中,可以使用它们创建源序列以供运行附加查询。

第二个方法产生其类别 ID 列在左侧类别列表中的所有产品的序列。 请注意,这种方法使用 let 子句和 Contains 方法创建了一个临时数组。 还可以在查询前创建该数组并去掉第一个 from 子句。

     class CustomJoins
     {

         #region Data

         class Product
         {
             public string Name { get; set; }
             public int CategoryID { get; set; }
         }

         class Category
         {
             public string Name { get; set; }
             public int ID { get; set; }
         }

         // Specify the first data source.
         List<Category> categories = new List<Category>()
 { 
     new Category(){Name="Beverages", ID=001},
     new Category(){ Name="Condiments", ID=002},
     new Category(){ Name="Vegetables", ID=003},         
 };

         // Specify the second data source.
         List<Product> products = new List<Product>()
{
   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},
   new Product{Name="Ice Cream", CategoryID=007},
   new Product{Name="Mackerel", CategoryID=012},
 };
         #endregion

         static void Main()
         {
             CustomJoins app = new CustomJoins();
             app.CrossJoin();
             app.NonEquijoin();

             Console.WriteLine("Press any key to exit.");
             Console.ReadKey();
         }

         void CrossJoin()
         {
             var crossJoinQuery =
                 from c in categories
                 from p in products
                 select new { c.ID, p.Name };

             Console.WriteLine("Cross Join Query:");
             foreach (var v in crossJoinQuery)
             {
                 Console.WriteLine("{0,-5}{1}", v.ID, v.Name);
             }
         }

         void NonEquijoin()
         {
             var nonEquijoinQuery =
                 from p in products
                 let catIds = from c in categories
                              select c.ID
                 where catIds.Contains(p.CategoryID) == true 
                 select new { Product = p.Name, CategoryID = p.CategoryID };

             Console.WriteLine("Non-equijoin query:");
             foreach (var v in nonEquijoinQuery)
             {
                 Console.WriteLine("{0,-5}{1}", v.CategoryID, v.Product);
             }
         }
     }
     /* Output:
 Cross Join Query:
 1    Tea
 1    Mustard
 1    Pickles
 1    Carrots
 1    Bok Choy
 1    Peaches
 1    Melons
 1    Ice Cream
 1    Mackerel
 2    Tea
 2    Mustard
 2    Pickles
 2    Carrots
 2    Bok Choy
 2    Peaches
 2    Melons
 2    Ice Cream
 2    Mackerel
 3    Tea
 3    Mustard
 3    Pickles
 3    Carrots
 3    Bok Choy
 3    Peaches
 3    Melons
 3    Ice Cream
 3    Mackerel
 Non-equijoin query:
 1    Tea
 2    Mustard
 2    Pickles
 3    Carrots
 3    Bok Choy
 Press any key to exit.
      */

在下面的示例中,查询必须基于匹配键联接两个序列,而对于内部(右侧)序列而言,无法在 join 子句本身之前获取这些键。 如果此联接是使用 join 子句执行的,则必须为每个元素调用 Split 方法。 使用多个 from 子句可使查询避免反复进行方法调用的系统开销。 然而,由于 join 进行了优化,因此在此特定情况下,它仍然可能比使用多个 from 子句快。 结果会有所不同,主要取决于方法调用的系统开销有多大。

class MergeTwoCSVFiles
{
    static void Main()
    {
        // See section Compiling the Code for information about the data files. 
        string[] names = System.IO.File.ReadAllLines(@"../../../names.csv");
        string[] scores = System.IO.File.ReadAllLines(@"../../../scores.csv");

        // Merge the data sources using a named type. 
        // You could use var instead of an explicit type for the query.
        IEnumerable<Student> queryNamesScores =
            // Split each line in the data files into an array of strings. 
            from name in names
            let x = name.Split(',')
            from score in scores
            let s = score.Split(',')
            // Look for matching IDs from the two data files. 
            where x[2] == s[0]
            // If the IDs match, build a Student object. 
            select new Student()
            {
                FirstName = x[0],
                LastName = x[1],
                ID = Convert.ToInt32(x[2]),
                ExamScores = (from scoreAsText in s.Skip(1)
                              select Convert.ToInt32(scoreAsText)).
                              ToList()
            };

        // Optional. Store the newly created student objects in memory 
        // for faster access in future queries
        List<Student> students = queryNamesScores.ToList();

        foreach (var student in students)
        {
            Console.WriteLine("The average score of {0} {1} is {2}.",
                student.FirstName, student.LastName, student.ExamScores.Average());
        }

        //Keep console window open in debug mode
        Console.WriteLine("Press any key to exit.");
        Console.ReadKey();
    }
}

class Student
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int ID { get; set; }
    public List<int> ExamScores { get; set; }
}

/* Output: 
    The average score of Omelchenko Svetlana is 82.5.
    The average score of O'Donnell Claire is 72.25.
    The average score of Mortensen Sven is 84.5.
    The average score of Garcia Cesar is 88.25.
    The average score of Garcia Debra is 67.
    The average score of Fakhouri Fadi is 92.25.
    The average score of Feng Hanying is 88.
    The average score of Garcia Hugo is 85.75.
    The average score of Tucker Lance is 81.75.
    The average score of Adams Terry is 85.25.
    The average score of Zabokritski Eugene is 83.
    The average score of Tucker Michael is 92.
 */

编译代码

  • 创建一个面向 .NET Framework 3.5 或更高版本的 Visual Studio 控制台应用程序项目。 默认情况下,该项目具有一个对 System.Core.dll 的引用以及一条针对 System.Linq 命名空间的 using 指令。

  • 将 Program 类替换为上述示例中的代码。

  • 按照 如何:联接不同文件的内容 (LINQ) 中的指令设置数据文件(scores.csv 和 names.csv)。

  • 按 F5 编译并运行程序。

  • 按任意键退出控制台窗口。

请参见

任务

如何:对 Join 子句的结果进行排序(C# 编程指南)

参考

join 子句(C# 参考)

概念

LINQ 查询表达式(C# 编程指南)

联接运算