Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

List<T>.AddRange and the yield statement

I am aware that the yield keyword indicates that the method in which it appears is an iterator. I was just wondering how that works with something like List<T>.AddRange.

Let's use the below example:

static void Main()
{
    foreach (int i in MyInts())
    {
        Console.Write(i);
    }
}

public static IEnumerable<int> MyInts()
{  
    for (int i = 0; i < 255; i++)
    {
        yield return i;
    }
}

So in the above example after each yield, a value is returned in the foreach loop in Main and is printed to the console.

If we change Main to this:

static void Main()
{
    var myList = new List<int>();
    myList.AddRange(MyInts());
}

how does that work? Does AddRange get called for each int returned by the yield statement or does it somehow wait for all 255 values before adding the entire range?

like image 448
Flack Avatar asked Jul 26 '17 02:07

Flack


2 Answers

The implementation of AddRange will iterate over the IEnumerable input using the iterator's .MoveNext() method until all values have been produced by your yielding method. This can be seen here.

So myList.AddRange(MyInts()); is called once and its implementation forces MyInts to return all of it values before moving on.

AddRange exhausts all values of the iterator because of how is implemented, but the following hypothetic method would only evaluate the first value of the iterator:

public void AddFirst<T>(IEnumerable<T> collection)
{
    Insert(collection.First());
}

An interesting experiment while you play around with this is to add a Console.WriteLine(i); line in your MyInts method to see when each number is generated.

like image 186
dee-see Avatar answered Sep 23 '22 04:09

dee-see


Short answer: When you call AddRange, it will internally iterate every item in your IEnumerable and add to the list.

If you did something like this:

var myList = new List<int>();
myList.AddRange(MyInts());

foreach (int i in myList)
{
    Console.Write(i);
}

Then your values would be iterated twice, from the start to the end:

  • Once when adding to your list
  • Then in your for loop

Playing a bit

Now, let's suppose you created your own extension method for AddRange like this:

public static IEnumerable<T> AddRangeLazily<T>(this ICollection<T> col, IEnumerable<T> values)
{
    foreach (T i in values)
    {
        yield return i; // first we yield
        col.Add(i); // then we add
    }
}

Then you could use it like this:

foreach (int i in myList.AddRangeLazily(MyInts()))
{
    Console.Write(i);
}

...and it would be iterated twice as well, without going from the start to the end both times. It would lazily add each value to the list/collection and at the same time allow you to do something else (like printing it to output) after every new item being added.

If you had some sort of logic to stop the adding to the list in the middle of the operation, this should be helpful somehow.

The downside if this AddRangeLazily is: values will only be added to the collection once you iterate over AddRangeLazily like my code sample. If you just do this:

var someList = new List<int>();
someList.AddRangeLazily(MyInts());
if (someList.Any())
    // it wouldn't enter here...

...it won't add values at all. If you wanted that behaviour, you should use AddRange. Forcing the iterationg over AddRangeLazily method would work, though:

var someList = new List<int>();
someList.AddRangeLazily(MyInts());
if (someList.AddRangeLazily(MyInts()).Count())
    // it would enter here...thus adding all values to the someList

...however, depending on how lazy is the method you calling, it wouldn't iterate everything. For example:

var someList = new List<int>();
someList.AddRangeLazily(MyInts());
if (someList.AddRangeLazily(MyInts()).Any())
    // it would enter here, plus adding only the first value to someList

Since Any() is true as soon as any item exists, then Any() just needs one iterationg to return true, therefore it just needs the first item to be iterated over.

I actually don't remember having to do something like this, it was just to play around with yield.

Fiddle here!!!

like image 35
Alisson Reinaldo Silva Avatar answered Sep 23 '22 04:09

Alisson Reinaldo Silva