了解 LINQ 中的 DefaultIfEmpty

Understanding DefaultIfEmpty in LINQ

我不明白 DefaultIfEmpty 方法是如何工作的。它通常让人想起 LINQ 中的 left-outer join。

一个代码示例我不明白的地方

,

#region left-outer-join
string[] categories = {
    "Beverages",
    "Condiments",
    "Vegetables",
    "Dairy Products",
    "Seafood"
};

List<Product> products = GetProductList();

var q = from c in categories
        join p in products on c equals p.Category into ps
        from p in ps.DefaultIfEmpty()
        select (Category: c, ProductName: p == null ? "(No products)" : p.ProductName);

foreach (var v in q)
{
    Console.WriteLine($"{v.ProductName}: {v.Category}");
}
#endregion

代码来自 101 Examples of LINQ.

答案

Does p refer to products after into keyword?

from子句中的p是一个新的局部变量,指的是一个类别的单个产品。

Is ps the group of product objects? I mean a sequence of sequences.

是的,ps 是类别 c 的产品组。但它不是一个sequences的sequences,只是一个简单的IEnumerable<Product>,就像c是一个单一的类别,并不是group中的所有类别都join。

在查询中您只能看到一个结果 的数据,而不会看到整个组连接结果。查看最后的 select,它打印 one 类别和 one 产品。该产品来自一个类别加入的ps产品组。

查询然后遍历所有类别及其所有产品组。

If DefaultIfEmpty() isn't used, doesn't p, from p in ps.DefaultIfEmpty(), run into select? Why?

它不等于 Select,因为 from 子句创建了一个新的与自身的连接,它变成了 SelectMany.

结构

分部查询,先加入群:

from c in categories
join p in products on c equals p.Category into ps

此后只有cps可用,代表一个类别及其加入的产品。

现在请注意,整个 查询的格式与:

from car in Cars
from passenger in car.Passengers
select (car, passenger)

它使用 Cars.SelectMany(car => car.Passengers, (car, passenger) => (car, passenger));

Cars 与其自己的 Passengers 连接起来

所以在你的查询中

from group_join_result into ps
from p in ps.DefaultIfEmpty()

使用 SelectMany 通过 DefaultIfEmpty 使用自己的数据(分组产品列表)运行 对先前的组连接结果创建新连接。

结论

最后,复杂性在于 Linq 查询而不是 DefaultIfEmpty 方法。我在评论中发布的 MSDN 页面上简单地解释了该方法。它只是将没有元素的集合转换为具有 1 个元素的集合,该元素是 default() 值或提供的值。

编译源码

这大约是查询编译成的 C# 代码:

        //Pairs of: (category, the products that joined with the category)
        IEnumerable<(string category, IEnumerable<Product> groupedProducts)> groupJoinData = Enumerable.GroupJoin(
            categories,
            products,
            (string c) => c,
            (Product p) => p.Category,
            (string c, IEnumerable<Product> ps) => (c, ps)
        );

        //Flattening of the pair collection, calling DefaultIfEmpty on each joined group of products
        IEnumerable<(string Category, string ProductName)> q = groupJoinData.SelectMany(
                    catProdsPair => catProdsPair.groupedProducts.DefaultIfEmpty(),
                    (catProdsPair, p) => (catProdsPair.category, (p == null) ? "(No products)" : p.ProductName)
        );

在 ILSpy 的帮助下使用 C# 8.0 视图完成。

我通常不会回答我自己的问题,但是,我认为有些人可能会觉得这个问题有些复杂。 第一步,弄清楚DefaultIfEmpty方法组的工作逻辑(LINQ不支持其重载版本,顺便说一句)。

class foo
{
    public string Test { get; set; }
}
// list1
var l1 = new List<foo>();
//l1.Add(null);     --> try the code too by uncommenting
//list2
var l2 = l1.DefaultIfEmpty();

foreach (var x in l1)
    Console.WriteLine((x == null ? "null" : "not null") + "  entered l1");

foreach (var x in l2)
    Console.WriteLine((x == null ? "null" : "not null") + "  entered l2");

当运行时,看到它给出了null entered l2 out的结果。 如果l1.Add(null);被注释了怎么办?任你摆布,一点也不难猜。

l2 有一个 itemnull 因为 foo 不是像 [=17= 这样的积木类型之一]、StringChar。如果是,默认促销将应用于,例如对于字符串," "(空白字符)提供给。

现在让我们检查一下提到的 LINQ 语句。

Just for a remembrance, unless an aggregate operator or a To{a collection}() is applied to a LINQ expression, lazy evaluation(honor deferred) is carried out.

下图虽然不属于 C#,但有助于理解其含义。

根据惰性求值,我们现在明智地认识到使用 查询表达式 的 LINQ 在请求时求值,即按需求值。

因此,ps 包含产品项,前提是满足 joinon 关键字表示的相等性。此外,ps 在 LINQ 表达式的每个需求中都有不同的产品项目。否则,除非使用 DefaultIfEmpty(),否则不会命中 select,从而不会迭代并且不会产生任何 Console.WriteLine($"{productName}: {category}");。 (如果我错了,请在这一点上纠正我。)