简体   繁体   中英

Understanding DefaultIfEmpty in LINQ

I don't understand how DefaultIfEmpty method works. It is usually used to be reminiscent of left-outer join in LINQ.

  • DefaultIfEmpty() method must be run on a collection .
  • DefaultIfEmpty() method cannot be run on null collection reference.

A code example I don't understand some points that

  • Does p , which is after into keyword, refer to products ?
  • Is ps the group of product objects? I mean a sequence of sequences.
  • If DefaultIfEmpty() isn't used, doesn't p , from p in ps.DefaultIfEmpty() , run into select ? Why?

,

#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

Code from 101 Examples of LINQ .

I ain't generally answer my own question, however, I think some people might find the question somewhat intricate. In the first step, the working logic of the DefaultIfEmpty method group should be figured out(LINQ doesn't support its overloaded versions, by the by).

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");

When being run, seeing that it gives null entered l2 out result out. What if l1.Add(null); is commented in? It is at your disposal, not hard to guess at all.

l2 has an item which is of null since foo is not one of the building block types like Int32 , String , or Char . If it were, default promotion would be applied to, eg for string, " " (blank character) is supplied to.

Now let's examine the LINQ statement being mentioned.

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.

在此处输入图像描述

The followed image, albeit not belonging to C#, helps to get what it means.

In the light of the lazy evaluation, we are now wisely cognizant of the fact that the LINQ using query expression is evaluated when requested, that is, on-demand.

在此处输入图像描述

So, ps contains product items iff the equality expressed at on keyword of join is satisfied. Further, ps has different product items at each demand of the LINQ expression. Otherwise, unless DefaultIfEmpty() is used, select is not hit thereby not iterating over and not yielding any Console.WriteLine($"{productName}: {category}"); . (Please correct me at this point if I'm wrong.)

Answers

Does p refer to products after into keyword?

The p in the from clause is a new local variable referring to a single product of one category.

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

Yes, ps is the group of products for the category c . But it is not a sequence of sequences, just a simple IEnumerable<Product> , just like c is a single category, not all categories in the group join.

In the query you only see data for one result row , never the whole group join result. Look at the final select , it prints one category and one product it joined with. That product comes from the ps group of product that one category joined with.

The query then does the walking over all categories and all their groups of products.

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

It is not equal to a Select , because the from clause creates a new join with itself, which turns into SelectMany .

Structure

Taking the query by parts, first the group join:

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

After this only c and ps are usable, representing a category and its joined products.

Now note that the whole query is in the same form as:

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

Which joins Cars with its own Passengers using Cars.SelectMany(car => car.Passengers, (car, passenger) => (car, passenger));

So in your query

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

creates a new join of the previous group join result with its own data (lists of grouped products) ran through DefaultIfEmpty using SelectMany.

Conclusion

In the end the complexity is in the Linq query and not the DefaultIfEmpty method. The method is simply explained on the MSDN page i posted in comment. It simply turns a collection with no elements into collection that has 1 element, which is either the default() value or the supplied value.

Compiled source

This is approximately the C# code the query gets compiled to:

        //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)
        );

Done with the help of ILSpy using C# 8.0 view.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM