Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

IEnumerable<T> from Enumerable.FromRange().Select() vs ToList()

This really stumped me, as I expected 'pass by reference' behavior. I expected this code to print "5,5,5", but instead it prints "7,7,7".

IEnumerable<MyObj> list = Enumerable.Range(0, 3).Select(x => new MyObj { Name = 7 });
Alter(list);
Console.WriteLine(string.Join(',',list.Select(x => x.Name.ToString())));
Console.ReadLine();

void Alter(IEnumerable<MyObj> list)
{
    foreach(MyObj obj in list)
    {
        obj.Name = 5;
    }
}

class MyObj
{
    public int Name { get; set; }
}

Whereas this prints "7,7,7" as expected.

IEnumerable<MyObj> list = Enumerable.Range(0, 3).Select(x => new MyObj { Name = 7 }).ToList();
Alter(list);
Console.WriteLine(string.Join(',',list.Select(x => x.Name.ToString())));
Console.ReadLine();

void Alter(IEnumerable<MyObj> list)
{
    foreach(MyObj obj in list)
    {
        obj.Name = 5;
    }
}

class MyObj
{
    public int Name { get; set; }
}

Obviously this is a simplified version of the actual code I was writing. This feels a lot more like behavior I've run into with a property that instantiates a new instance of an object. Here I understand why I would get a new instance every time I reference Mine.

public MyObj Mine => new MyObj();

I was just very surprised to see this behavior in the above code, where it feels more like I've "locked in" the enumerated objects. Can anyone help me understand this?

like image 751
Andrew Avatar asked Oct 24 '25 17:10

Andrew


2 Answers

Select produces an object that has lazy evaluation. This means all steps are executed on demand.

So, when this line gets executed:

IEnumerable<MyObj> list = Enumerable.Range(0, 3).Select(x => new MyObj { Name = 7 });

no MyObj instance is created yet - only the enumerable that has all the information to compute what you've indicated.

When you call Alter, this gets executed during iteration via foreach and all objects get 5 assigned to their properties.

But when you print it, everything gets executed again here:

Console.WriteLine(string.Join(',',list.Select(x => x.Name.ToString())));

So during execution of string.Join, brand new MyObj instances are created with new MyObj { Name = 7 } and printed.

like image 134
BartoszKP Avatar answered Oct 27 '25 08:10

BartoszKP


This feels a lot more like behavior I've run into with a property that instantiates a new instance of an object

Your feelings are correct

As a preface to this answer I want to point out that in C# we can store methods inside variables like we can store data:

var method = () => new MyObj();

The () => new MyObj() is the "method"; it has no name, takes no parameters and returns a new MyObj. We don't actually refer to it as a method, we tend to call it a lambda, but for all intents and purposes it behaves like what you're familiar with as "a method":

public MyObj SomeName(){
    return new MyObj();
}

You can probably pick out the common parts- the compiler guesses the return type, provides a name for it internally because we don't care and when it's a single statement that produces a value, the compiles fills in the return keyword for us too. It's a very compact method logic.

So, back to this variable called method:

var method = () => new MyObj();

You could run it like:

var myObj = method.Invoke();

You could even remove the Invoke word and just write method(), but I'll leave Invoke in for now, for clarity. When invoked the method will run, return a new MyObj data and that data would be stored in myObj..

So, most times when we code, a variable stores data but sometimes it stores a method, and this is handy..


When you make a LINQ Select it requires you to give it a method that it will run every time the resulting enumerable is enumerated:

var enumerable = new []{1,2,3}.Select(x => new MyObj());

It doesn't run the method x => new MyObj() you give at the time you call Select, it doesn't generate any data; it just stores the method for later

Every time you loop over (enumerate) this it will run that method (x => new MyObj()) up to 3 times - I say up to, because you don't have to fully enumerate but if you do, there are 3 elements in the array so the enumeration can cause at most 3 invocations of the method.

LINQ Select doesn't run the method and grab the data - it effectively creates a collection of data-providing methods rather than a collection of data

Conceptually it's like Select produces you an array of full of methods:

var enumerable = new[]{
  () => new MyObj(),
  () => new MyObj(),
  () => new MyObj()
};

You could enumerate this and run these methods yourself:

foreach(var method in enumerable)
  method.Invoke();

Your Alter() ran the enumeration (did a loop), got each new object returned from each method, modified it, then it was thrown away. Conceptually your Alter method did this:

foreach(var method in enumerable)
  method.Invoke().Name = 5;

The method was run, a new MyObj was made with some default value for Name, then Name was changed to 5, then the object data was thrown away

After your Alter was done with you enumerated it again, new objects were made again because the methods were run again, the Name was of course unaltered - new object, no changes. Nothing here is geared up to remember the data being generated; the only thing that is remembered is the list of methods that generate the data


When you put a ToList() on the end, that's different - ToList internally enumerates the enumerable, causes the methods that generate the objects to be run but critically it stashes the resulting objects in a list and gives you the list of objects. Instead of being a collection of methods that provides values, it's a collection of values that you can alter.

It's like it does this:

var r = new List<MyObj>();

foreach(var method in enumerable)
  r.Add(method.Invoke());

return r;

This means you get a list of data back (not a list of methods that provide data), and if you then loop ovet this list you're altering data that is stored in the list, rather than running methods, altering the returned value and throwing it away

like image 41
Caius Jard Avatar answered Oct 27 '25 07:10

Caius Jard



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!