18

Reading the Google C# style guide I came across this:

Generators vs containers Use your best judgement, bearing in mind: Generator code is often less readable than filling in a container. Generator code can be more performant if the results are going to be processed lazily, e.g. when not all the results are needed. Generator code that is directly turned into a container via ToList() will be less performant than filling in a container directly. Generator code that is called multiple times will be considerably slower than iterating over a container multiple times.

I had little trouble understanding most of the guide, but here I simply don't know what they are talking about! What is "generator code" ?

27

This code is a generator (Microsoft documentation refer to these as Iterator Methods, see also yield (C# Reference)):

public IEnumerable<string> GetHelloWorld()
{
    yield return "Hello";
    yield return "World";
}

It is a method that generates an iterator enumerable.

They are evaluated lazily, as you probably are aware. That is mentioned in the guideline:

Generator code can be more performant if the results are going to be processed lazily, e.g. when not all the results are needed

On the other hand, we can fill a container and return it:

public IEnumerable<string> GetHelloWorld()
{
    var list = new List();
    list.Add("Hello");
    list.Add("World");
    return list;
}

Not the best way to write that (we could have used a Collection Initializer, for example), but you get the idea.

This code is eager.

A container is just any collection or similar type that contains items. That is why they tell you this:

Generator code that is directly turned into a container via ToList() will be less performant than filling in a container directly.


I talk a little more about which one to use in my answer to yield return vs without yield return.

| improve this answer | |
  • Thanks, I had a suspicion that might be it, but nowhere in the official C# documentation does it ever use the term "generator" that I am aware of, so I think it could be clearer! – George Barwood Jun 23 at 13:21
  • 11
    @George, Google is using the more common language-agnostic term here. C# has a few of these annoying cases where they just make up their own term for something. – Karl Bielefeldt Jun 23 at 13:29
  • 2
    @GeorgeBarwood: To be fair, I don't think "generator" is the best name they could've picked for it, as you could eagerly generate a list of computed values as well (e.g. for(int i = 0; i < 100; i++) { list.Add("number " + i); }). This is one of those cases where you need to know that's it's the name for a specific thing and can't just divine it based on the name alone. – Flater Jun 23 at 13:35
  • 1
    FWIW, it does not seem C# has a specific name for those. The documentation for [docs.microsoft.com/en-us/dotnet/csharp/language-reference/… just refers to them as iterators, like any other IEnumerable/IEnumerator. – spectras Jun 24 at 7:30
  • 3
    That google documentation is going to become more confusing still when C# 9 lands with Source Generators – Damien_The_Unbeliever Jun 24 at 9:33
12

It seems that they are referring to code that generates sequences or lists, usually via LINQ or a yield return. The container they are talking about would be an array or list that has been created from that generator.

To the first point "Generator code is often less readable than filling in a container.": Which is easier to read?

var x = new [] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};

Or

IEnumerable<int> CreateNumbers()
{
    for (int i = 0; i < 10; i++)
    {
        yield return i;
    }
}

var x = CreateNumbers().ToArray();

The code to create that simple array via a generator (the second example) is far harder to read than the first one.

"Generator code can be more performant if the results are going to be processed lazily, e.g. when not all the results are needed." Say you have a generator function that will return all integers from 0 to 100,000,000. But say your calling code will process elements until it hits one that is evenly divisible by 10. What this is saying is that the generator will probably be better here because it won't waste the time and space to create a 100,000,000 element array up front when only 10 items will get used. However if you are going to use every element anyway, the generator will be a little slower because of the overhead of using a yield return.

"Generator code that is directly turned into a container via ToList() will be less performant than filling in a container directly." Goes back to the previous point that there is overhead associated with using yield return. If you can skip that, you will be better off.

"Generator code that is called multiple times will be considerably slower than iterating over a container multiple times." If you need to use a sequence more than once, generate it once and save it to use multiple times. It's faster and cheaper to generate once and reuse many times than to generate multiple times.

| improve this answer | |
  • It's too bad there isn't an overload for MoveNext that accepts a number of items to move forward; if such a thing existed, and were coupled with a variant of "yield return" that could yield a list, that would have allowed some kinds of generators to be much more efficient, especially if they could cache things like their length. – supercat Jun 23 at 22:01
  • 1
    @supercat surely you can achieve the same by creating custom Enumerators – IMil Jun 24 at 1:55
  • @supercat Do you mean Enumerable.Skip plus something like Python's yield from? – Caleth Jun 24 at 10:29
  • There's (unfortunately!) no such construct as yield from in C#. But you can workaround the lack of such a construct by using returns in your method, and declaring an inner local function that uses yield return. – Emanuel Vintilă Jun 24 at 11:05
  • @Caleth: The Enumerable.Skip call will in general get executed as a sequence of IEnumerator<T>.MoveNext calls. If one uses e.g. Enumerable.Concat to join two 10,000-item lists, and then calls Enumerable.Skip to advance 12,000 items, that will do 10,000 separate actions. If IEnumerator<T>.MoveNext` had allowed code to specify the number of items, and returned the difference between the requested and actual number moved, then the first list's enumerator could be asked advance 12,000 items, found that it moved all but 2,000, then the second list's enumerator asked to advance 2,000. – supercat Jun 24 at 15:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.