Summary

Erik Dietrich demonstrates a subtle C# bug caused by misunderstanding yield return: a method returning IEnumerable<T> via yield doesn’t return a collection — it returns a state machine (a “promise to generate values”). Mutating the yielded values and re-iterating produces unexpected results because the state machine regenerates fresh values on each enumeration.

Erik Dietrich 演示了一個因誤解 yield return 導致的 C# 微妙錯誤:透過 yield 回傳 IEnumerable<T> 的方法不是回傳集合,而是回傳一個狀態機(「生成值的承諾」)。修改 yielded 值後重新迭代會產生意外結果,因為狀態機每次枚舉都會重新生成新值。

Key Points

  • yield return generates a compiler-created state machine, not a collection
  • Each time you iterate a yield-based IEnumerable, it runs the generator from scratch
  • Mutating objects retrieved from a yield enumerable doesn’t change the generator’s behavior — mutations are discarded
  • Fix: materialize the sequence immediately with .ToList() before mutation
  • LINQ expressions have the same laziness: any LINQ chain is deferred until enumerated
  • Stateful enumerables (streams, RNGs) produce different results on each enumeration — this is intentional but surprising

Insights

The mental model shift needed is: GetPoints() doesn’t “return points” — it “returns a points factory.” Modifying a factory’s output doesn’t change the factory. This is a surprisingly deep consequence of deferred execution: the IEnumerable abstraction conflates “sequence definition” with “sequence realization” in a way that frequently causes bugs. The .ToList() pattern is the canonical fix, but understanding why it works (it forces immediate realization) matters more than memorizing the fix. The same issue appears in any lazy language feature or library.

Connections

Raw Excerpt

A method that returns an IEnumerable and does so using yield return isn’t defining a return value — it’s defining a protocol for interacting with client code.