How to force my lambda expressions to evaluate ear

2019-04-20 18:19发布

问题:

I have written the following C# code:

_locationsByRegion = new Dictionary<string, IEnumerable<string>>();
foreach (string regionId in regionIds)
{
    IEnumerable<string> locationIds = Locations
        .Where(location => location.regionId.ToUpper() == regionId.ToUpper())
        .Select(location => location.LocationId); //If I cast to an array here, it works.
    _locationsByRegion.Add(regionId, LocationIdsIds);
}

This code is meant to create a a dictionary with my "region ids" as keys and lists of "location ids" as values.

However, what actually happens is that I get a dictionary with the "region ids" as keys, but the value for each key is identical: it is the list of locations for the last region id in regionIds!

It looks like this is a product of how lambda expressions are evaluated. I can get the correct result by casting the list of location ids to an array, but this feels like a kludge.

What is a good practice for handling this situation?

回答1:

You're using LINQ. You need to perform an eager operation to make it perform the .Select. ToList() is a good operator to do that. List is generic it can be assigned to IEnumberable directly.

In the case where you're using LINQ it does lazy evaluation by default. ToList/eager operations force the select to occur. Before you use one of these operators the action is not performed. It is like executing SQL in ADO.NET kind of. If you have the statement "Select * from users" that doesn't actually perform the query until you do extra stuff. The ToList makes the select execute.



回答2:

Your closing over the variable, not the value.

Make a local copy of the variable so you capture the current value from the foreach loop instead:

_locationsByRegion = new Dictionary<string, IEnumerable<string>>();
foreach (string regionId in regionIds)
{
    var regionToUpper = regionId.ToUpper();
    IEnumerable<string> locationIds = Locations
        .Where(location => location.regionId.ToUpper() == regionToUpper)
        .Select(location => location.LocationId); //If I cast to an array here, it works.
    _locationsByRegion.Add(regionId, LocationIdsIds);
}

Then read this:

http://msdn.microsoft.com/en-us/vcsharp/hh264182

edit - Forcing a eager evaluation would also work as others have suggested, but most of the time eager evaluations end up being much slower.



回答3:

Call ToList() or ToArray() after the Select(...). Thus entire collection will be evaluated right there.



回答4:

Actually the question is about lookup creation, which could be achieved simpler with standard LINQ group join:

var query = from regionId in regionIds
            join location in Locations
            on regionId.ToLower() equals location.regionId.ToLower() into g
            select new { RegionID = regionId, 
                         Locations = g.Select(location => location.LocationId) };

In this case all locations will be downloaded at once, and grouped in-memory. Also this query will not be executed until you try to access results, or until you convert it to dictionary:

var locationsByRegion = query.ToDictionary(x => x.RegionID, x => x.Locations);