In C#, when implementing a method that returns a collection or sequence (a series of data), it is often recommended to use the yield return syntax instead of creating and returning a collection like List<T>.
Using yield allows the compiler to automatically generate a state machine, enabling “Lazy Evaluation,” where values are generated one by one at the exact timing the caller requires them. This makes it possible to represent infinitely continuing numerical sequences and minimize memory consumption.
Here, using a sequence that calculates powers of 2 (1, 2, 4, 8…) as a subject, I will explain how to implement iterators using yield return and how to define termination conditions using yield break.
Table of Contents
- Mechanism of yield return
- Practical Code Example: Generating a Sequence of Powers of 2
- Execution Result
- Technical Points
- Improved Memory Efficiency
- Affinity with LINQ
- Overflow Protection and yield break
- Summary
Mechanism of yield return
Methods containing yield return define their return value as IEnumerable<T> (or IEnumerator<T>). Actual processing does not execute when this method is called; instead, execution begins only when elements are requested via a foreach loop or similar mechanism.
yield return value;: Returns the value to the caller, saves the current processing position, and pauses execution.yield break;: Terminates the generation of the sequence (completes the iteration).
Practical Code Example: Generating a Sequence of Powers of 2
The following code is an implementation example of an iterator method that starts at 1 and successively returns values doubled from the previous one. It also incorporates a safety mechanism to stop generation immediately before exceeding the range of the long type (overflowing).
using System;
using System.Collections.Generic;
using System.Linq;
namespace IteratorSample
{
class Program
{
static void Main()
{
// Scenario:
// Generate a sequence of Powers of 2 and use it according to conditions.
// Although GeneratePowersOfTwo contains an infinite loop,
// it is controlled by the caller (LINQ), so only the necessary amount is calculated.
var powers = GeneratePowersOfTwo()
.Select((Value, Index) => new { Index, Value }) // Attach index
.TakeWhile(x => x.Index <= 10); // Get only the first 11 items (2^0 to 2^10)
Console.WriteLine("--- Sequence of Powers of 2 (2^0 to 2^10) ---");
foreach (var item in powers)
{
Console.WriteLine($"2^{item.Index} = {item.Value}");
}
Console.WriteLine("\n--- Enumerating until just before overflow ---");
// Case of enumerating up to the limit of the long type.
// Since yield break is used in GeneratePowersOfTwo, it does not become an infinite loop.
int count = 0;
foreach (var value in GeneratePowersOfTwo())
{
count++;
}
Console.WriteLine($"Total generated items: {count}");
}
/// <summary>
/// Iterator method that sequentially returns powers of 2.
/// </summary>
/// <returns>Sequence of long type</returns>
static IEnumerable<long> GeneratePowersOfTwo()
{
long current = 1;
// Return 2^0 (= 1)
yield return current;
// Takes the form of an infinite loop, but pauses at each yield return.
while (true)
{
// Check for overflow before calculating the next value
// Terminate if current * 2 exceeds long.MaxValue
if (current > long.MaxValue / 2)
{
// Terminate iteration
yield break;
}
current *= 2;
// Return current value and pause processing
yield return current;
}
}
}
}
Execution Result
--- Sequence of Powers of 2 (2^0 to 2^10) ---
2^0 = 1
2^1 = 2
2^2 = 4
2^3 = 8
2^4 = 16
2^5 = 32
2^6 = 64
2^7 = 128
2^8 = 256
2^9 = 512
2^10 = 1024
--- Enumerating until just before overflow ---
Total generated items: 63
Technical Points
1. Improved Memory Efficiency
When creating and returning a List<long> in a standard method, all calculation results must be held in memory. However, when using yield, only the current value and state are held in memory, making it extremely memory-efficient when handling large amounts of data (or infinite data).
2. Affinity with LINQ
Since methods implemented with yield return return IEnumerable<T>, they can be combined directly with LINQ methods (such as Take, Where, Select).
In the example above, the GeneratePowersOfTwo method does not specify “how far to calculate”; instead, the caller determines the amount of data needed using .TakeWhile(...). Being able to separate the logic of data “generation” and “consumption” is a major advantage of iterators.
3. Overflow Protection and yield break
Even when handling infinite sequences, it is important to establish termination conditions for safety. In the code above, yield break is executed before performing a calculation that would exceed the maximum value of the long type, terminating the sequence normally. This prevents the program from crashing or falling into an infinite loop even if the caller forgets to limit it with Take or similar methods.
Summary
By using yield return, you can create efficient and readable iterators without writing complex state machines. Actively utilize this when you want to treat data as a “stream,” such as reading a huge file line by line or generating computationally expensive sequences.
