Alejo
Alejo

Reputation: 1923

Does Linq's IEnumerable.Select return a reference to the original IEnumerable?

I was trying to clone an List in my code, because I needed to output that List to some other code, but the original reference was going to be cleared later on. So I had the idea of using the Select extension method to create a new reference to an IEnumerable of the same elements, for example:

List<int> ogList = new List<int> {1, 2, 3};
IEnumerable<int> enumerable = ogList.Select(s => s);

Now after doing ogList.Clear(), I was surprised to see that my new enumerable was also empty.

So I started fiddling around in LINQPad, and saw that even if my Select returned different objects entirely, the behaviour was the same.

List<int> ogList = new List<int> {1, 2, 3};
IEnumerable<int> enumerable = ogList.Select(s => 5); // Doesn't return the original int
enumerable.Count().Dump(); // Count is 3
ogList.Clear();
enumerable.Count().Dump(); // Count is 0!

Note that in LINQPad, the Dump()s are equivalent to Console.WriteLine().

Now probably my need to clone the list in the first place was due to bad design, and even if I didn't want to rethink the design I could easily clone it properly. But this got me thinking about what the Select extension method actually does.

According to the documentation for Select:

This method is implemented by using deferred execution. The immediate return value is an object that stores all the information that is required to perform the action. The query represented by this method is not executed until the object is enumerated either by calling its GetEnumerator method directly or by using foreach in Visual C# or For Each in Visual Basic.

So then I tried adding this code before clearing:

foreach (int i in enumerable)
{
    i.Dump();
}

The result was still the same.

Finally, I tried one last thing to figure out if the reference in my new enumerable was the same as the old one. Instead of clearing the original List, I did:

ogList.Add(4);

Then I printed out the contents of my enumerable (the "cloned" one), expecting to see '4' appended to the end of it. Instead, I got:

5
5
5
5 // Huh?

Now I have no choice but to admit that I have no idea how the Select extension method works behind the scenes. What's going on?

Upvotes: 5

Views: 9429

Answers (4)

Alexandru Clonțea
Alexandru Clonțea

Reputation: 1886

List/List<T> are for all intents and purposes fancy resizable arrays. They own and hold the data for value types such as your ints or references to the data for reference types in memory and they always know how many items they have.

IEnumerable/IEnumerable<T> are different beasts. They provide a different service/contract. An IEnumerable is fictional, it does not exist. It can create data out of thin air, with no physical backing. Their only promise is that they have a public method called GetEnumerator() that returns an IEnumerator/IEnumerator<T>. The promise that an IEnumerator makes is simple: some item could be available or not at a time when you decide you need it. This is achieved through a simple method that the IEnumerator interface has: bool MoveNext() - which returns false when the enumeration is completed or true if there was in fact a new item that needed to be returned. You can read the data through a property that the IEnumerator interface has, conveniently called Current.

To get back to your observations/question: as far as the IEnumerable in your example is concerned, it does not even think about the data unless your code tells it to fetch some data.

When you are writing:

List<int> ogList = new List<int> {1, 2, 3};
IEnumerable<int> enumerable = ogList.Select(s => s);

You are saying: Listen here IEnumerable, I might come to you asking for some items at some point in the future. I'll tell you when I will need them, for now sit still and do nothing. With Select(s => s) you are conceptually defining an identity projection of int to int.

A very rough simplified, non-real-life implementation of the select you've written is:

IEnumerable<T> Select(this IEnumerable<int> source, Func<int,T> transformer) something like
{
    foreach (var i in source) //create an enumerator for source and starts enumeration
    {
        yield return transformer(i); //yield here == return an item and wait for orders
    }
}

(this explains why you got a 5 when expecting a for, your transform was s => 5)

For value types, such as the ints in your case: If you want to clone the list, clone the whole list or part of it for future enumeration by using the result of an enumeration materialized through a List. This way you create a list that is a clone of the original list, entirely detached from its original list:

IEnumerable<int> cloneOfEnumerable = ogList.Select(s => s).ToList();

Later edit: Of course ogList.Select(s => s) is equivalent to ogList. I'm leaving the projection here, as it was in the question.

What you are creating here is: a list from the result of an enumerable, further consumed through the IEnumerable<int> interface. Considering what I've said above about the nature of IList vs IEnumerable, I would prefer to write/read:

IList<int> cloneOfEnumerable = ogList.ToList();

CAUTION: Be careful with reference types. IList/List make no promise of keeping the objects "safe", they can mutate to null for all IList cares. Keyword if you ever need it: deep cloning.

CAUTION: Beware of infinite or non-rewindable IEnumerables

Upvotes: 12

Harald Coppoolse
Harald Coppoolse

Reputation: 30512

You have to be aware that an object that implements IEnumerable does not have to be a collection itself. It is an object that makes it possible to get an object that implements IEnumerator. Once you have the enumerator you can ask for the first element and for the next element until there are no more next elements.

Every LINQ function that returns an IEnumerable is not the sequence itself, it only enables you to ask for the enumerator. If you want a sequence, you'll have to use ToList.

There are several other LINQ functions that do not return an IEnumerable, but for instance a Dictionary, or only one element (FirstOrDefault(), Max(), Single(), Any(). These functions will get the enumerator from the IEnumerable and start enumerating until they have the result. Any will only have to check if you can start enumerating. Max will enumerate over all elements and remember the largest one. etc.

You'll have to be aware: as long as your LINQ statement is an IEnumerable of something, your source sequence is not accessed yet. If you change your source sequence before you start enumerating, the enumeration is over your changed source sequence.

If you don't want this, you'll have to do the enumeration before you change your source. Usually this will be ToList, but this can be any of the non-deferred function: Max(), Any(), FirstOrDefault(), etc.

List<TSource> sourceItems = ...
var myEnumerable = sourceItems
    .Where(sourceItem => ...)
    .GroupBy(sourceItem => ...)
    .Select(group => ...);

// note: myEnumerable is an IEnumerable, it is not a sequence yet.
var list1 = sourceItems.ToList();         // Enumerate over the sequence
var first = sourceItems.FirstOrDefault(); // Enumerate and stop after the first

// now change the source, and to the same things again
sourceItems.Clear();
var list1 = sourceItems.ToList();         // returns empty list
var first = sourceItems.FirstOrDefault(); // return null: there is no first element

So every LINQ function that does not return IEnumerable, will start enumerating over sourceItems as the sequence is at the moment that you start enumerating. The IEnumerable is not the sequence itself.

Upvotes: 1

Alexei - check Codidact
Alexei - check Codidact

Reputation: 23108

Provided answers explain why you are not obtaining a cloned list (due to deferred execution of some LINQ extension methods).

However, keep in mind that list.Select(e => e).ToList() will get a real clone only when dealing with value types such as int.

If you have a list of reference types you will receive a cloned list of references to existent objects. In this case you should consider one of the solutions provided here for deep-cloning or my favorite from here (which might be limited by object inner structure).

Upvotes: 1

John Wu
John Wu

Reputation: 52280

This is an enumerable.

var enumerable = ogList.Select(s => s);

If you iterate through this enumerable, LINQ will in turn iterate over the original resultset. Each and every time. If you do anything to the original enumerable, the results will also be reflected in your LINQ calls.

If you need to freeze the data, store it in a list instead:

var enumerable = ogList.Select(s => s).ToList();

Now you've made a copy. Iterating over this list will not touch the original enumerable.

Upvotes: 1

Related Questions