Phil
Phil

Reputation: 627

When does LINQ execute an updated data list?

I'm curious when execution occurs, especially when updating data and calling a second time. Is it whenever the query variable is being used, such as in the foreach statement? Or, is it when I update the list, such as nums[1] = 99?

    int[] nums = { 1, -2, 3, 0, -4, 5 };

    var posNums = from n in nums
                  where n > 0
                  select n;

    foreach (int i in posNums)
        Console.Write("" + i + " ");
    Console.WriteLine();

    nums[1] = 99;

    foreach (int i in posNums)
        Console.Write("" + i + " "); 
    Console.WriteLine(); 

Upvotes: 3

Views: 111

Answers (3)

Andrew Cooper
Andrew Cooper

Reputation: 32586

The posNums query will be executed each time you iterate over the result in the foreach's.

A simple way to see the this in action is to introduce a side-effect in the query. The compiler converts your query expression to:

var posNums = nums.Where(n => n > 0);

We can modify your code with a bit more console output and see exactly where things are getting executed:

int[] nums = { 1, -2, 3, 0, -4, 5 };

Console.WriteLine("Before query creation");

var posNums = nums.Where(n => { Console.WriteLine("   Evaluating " + n); return n > 0; });

Console.WriteLine("Before foreach 1");

foreach (int i in posNums)
    Console.WriteLine("   Writing " + i);

Console.WriteLine("Before array modification");

nums[1] = 99;

Console.WriteLine("Before foreach 2");

foreach (int i in posNums)
    Console.WriteLine("   Writing " + i);

Output is:

Before query creation
Before foreach 1
   Evaluating 1
   Writing 1
   Evaluating -2
   Evaluating 3
   Writing 3
   Evaluating 0
   Evaluating -4
   Evaluating 5
   Writing 5
Before array modification
Before foreach 2
   Evaluating 1
   Writing 1
   Evaluating 99
   Writing 99
   Evaluating 3
   Writing 3
   Evaluating 0
   Evaluating -4
   Evaluating 5
   Writing 5

Upvotes: 3

Servy
Servy

Reputation: 203827

The easiest way of seeing exactly what's going on is to actually build something equivalent to what Where would return and step through it. Here is an implementation that is functionally equivalent to Where (at least in as much as where the source sequence is iterated and the result).

I've omitted some of the performance optimizations to keep attention on what's important, and written out some operations "the long way" for clarity:

public static IEnumerable<T> Where<T>(
    this IEnumerable<T> source,
    Func<T, bool> predicate)
{
    return new WhereEnumerable<T>(source, predicate);
}

public class WhereEnumerable<T> : IEnumerable<T>
{
    private IEnumerable<T> source;
    private Func<T, bool> predicate;
    public WhereEnumerable(IEnumerable<T> source, Func<T, bool> predicate)
    {
        this.source = source;
        this.predicate = predicate;
    }
    public IEnumerator<T> GetEnumerator()
    {
        return new WhereEnumerator<T>(source.GetEnumerator(), predicate);
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

public class WhereEnumerator<T> : IEnumerator<T>
{
    private IEnumerator<T> source;
    private Func<T, bool> predicate;
    public WhereEnumerator(IEnumerator<T> source, Func<T, bool> predicate)
    {
        this.source = source;
        this.predicate = predicate;
    }

    public T Current { get; private set; }

    public void Dispose()
    {
        source.Dispose();
    }

    object IEnumerator.Current
    {
        get { return Current; }
    }

    public bool MoveNext()
    {
        while (source.MoveNext())
            if (predicate(source.Current))
            {
                Current = source.Current;
                return true;
            }
        return false;
    }

    public void Reset()
    {
        throw new NotImplementedException();
    }
}

It's also worth having, for reference, what a foreach loop is equivalent to:

foreach (int i in posNums)
    Console.Write("" + i + " ");

is equivalent to:

using(IEnumerator<int> iterator = posNums.GetEnumerator())
    while(iterator.MoveNext())
    {
        int i = iterator.Current;
        Console.Write("" + i + " ");
    }

So now you can walk through and see when the sequence's values are actually pulled. (I'd encourage you to walk through this code with a debugger, using this Where in place of LINQ's Where in your own code, to see what's going on here.)

Calling Where on a sequences doesn't affect the sequence at all, and the sequence changing doesn't affect the result of Where at all. It is when MoveNext is actually called that the enumerable begins to pull the values from the underlying enumerable, and MoveNext is called when you have a foreach loop (among other possibilities).

Something else that we can see here is that each time we call foreach we call GetEnumerator again, which gets a brand new enumerator form Where, which gets a brand new enumerator from the underlying source sequence. This means that each time you call foreach you're iterating the underlying sequence again, from the start.

Upvotes: 0

David
David

Reputation: 10708

Linq defers analysis until the sequence is iterated over, either by a Foreach statement or getting the iterator. Note that under the hood, .ToArray() and .ToList calls perform such an iteration. You can see this by using the method-call version and pressing F9 to breakpoint the passed in lambda.

var posNums = nums
    .Where(n => n > 0);

Note that because Linq functions create Enumerators, they will also re-evaluate all your query functions each time you iterate the sequence, so it is often advantageous to copy the collection to memory using .ToArray() if you want to perform multiple (or nested!) iterations over the results of the query. If you want to perform multiple iterations over the changing data source then you wan to reuse the same Linq result.

If you're curious, you can also view the source code the .NET framework uses for various Linq statements at the Reference Source

Upvotes: 6

Related Questions