C# 2.0 引入的 yield return 不只是語法糖,更是編譯器自動生成有限狀態機,在單執行緒上實現協同多任務。本文深度剖析協程本質、C# 實作機制、跨語言對比(Ruby、C++ STL、Boost.Lambda、Win32 Fiber),並展望現代 async/await 的繼承關係。
協程 = 行為 + 控制(邏輯+狀態)
foreach 與 yield return 在同一執行緒交替執行,介於「順序執行」與「搶佔式執行緒」之間:調度由程式碼邏輯隱式協同完成,而非 OS 強制切換。
public class Tokens : IEnumerable<string>
{
private readonly string[] _elements = { "a", "b", "c" };
public IEnumerator<string> GetEnumerator()
{
for (int i = 0; i < _elements.Length; i++)
yield return _elements[i]; // 編譯器介入點
}
}
// 消費端
foreach (var item in new Tokens())
Console.WriteLine(item);
private sealed class <GetEnumerator>d__0 : IEnumerator<string>, IEnumerator, IDisposable
{
private int <>1__state; // 狀態:-2=結束, -1=執行中, 0=初始, 1=yield return 後
private string <>2__current; // 當前 yield 的值
public Tokens <>4__this; // 外部 this 參考
private int <i>5__1; // 區域變數 i 被提升為欄位
public string Current => <>2__current;
public bool MoveNext()
{
switch (<>1__state)
{
case 0:
<>1__state = -1;
<i>5__1 = 0;
goto case 1;
case 1:
if (<i>5__1 < <>4__this._elements.Length)
{
<>2__current = <>4__this._elements[<i>5__1];
<>1__state = 1;
<i>5__1++;
return true; // yield return
}
<>1__state = -2;
return false;
}
return false;
}
public void Reset() => throw new NotSupportedException();
public void Dispose() { }
// IEnumerator.Current 顯式實作...
}
i 變成 <i>5__1,跨 MoveNext 呼叫保持狀態<>1__state 記錄執行位置,switch 跳轉
def textfiles(dir)
Dir.chdir(dir)
Dir["*"].each do |entry|
yield dir + "\\" + entry if /\.txt$/.match?(entry)
textfiles(entry) { |file| yield dir + "\\" + file } if File.directory?(entry)
end
Dir.chdir("..")
end
textfiles("C:\\") { |file| puts file }
Ruby 的 yield 直接將控制權交給區塊,語言層面內建協程語義。
vector<int> v = {1,2,3};
for_each(v.begin(), v.end(), [](int& x){ x *= -2; }); // C++11 Lambda
for_each) 表達控制邏輯
for_each(v.begin(), v.end(), _1 *= -2); // 運算子重載構建表達式樹
仍需顯式傳遞迭代器範圍,控制邏輯/狀態未內建。
public class Tokens : IEnumerable
{
public IEnumerator GetEnumerator() => new TokenEnumerator(this);
private class TokenEnumerator : IEnumerator
{
private int _pos = -1;
private readonly Tokens _t;
public TokenEnumerator(Tokens t) => _t = t;
public bool MoveNext() => ++_pos < _t._elements.Length;
public void Reset() => _pos = -1;
public object Current => _t._elements[_pos];
}
}
控制邏輯鎖死在 MoveNext,擴展性極差。
// 偽代碼流程
ConvertThreadToFiber();
fiber = CreateFiber(FiberProc, mainFiber);
while (cur >= 0) { Print(cur); SwitchToFiber(fiber); }
DeleteFiber(fiber);
// C# 2.0 匿名方法 (閉包捕獲區域變數)
list.ForEach(delegate(int x) { Console.WriteLine(x * 2); });
// 相當於 Boost.Lambda,但編譯器自動生成閉包類別
// 為後來的 Lambda (C# 3.0) 與 async/await (C# 5.0) 鋪路
| 版本 | 特性 | 本質 | 應用場景 |
|---|---|---|---|
| C# 2.0 | yield return | 同步協程/序列生成器 | IEnumerable、資料管線 |
| C# 2.0 | 匿名方法 | 閉包/行為內嵌 | 回調、事件處理 |
| C# 3.0 | Lambda 運算式 | 匿名方法語法糖 | LINQ、表達式樹 |
| C# 5.0 | async/await | 非同步協程/狀態機 | I/O 非同步、UI 響應 |
async/await 可視為 yield return 的非同步泛化:
yield return:同步產出值,呼叫端 MoveNext() 驅動await:非同步暫停,Task 完成時由 SynchronizationContext 排程續體yield return 實現延遲執行、記憶體效率高的序列yield 迭代器中混入副作用(I/O、鎖),導致行為不可預測IAsyncEnumerable<T> (C# 8.0) + await foreachReset() 通常拋異常yield 在 try/finally 中需注意 Dispose() 呼叫時機結語:C# 的協程之路,是從編譯器生成同步狀態機 (yield) 起步,經由閉包捕獲語義 (匿名方法/Lambda),最終演化為運行時排程非同步狀態機 (async/await)。理解這條脈絡,不僅能寫出更高效的迭代器,更能深刻掌握現代 C# 非同步程式設計的底層邏輯。下一步,嘗試手寫一個簡易的 Task 排程器,親自體驗狀態機如何驅動非同步流程。