Zum Hauptinhalt springen

Yield in csharp

  • lets a stateful method be called mutiple times. "pausing" between each call.
Console.WriteLine("returning a list");
var people = DataAccess.GetPeople();
foreach(var p in people)
Console.WriteLine($"Read {p.FirstName} {p.LastName}");


Console.WriteLine("\nreturning with yield");
var ypeople = DataAccess.GetYieldedPeople();
foreach(var p in ypeople)
Console.WriteLine($"Read {p.FirstName} {p.LastName}");

public static class DataAccess {

// we first create all objects, then hold them in memory for the whole duration
// and pass down a pointer to that whole object
public static IEnumerable<PersonModel> GetPeople() {
List<PersonModel> output = new();
output.Add(new PersonModel("Tim", "Hernandez"));
output.Add(new PersonModel("Adam", "Ondra"));
output.Add(new PersonModel("James", "Bod"));
return output;
}


// with yield we are able to only execute till the yield and return that
public static IEnumerable<PersonModel> GetYieldedPeople() {
yield return new PersonModel("Tim", "Hernandez"); // the program only runs this line on the first .next() call
yield return new PersonModel("Adam", "Ondra"); // and only this line on the 2nd call
yield return new PersonModel("James", "Bod"); // and only this line on the last call
}
}

public class PersonModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
public PersonModel(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
System.Console.WriteLine($"Initialized user {FirstName} {LastName}");
}
}
  • in our logs we can see that we really only created the objects before returning in the 2nd case. While in the first example creating all 3 at once, before any return happens.

benefits of yield

  • yield does add a bit of overhead. (the state of the method/closure it is run in)
  • memory wise it often can be much more efficient. And allocations, especially on the heap or those long lived gc-objects. If those can be avoided it is always a big benefit
  • Calls of .Take(2) or .FirstOrDefault() or .TakeLast(1) can be used, that do not collect the whole Iterator.
    • Take(2) will only try to take 2 of the iterator (and cast it as an IEnumerable again)
    • while Last(), First(), LastOrDefault() will directly return the object (if it exists)
  • when working with real databases this is a great tool to use together with pagination.

example of the consumer declaring how many to take

// nrs 1-50
var primeNumbers = Generators.GetPrimes();
var firstNumbers = primeNumbers.Take(50);
Console.WriteLine(String.Join("\n", firstNumbers));

// if we want the next 50-100 we must be explicit like this:
var secondNubers = primeNumbers.Skip(50).Take(50);
foreach (var nr in secondNubers)
Console.WriteLine(nr);



public class Generators
{
public static IEnumerable<int> GetPrimes()
{
int counter = 0;

while (true)
{
if (IsPrime(counter))
{
yield return counter;
}
counter ++;
}
}

private static bool IsPrime(int value)
{
bool output = true;

for (int i = 2; i <= value / 2; i++)
{
if (value % i == 0)
{
output = false;
break;
}
}
return output;
}
}

underlying enumerator

  • This is the Iterator that gets used.
  • this unlike the IEnumerable "saves it's state"
// the underlying Enumerator
var iterator = primeNumbers.GetEnumerator();
for (int i=0; i<10; i++) {
if(iterator.MoveNext()) {
Console.WriteLine(iterator.Current);
} else {
Console.WriteLine("no more elements in Iterator");
}
}

// the iterator unlike the IEnumerable will remember it's state
iterator.MoveNext();
Console.WriteLine('the 11th number: ' + iterator.Current);