左外连接,这两种方法有何区别?

3
这两种使用LINQ进行左外连接的方法有何区别?对于这两种方法,我都使用了两个买家和供应商列表,并通过共同的地区将它们连接起来,以查找在同一地区的供应商和买家。
class Supplier
{
    public string Name { get; set; }
    public string District { get; set; }
}

class Buyer
{
    public string Name { get; set; }
    public string District { get; set; }
}
List<Buyer> buyers = new List<Buyer>()
{
    new Buyer() { Name = "Johny", District = "Fantasy District" },
    new Buyer() { Name = "Peter", District = "Scientists District" },
    new Buyer() { Name = "Paul", District = "Fantasy District" },
    new Buyer() { Name = "Maria", District = "Scientists District" },
    new Buyer() { Name = "Joshua", District = "EarthIsFlat District" },
    new Buyer() { Name = "Sylvia", District = "Developers District" },
    new Buyer() { Name = "Rebecca", District = "Scientists District" },
    new Buyer() { Name = "Jaime", District = "Developers District" },
    new Buyer() { Name = "Pierce", District = "Fantasy District" }
};
List<Supplier> suppliers = new List<Supplier>()
{
    new Supplier() { Name = "Harrison", District = "Fantasy District" },
    new Supplier() { Name = "Charles", District = "Developers District" },
    new Supplier() { Name = "Hailee", District = "Scientists District" },
    new Supplier() { Name = "Taylor", District = "EarthIsFlat District" }
};

首先:

var suppliersAndBuyers = from s in suppliers
                         orderby s.District
                         join b in buyers on s.District equals b.District into buyersGroup
                         select buyersGroup.DefaultIfEmpty(
                             new Buyer()
                             {
                                 Name = string.Empty,
                                 District = s.District
                             });

foreach (var item in suppliersAndBuyers)
{
    foreach (var buyer in item)
    {
        Console.WriteLine($"{buyer.District} {buyer.Name}");
    }
}

第二种方法:

var suppliersAndBuyers = from s in suppliers
                                 orderby s.District
                                 join b in buyers on s.District equals b.District into buyersGroup
                                 from bG in buyersGroup.DefaultIfEmpty()
                                 select new
                                 {
                                     Name = bG.Name == null ? string.Empty : bG.Name,
                                     s.District,
                                 };

foreach (var item in suppliersAndBuyers)
{
    Console.WriteLine($"{item.District} {item.Name}");
}

这两种方法都可以产生完全相同的输出结果,它们之间唯一的区别是输出结果的方式吗?我应该使用哪一个?

编辑:第一种方法返回IEnumerable<IEnumerable<Buyer>>,而第二种方法返回IEnumerable<AnonymousType>,这两种方法之间唯一有意义的区别是它们返回的类型不同吗?这是决定使用哪种方法的唯一因素吗,无论我想要一个类型还是匿名类型?


2
提示:Name = bG.Name == null ? string.Empty : bG.Name -> Name = bG.Name ?? "" - ErikE
1
这是一个假设问题,它会影响数据库调用还是仅在内存中操作? - Igor
@Igor,我正在学习LINQ,所以我没有真正的场景,我正在按照MSDN网站上的Join示例进行操作,并看到了这两种方法,应用它们,但无法弄清楚它们之间的区别。 - Darkbound
1
在第二种方法中,您需要再次遍历可枚举对象,重新构建所有对象。个人而言,我总是更喜欢第一种方法。不过,我认为第一种方法也会创建许多空对象并且不使用它们,从而使用了不必要的额外内存... - Yotam Salmon
@Darkbound 我已经在我的回答中回答了你的第二个问题。现在将答案添加到原始问题中。 - Yotam Salmon
显示剩余7条评论
4个回答

3
好的,据我所见:(A)
var suppliersAndBuyers = from s in suppliers
                         orderby s.District

列举供应商列表。这是显而易见的。现在,将其加入买家列表中:
var suppliersAndBuyers = from s in suppliers
                         orderby s.District
                         join b in buyers on s.District equals b.District

这会创建匹配项(一些对象,我不知道它们的类型,因为我没有正常的Visual Studio实例在我面前)。但例如,就像Harrison:Jonnie, Hailee:Peter, ...。现在我们可以基于这些匹配项(由变量bs表示)创建一个对象的IEnumerable,如下所示:

var suppliersAndBuyers = from s in suppliers
                         orderby s.District
                         join b in buyers on s.District equals b.District
                         select new {
                             Supplier = s, Buyer = b
                         }

这将创建一个匿名类型的IEnumerable,每个对象表示一个供应商和买家的配对。
var suppliersAndBuyers = from s in suppliers
                         orderby s.District
                         join b in buyers on s.District equals b.District into buyersGroup

但是根据你标题中所写,你决定使用了左连接。它的作用是将在片段A中创建的列表中的每个元素与买家列表中的所有匹配对象进行匹配。这将产生一个匹配项的可枚举集合。例如,对于供应商列表中的第一个条目Harrison,您将得到一个包含Johnny、Paul和Pierce的可枚举集合。对于供应商列表中的其他元素也是如此,按其District排序。
这就是为什么最终得到了一个IEnumerable>的原因。因为对于每个供应商(第一维IEnumerable),您都有一个“Buyer”列表(第二维+类型说明)。
然后,我认为合并空条目已经过时,因为您不应该有null,而只是空的IEnumerable,当您遍历它们时,您将不会遇到任何元素。(虽然我不确定最后一段,因为我从未编译过代码,所以我不知道)
现在关于合并部分,第一个示例为每个条目创建一个新的Buyer对象,然后取DefaultIsEmpty。对于第二个示例,它首先创建第一维和第二维FULL IEnumerables,然后在再次迭代时,它合并了空值。正如我在评论中提到的那样,这是一个不必要的循环。

谢谢Yotam,这解决了IEnumerable<IEnumerable<T>>的困惑,但问题仍然存在,何时使用其中任何一种方法?您在最初的回复中提到,区别在于第二种方法会多次迭代组,但当我使用第一种方法打印它们时,我仍然会多次迭代。唯一的“有意义的区别”是一个返回<SomeType>,另一个返回匿名类型吗? - Darkbound
1
实际上,对你来说,区别在于表达式的类型。但是需要澄清的是,我告诉你第二个示例执行了一个额外的迭代,这并不会影响你编写的后续 for 循环。但是两者都有 2 个 IEnumerable 的维度。这意味着你将有 2 层嵌套的 for。如果你想要节省一层并只使用一个 for,你可以停留在第二个代码片段,并且不使用左连接,而是使用内连接。然后你将得到一个匹配项的 1 维 IEnumerable。 - Yotam Salmon

2

1
实际上我正在返回IEnumerable<IEnumerable<Buyer>>,但这仍然没有回答什么是区别以及何时应用这两种方法。 - Darkbound
或者至少我无法推断出答案 :) - Darkbound
@mjwills 我明白为什么一个返回买家,另一个返回匿名类型,但我不明白这是否是两种方法之间唯一的实际区别? - Darkbound
1
@Darkbound,除此之外我看不出有什么主要的区别。请查看链接的参考资料,以便决定何时使用它们中的哪一个。 - Salah Akbari

2

好的,我花了一些时间才解密出这个问题 :)

首先,在第一个示例中,您正在执行以下操作

select buyersGroup.DefaultIfEmpty( new Buyer() { Name = string.Empty, District = s.District });

该行的含义是...选择所有buyersGroups,如果为空,则返回new Buyer() { Name = string.Empty, District = s.District }(因为您将其定义为默认值)。

在第二个示例中

from bG in buyersGroup.DefaultIfEmpty() select new { Name = bG.Name == null ? string.Empty : bG.Name, s.District, };

您首先定义默认值,如果组为空,则为buyersGroup.DefaultIfEmpty(),然后才进行选择。请仔细注意DefaultIfEmpty括号的内容。

编辑

我似乎找不到需要使用DefaultIfEmpty的原因...不认为您可以在左侧获取null...这可能会使代码稍微容易理解一些。


2
请参见Enumerable.DefaultIfEmpty。在您的第一个示例中,您将new Buyer作为默认值传递给该扩展方法,以便在联接中找不到Buyer实例时返回(即您的外部联接)。之后没有选择语句,所以联接的结果在您的结果中被建模为IEnumerable<IEnumerable<Buyer>>
您的第二个查询确实使用了带有匿名投影的select,这就是为什么它会导致平坦化集合的原因。
纯粹看您的结果类型
  • you could mimic the results of the first query by removing the select in the second query

    from s in suppliers
    orderby s.District
    join b in buyers on s.District equals b.District into buyersGroup
    from bG in buyersGroup.DefaultIfEmpty();
    
  • To go the other direction, you could add the same select statement from the 2nd query at the end of the first.

    from s in suppliers
    orderby s.District
    join b in buyers on s.District equals b.District into buyersGroup
    select buyersGroup.DefaultIfEmpty(
        new Buyer()
        {
            Name = string.Empty,
            District = s.District
        ))
    select new
    {
        Name = bG.Name == null ? string.Empty : bG.Name,
        s.District,
    };
    
关于为什么要使用其中一种,这是个人观点的问题,也取决于具体情况。在你的非常简单的示例中,没有其他上下文,使用投影会更易读。如果您想将结果传递给另一个方法或从执行方法返回,则 IEnumerable<IEnumerable<Buyer>> 是唯一的方法(如果限制在这两个示例中)。如果您必须在数据存储中执行此操作,则建议您分析查询以查看正在执行的查询以及进行分析。简而言之,在特定的实际情况下,除非存在可测量的差异,并且该度量值和所应用的权重取决于该情况,否则没有正确/错误的答案。

谢谢,我明白了,目前唯一的问题(也是我的主要问题)是何时使用这两种方法中的任何一种。这两种方法之间唯一的“有意义的区别”是其中一种返回某种类型,而另一种返回匿名类型吗? - Darkbound

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接