Reputation: 20891
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
p
, which is after into
keyword, refer to products
?ps
the group of product objects? I mean a sequence of sequences.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.
Upvotes: 2
Views: 3654
Reputation: 20891
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, e.g. 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.)
Upvotes: 3
Reputation: 706
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
.
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.
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.
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.
Upvotes: 0