cbp
cbp

Reputation: 25628

Why do (lazy) LINQ queries "act strange" compared to strictly-evaluated lists?

Why does this test fail?

        private class TestClass
        {
            public string Property { get; set; }
        }

        [Test]
        public void Test()
        {
            var testClasses = new[] { "a", "b", "c", "d" }
                .Select(x => new TestClass());

            foreach(var testClass in testClasses)
            {
                testClass.Property = "test";
            }

            foreach(var testClass in testClasses)
            {
                Assert.That(!string.IsNullOrEmpty(testClass.Property));
            }
        }

The problem is obviously to do with lazy yielding in the Select statement, because if I add a .ToList() call after the Select() method, the test passes.

Upvotes: 1

Views: 156

Answers (3)

Marc Gravell
Marc Gravell

Reputation: 1062745

Because of how LINQ works, you are actually creating 8 different versions of TestClass - one set of 4 per foreach. Essentially the same as if you had:

var testClasses = new[] { "a", "b", "c", "d" };
foreach(var testClass in testClasses.Select(x => new TestClass()))
{
    testClass.Property = "test";
}

foreach(var testClass in testClasses.Select(x => new TestClass()))
{
    Assert.That(!string.IsNullOrEmpty(testClass.Property));
}

The first set (discarded) have the property set.

By calling ToList() at the end of testClasses, you force it to store and re-use the same 4 TestClass instances, hence it passes.

Upvotes: 5

Darin Dimitrov
Darin Dimitrov

Reputation: 1038770

It's because LINQ extension methods such as Select return IEnumerable<T> which are lazy. Try to be more eager:

var testClasses = new[] { "a", "b", "c", "d" }
    .Select(x => new TestClass())
    .ToArray();

Upvotes: 0

Isak Savo
Isak Savo

Reputation: 35884

Every time you iterate over the testClasses variable, you will run the code in the .Select() lambda expression. The effect in your case is that the different foreach loops gets different instances of TestClass objects.

As you noticed yourself, sticking a .ToList() at the end of the query will make sure the ot is only executed once.

Upvotes: 2

Related Questions