2

In what cases foreach uses a ref, and in what cases foreach uses a copy?

using System;
using System.Linq;

class A {
    public int v;
}

class Program
{
    static void Main() {
        var ints = new int[] { 0, 1, 2 };
        
        var array = ints.Select(i=>new A {v = i}).ToArray();
        foreach(var a in array) {
            a.v = 999;
        }

        var enumerable = ints.Select(i=>new A {v = i});
        foreach(var a in enumerable) {
            a.v = 999;
        }

        Console.WriteLine($"array.First = {array.First().v}");
        Console.WriteLine($"enumerable.First = {enumerable.First().v}");
    }
}

jdoodle.com/ia/Jce

Output:

array.First = 999
enumerable.First = 0

Seems that in foreach(var a in enumerable) { a is copy not a ref, while in foreach(var a in array) { a is a ref.

Can someone explain this?

Wei Liu
  • 555
  • 8
  • 24
  • 7
    I think you misunderstand how reference-types and Linq's lazy-evaluation work - and what `ToArray` actually does... because `foreach` doesn't do what you think it does here. – Dai Jun 15 '23 at 02:14

3 Answers3

5

This is not really about foreach. It has more to do with how you constructed array and enumerable. foreach does the same thing in both cases (getting the enumerator and calling MoveNext and Current to iterate through the enumerable). It is the difference between enumerable and array that causes the difference in output.

array is an A[], so if you change the vs of its elements, and then get the first of those element, you will obviously see the change. A is a reference type, so a in the foreach is a reference to the element in the array.

enumerable however, is something produced by Select. Nothing substantial is done if you just call Select. It just creates an IEnumerable<A>, that when enumerated over, creates a bunch of A objects. The important point here is that the Select lambda is run whenever enumerable is being enumerated. If you don't enumerate it, nothing happens. This is called deferred execution.

So the second foreach enumerates enumerable, creating a bunch of A objects, and you then change the vs of those As. Here is the important difference - the A objects don't get stored anywhere, unlike with an array. You "threw" away the A object after each iteration.

When you call enumerable.First() at the end, you start enumerating enumerable again - only once this time because you want only the first element. And what does enumerable do? It creates a new A object by running the code in Select.

Sweeper
  • 213,210
  • 22
  • 193
  • 313
1

Enumerable.Select uses deferred execution and returns a query that is "executed" while using the foreach or GetEnumerator method. .ToArray() returns an [] that contains the objects.

The first for loop iterates over the actual objects and modifies the values of the elements, while the second for loop projects each element of the sequence. Executing .First() on enumerable, projects the elements again and uses the original ints to return the first element from the list

Bharat
  • 74
  • 3
1

As others have said, this is an instance of deferred execution vs non-deferred execution.

Take the following example:

internal class Program
{
    // Note where this gets set in Main!
    static int x = 0;

    private static void Main(string[] args)
    {
        int[] arr = { 1, 2, 3 };
        var a = arr.Select(f => {
            Console.WriteLine("Evaluated");
            return f + x;
        });

        // This will affect how the above Select evaluates things.
        x = 5;

        foreach (var d in a)
        {
            Console.WriteLine(d);
        }
    }
}

When we do our foreach, the code in the Select will be run for each item in a (i.e., we will enumerate over a and run the console writeline and f + x each time we grab an item in a).

As soon as we tack on ToArray(), the evaluation has to happen immediately, meaning we no longer defer execution.

In your example, since we call ToArray() immediately, it means we immediately get a bunch of references to A objects. Additionally, since the evaluation happens on that line, we have a place that we're storing them.

With your enumerable variable, the evaluation of new A() results in us creating an A object each time we iterate in the foreach, but the A object we just made isn't apart of the original enumerable collection, so it's lost.

You'll see things more clearly if you take my example and run it with and without a call to ToArray() on the line with Select().

Visual Studio
  • 184
  • 1
  • 10