简体   繁体   中英

Strange behavior with LINQ to Objects

I'm seeing a strange behavior in my code, here's an analogous example using apples and persons, but the code is basically the same:

List<Apple> apples = ...
var selectableApples = apples.Select(a => new SelectableApple { SelectedByPerson = null, Apple = a });

foreach (Person person in persons)
{
    foreach (var unselectedApple in selectableApples.Where(aa => aa.SelectedByPerson == null))
    {
        if (/*the person satisfies some conditions*/)
        {
            // This gets executed like 100 times:
            unselectedApple.SelectedByPerson = person;
        }
    }
}

foreach (var selectedApple in selectableApples.Where(aa => aa.SelectedByPerson != null))
{
    Unreachable code - the collection is empty... WTF???
}

The SelectableApple class is just a plain C# class without logic, and public getters and setters for all the properties.

Why does this happen?

Thanks in advance!

The selectedApples is not a collection that contains objects, it's an expression that creates a collection on the fly. That means that the changes that you do to the objects are discarded, and when you loop selectedApples again it will be recreated from scratch.

Make it a collection using the ToList method:

var selectableApples = apples.Select(a => new SelectableApple { SelectedByPerson = null, Apple = a }).ToList();

There are a couple of issues here. The first being that the Where statement does not produce a list of objects. It is an expression statement.

Expression statements evaluate on the fly so changes to the objects produced are discarded each time you run the statement. Believe it or not this is a desirable result. This allows for you to handle complex nested for statements in a way that is more efficient and elegant.

The best way to answer your question is by analyzing what you have written and rework some of the code to show you a better way.

In your code:

List<Apple> apples = ...
var selectableApples = apples.Select(a => new SelectableApple { SelectedByPerson = null, Apple = a });

foreach (Person person in persons)
{
    foreach (var unselectedApple in selectableApples.Where(aa => aa.SelectedByPerson == null))
    {
        // This will ideally give all apples to the first person who
        // meets the conditions. As such this if condition can be moved
        // out side of the above the foreach loop.
        if (/*the person satisfies some conditions*/)
        {
            // This gets executed like 100 times:
            unselectedApple.SelectedByPerson = person;
        }
    }
}

foreach (var selectedApple in selectableApples.Where(aa => aa.SelectedByPerson != null))
{
    Unreachable code - the collection is empty... WTF???
}

So if we rework this code so that the if statement is out side of the inner loop. Your code will do the same logical thing. Mind you this does not yet fix the problem but takes you one step closer. Here is how the code will look:

List<Apple> apples = ...
var selectableApples = apples.Select(a => new SelectableApple { SelectedByPerson = null, Apple = a });

foreach (Person person in persons)
{
    // Now we can see that since this will all apples to the first person
    // who satisfies the below conditions we are still doing to much. And it
    // still does not work.
    if (/*the person satisfies some conditions*/)
    {
        foreach (var unselectedApple in selectableApples.Where(aa => aa.SelectedByPerson == null))
        {
            // This gets executed like 100 times:
            unselectedApple.SelectedByPerson = person;
        }
    }
}

foreach (var selectedApple in selectableApples.Where(aa => aa.SelectedByPerson != null))
{
    Unreachable code - the collection is empty... WTF???
}

Now we have started to group things so that a simpler answer can be seen. Since the if statement means that only the first person to satisfy the condition will be the person who gets all the apples. So lets get rid of the outer foreach loop and condense that down to LINQ.

List<Apple> apples = ...
var selectableApples = apples.Select(a => new SelectableApple { SelectedByPerson = null, Apple = a }); 
var selectedPerson = persons.Where(p => /*the person satisfies some conditions*/).First()

if(selectedPerson != null)
{
    foreach (var unselectedApple in selectableApples.Where(aa => aa.SelectedByPerson == null))
    {
        // This gets executed like 100 times:
        unselectedApple.SelectedByPerson = person;
    }
}

foreach (var selectedApple in selectableApples.Where(aa => aa.SelectedByPerson != null))
{
    Unreachable code - the collection is empty... WTF???
}

Looking at the above code, we can now see that the inner loop is just a modification on the original select. So lets look at that:

List<Apple> apples = ...
var selectedPerson = persons.Where(p => /*the person satisfies some conditions*/).First()
var selectableApples = apples.Select(a => new SelectableApple { SelectedByPerson = selectedPerson, Apple = a }); 


foreach (var selectedApple in selectableApples.Where(aa => aa.SelectedByPerson != null))
{
    // This should now run provided that some person passes the condition.
}

Now your code will run as desired, and you can take advantage of the lazy loading and the looping optimization provided within LINQ.

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