Traditional List<T> vs yield return in C#
Sometimes the difference isn’t “style”… it’s memory, latency, and control.
✅ 1) Traditional List<T>: eager execution
You build the whole result up front, then return it.
public static List<int> GetEvenNumbers_List(IEnumerable<int> source) { var result = new List<int>(); foreach (var n in source) if (n % 2 == 0) result.Add(n); return result; }
When it shines
- You need Count, Indexing, multiple enumerations
- You want to materialize once and reuse
- You want to avoid re-running expensive logic
Trade-offs
- Allocates memory for the entire list
- You pay the cost before the caller can consume anything
✅ 2) yield return: lazy execution (iterator)
You return items as they are produced, one by one.
public static IEnumerable<int> GetEvenNumbers_Yield(IEnumerable<int> source) { foreach (var n in source) if (n % 2 == 0) yield return n; }
When it shines
- Streaming / pipelines
- Large sequences (or even infinite sequences)
- You want the first items immediately (lower latency)
Trade-offs
- Each enumeration re-runs the logic
- Can surprise you if the source changes later
- Exceptions happen during enumeration, not at method call time
The “gotchas” you should know
⚠️ Multiple enumeration re-executes
var seq = GetEvenNumbers_Yield(numbers); seq.ToList(); // runs logic seq.ToList(); // runs logic AGAIN
If you want lazy generation but reuse the results, materialize once:
var cached = GetEvenNumbers_Yield(numbers).ToList();
⚠️ Exceptions happen later
var seq = GetEvenNumbers_Yield(GetNumbersThatMayThrow()); // no exception yet... foreach (var n in seq) { } // exception can happen here
That’s great for streaming… but can make debugging feel “delayed”.
⚠️ Resource lifetime matters
This is a classic bug: yield keeps execution alive across foreach.
public static IEnumerable<string> ReadLines(string path) { using var reader = File.OpenText(path); while (!reader.EndOfStream) yield return reader.ReadLine()!; }
This is fine because the using stays active while enumerating.
But if you return an iterator built on something disposed too early, you’ll get runtime surprises.
Quick rule of thumb
✅ Use List<T> when you need:
- random access /
Count/ multiple passes - stability + predictable execution timing
- caching results
✅ Use yield return when you need:
- streaming, large data, pipelines
- early results (low latency)
- composability with LINQ
One-liner takeaway
List<T> = eager + materialized
yield return = lazy + streamed
