2

I have noticed strange Linq select behavior in program below:

IEnumerable<string> s = new List<string>() { "1", "2", "3" };
var i = s.Select(url =>
    {
        Console.WriteLine(url);
        url = string.Format("--{0}--",url);
        return url;
    }
);

Console.WriteLine("done with selector");
foreach (string f in i)
{
    Console.WriteLine("f is {0}", f);
}

Output is:

1
f is --1--
2
f is --2--
3
f is --3--

I was expecting output to be :

1
2
3
f is --1--
f is --2--
f is --3--

How to explain such strange behavior? Is it some kind of code optimization?

Sergey Kalinichenko
  • 714,442
  • 84
  • 1,110
  • 1,523
vico
  • 17,051
  • 45
  • 159
  • 315
  • 6
    Why did you expect the second output? LINQ is lazy evaluated. – tkausl Nov 08 '17 at 17:33
  • 7
    This is called "deferred execution". Yes, it's some kind of code optimization. The code to generate the sequence is not executed until someone actually iterates it. – Blorgbeard Nov 08 '17 at 17:33
  • Related: https://stackoverflow.com/questions/7324033/what-are-the-benefits-of-a-deferred-execution-in-linq – Blorgbeard Nov 08 '17 at 17:35

3 Answers3

3

Your code is a perfect illustration to the way deferred execution works in LINQ:

  • The line "done with selector" is printed first because Select "remembers" the info for computing the sequence, without actually computing it
  • The lines coming from the lambda inside Select and from WriteLine inside the loop are interleaved, because the sequence is generated as you continue enumerating it
  • If you break out of the loop in the middle of the sequence, the rest of printouts from the lambda will never come.

If you add ToList after Select, your code will produce the sequence "eagerly", leading to the output that you expect (demo).

Sergey Kalinichenko
  • 714,442
  • 84
  • 1,110
  • 1,523
1

Try this.

    IEnumerable<string> s = new List<string>() { "1", "2", "3" };
    var i = s.Select(url =>
        {
            Console.WriteLine(url);
            url = string.Format("--{0}--",url);
            return url;
        }
    ).ToList();

    Console.WriteLine("done with selector");
    foreach (string f in i)
    {
        Console.WriteLine("f is {0}", f);
    }

Due to deferred execution, the lambda expression in the first line will only execute when the result is iterated in the second line.

This behavior is completely correct.

Here is a very good explanation.

bgarcia
  • 51
  • 1
  • 2
0

What you were expecting was for the query to execute and populate the variable i where you define the query.

This behaviour can be forced (and often is unnecessarily in less well written code) by casting .ToList() after the select like so:

IEnumerable<string> s = new List<string>() { "1", "2", "3" };
var i = s.Select(url =>
    {
        Console.WriteLine(url);
        url = string.Format("--{0}--",url);
        return url;
    }
).ToList();

Console.WriteLine("done with selector");
foreach (string f in i)
{
    Console.WriteLine("f is {0}", f);
} 

but it is rarely necessary.

This behaviour is similar to that experienced when using the yield and yield return keywords in c#. It may be worth you looking into them a little.

Bijan Rafraf
  • 393
  • 1
  • 7