🔷 C#

المكررات والمولدات في C# (.NET): دليل مفصل

What is it and why? (a real-life analogy)

Imagine you are a librarian, and in front of you is a huge bookshelf with books. Your task is to rewrite the titles of all the books into a catalog. You have two ways to do this:

Method 1 (greedy): You take all the books off the shelf at once, pile them on your desk, and only then start rewriting. If there are 10,000 books, your desk will be cluttered, you won't have room to work, and if there are a million books, the desk will simply break under their weight.

Method 2 (lazy, but smart): You approach the shelf, look at the first book, write down its title, then move to the second, write it down — and so on until the end. At any given moment, you only have one book in your hands. Your desk remains empty, and your memory is not overloaded.

In programming, iterators are that very "smart librarian." They allow you to iterate over elements of a collection (list, array, database) one by one, without loading all the data into memory at once. And generators are a special kind of iterator that can not only iterate over ready-made data but also create it on the fly, right during the iteration.

💡 Important: Iterators and generators are not magic, but just a convenient design pattern that is built right into the C# language. They help you write code that runs fast and doesn't waste extra memory.

Why is this needed in real life? Imagine you are writing a program to read a huge log file of 10 gigabytes. If you load it entirely into memory, the program will crash with an OutOfMemoryException. But if you use an iterator, you can read the file line by line, processing each line as it arrives. Memory usage will be just a few kilobytes!

Or another example: you need to generate an infinite sequence of Fibonacci numbers. A regular list won't work — it can't be infinite. But a generator can! It will give you the next number only when you ask for it.

Theory: how it works internally

To understand iterators, you need to understand two key concepts: IEnumerable and IEnumerator. Don't be scared by these scary names — it's actually quite simple.

The IEnumerable interface — "promise to iterate"

Think of it as a "certificate" that says: "I know how to iterate over my elements." Any collection that can be iterated over in a foreach loop (list, array, dictionary) implements this interface. It has only one method: GetEnumerator(), which returns... well, the iterator itself.

The IEnumerator interface — "the iteration process itself"

This is the specific "librarian" who walks along the shelves. It has three important members:

  • Current — the current element we are on (the book the librarian is holding).
  • MoveNext() — move to the next element. Returns true if the move was successful, and false if there are no more elements.
  • Reset() — go back to the beginning (rarely used, but sometimes necessary).

When you write foreach (var item in collection), the C# compiler invisibly does this for you:

// What actually happens inside the foreach loop
IEnumerator enumerator = collection.GetEnumerator();
try
{
    while (enumerator.MoveNext())  // While there is a next element
    {
        var item = enumerator.Current;  // Get the current one
        // ... your code with item ...
    }
}
finally
{
    // Important! Always release resources
    if (enumerator is IDisposable disposable)
        disposable.Dispose();
}

Generators (yield return) — "lazy factory"

And now for the most interesting part. C# has a special keyword yield return. When you use it in a method, the compiler itself creates an entire iterator class for you! You just write what and when to return, and all the "mechanics" (state, transitions, Current/MoveNext) are generated automatically by the compiler.

This is like telling the librarian: "Walk along the shelves, and every time you see a book with a red cover, write down its title." You don't tell him exactly how to walk or how to write — he knows that himself. You only describe the selection rule.

🔬 How it works under the hood: The compiler turns a method with yield return into a state machine. This state machine has states: "before the first yield", "between yields", "after the last yield". Each time MoveNext() is called, the machine moves to the next state and returns the required value. This is very efficient!

Syntax in C# (.NET): all forms and variants

Let's look at all the ways to create iterators and generators in C#. There are several, and each is suitable for its own situation.

1. Manual implementation of IEnumerator

The lowest-level way. You write the class yourself that implements the IEnumerator interface. Rarely used, but useful for understanding.

2. Implementing IEnumerable via yield return

The most popular way. You write a method that returns IEnumerable<T> and uses yield return.

3. Using ready-made LINQ iterators

LINQ (Language Integrated Query) provides many methods that return iterators: Where, Select, Take, Skip, and others. They all work "lazily" — data is processed only when you start iterating over it.

4. Async iterators (C# 8.0+)

If your code works with networks or files, you can use await foreach