我認為這是一個真命題:“沒有用.NET Reflector反編譯並閱讀過代碼的程序員不是專業的.NET程序 員”。.NET Reflector強大的地方就在於可以把IL代碼反編譯成可讀性頗高的高級語言代碼,並且能夠支 持相當多的“模式”,根據這些模式它可以在一定程度上把某些語法糖給還原,甚至可以支持簡單的 Lambda表達式和LINQ。只可惜,.NET Reflector還是無法做到極致,某些情況下生成的代碼還是無法還原 到易於理解——yield關鍵字便是這樣一個典型的情況。不過還行,對於不復雜的邏輯,我們可以通過人 肉來“整理”個大概。
簡單yield方法編譯結果分析
yeild的作用是簡化枚舉器,也就是IEnumerator<T>或IEnumerable<T>的實現。“人肉” 反編譯的關鍵在於發現編譯器的規律,因此我們先來觀察編譯器的處理結果。值得注意的是,我們這裡所 談的“分析”,都采用的是微軟目前的C# 3.0編譯器。從理論上來說,這些結果或是規律,都有可能無法 運用在Mono和微軟之前或今後的C#編譯器上。首先我們准備一段使用yield的代碼:
static IEnumerator<int> GetSimpleEnumerator()
{
Console.WriteLine("Creating Enumerator");
yield return 0;
yield return 1;
yield return 2;
Console.WriteLine("Enumerator Created");
}
為了簡化問題,我們在這裡采用IEnumerator<T>。自動生成的IEnumerable<T>和 IEnumerator<T>區別不大,您可以自己觀察一下,有機會我會單獨討論和分析其中的區別。經過編 譯之後再使用.NET Reflector進行反編譯,得到的結果是:
private static IEnumerator<int> GetSimpleEnumerator()
{
return new <GetSimpleEnumerator>d__0(0);
}
[CompilerGenerated]
private sealed class <GetSimpleEnumerator>d__0 : IEnumerator<int>, ...
{
// Fields
private int <>1__state;
private int <>2__current;
// Methods
[DebuggerHidden]
public <GetSimpleEnumerator>d__0(int <>1__state)
{
this.<>1__state = <>1__state;
}
private bool MoveNext()
{
switch (this.<>1__state)
{
case 0:
this.<>1__state = -1;
Console.WriteLine("Creating Enumerator");
this.<>2__current = 0;
this.<>1__state = 1;
return true;
case 1:
this.<>1__state = -1;
this.<>2__current = 1;
this.<>1__state = 2;
return true;
case 2:
this.<>1__state = -1;
this.<>2__current = 2;
this.<>1__state = 3;
return true;
case 3:
this.<>1__state = -1;
Console.WriteLine("Enumerator Created");
break;
}
return false;
}
...
}
以上便是編譯器生成的邏輯,它將yield關鍵字這個語法糖轉化為普通的.NET結構(再次強調,這只是 微軟目前的C# 3.0編譯器所產生的結果)。從中我們可以得出一些結論:
原本GetSimpleEnumerator方法中包含yield的邏輯不復存在,取而代之的是一個由編譯器自動生成的 IEnumerator類的實例。
原本GetSimpleEnumerator方法中包含yield的邏輯,被編譯器自動轉化為對應IEnumerator類中的 MoveNext方法的邏輯。
編譯器將包含yield邏輯轉化為一個狀態機,並使用自動生成的state字段保存當前狀態。
每次調用MoveNext方法時,都通過switch語句判斷state的值,直接進入特定的邏輯片斷,並指定下一 個狀態。
因為從yield關鍵字的作用便是“中斷”一個方法的邏輯,使它在下次執行MoveNext方法的時候繼續執 行。這就意味著自動生成的 MoveNext代碼必須通過某一個手段來保留上次調用結束之後的“狀態”,並 根據這個狀態決定下次調用的“入口”——這是個典型的狀態機的“思路”。由此看來,編譯器如此實現 ,其“設計”意圖也是比較直觀的,相信您理解起來也不會有太大問題。
較為復雜的yield方法
上一個例子非常簡單,因為GetSimpleEnumerator的邏輯非常簡單(只有“順序”,而沒有“循環”和 “選擇”)。此外,這個方法也沒有使用局部變量及參數,於是我們這裡不妨再准備一個相對復雜的方法 :
private static IEnumerator<int> GetComplexEnumerator(int[] array)
{
<GetComplexEnumerator>d__2 d__ = new <GetComplexEnumerator>d__2(0);
d__.array = array;
return d__;
}
[CompilerGenerated]
private sealed class <GetComplexEnumerator>d__2 : IEnumerator<int>, ...
{
// Fields
private int <>1__state;
private int <>2__current;
public int <i>5__4;
public int <i>5__6;
public int <sumEven>5__3;
public int <sumOdd>5__5;
public int[] array;
// Methods
[DebuggerHidden]
public <GetComplexEnumerator>d__2(int <>1__state)
{
this.<>1__state = <>1__state;
}
private bool MoveNext()
{
// 第一部分
switch (this.<>1__state)
{
case 0:
this.<>1__state = -1;
Console.WriteLine("Creating Enumerator");
this.<sumEven>5__3 = 0;
this.<i>5__4 = 0;
goto
Label_0094
;
case 1:
this.<>1__state = -1;
goto
Label_0086
;
case 2:
goto
Label_00F4
;
default:
goto
Label_0123
;
}
// 第二部分
Label_0086:
this.<i>5__4++;
Label_0094:
if (this.<i>5__4 < this.array.Length)
{
if ((this.array[this.<i>5__4] % 2) == 0)
{
this.<sumEven>5__3 += this.array[this.<i>5__4];
this.<>2__current = this.<sumEven>5__3;
this.<>1__state = 1;
return true;
}
goto
Label_0086
;
}
this.<sumOdd>5__5 = 0;
this.<i>5__6 = 0;
while (this.<i>5__6 < this.array.Length)
{
if ((this.array[this.<i>5__6] % 2) == 0)
{
goto
Label_00FB
;
}
this.<sumOdd>5__5 += this.array[this.<i>5__6];
this.<>2__current = this.<sumOdd>5__5;
this.<>1__state = 2;
return true;
Label_00F4:
this.<>1__state = -1;
Label_00FB:
this.<i>5__6++;
}
Console.WriteLine("Enumerator Created.");
Label_0123:
return false;
}
...
}
這下MoveNext的邏輯便一下子復雜了很多。我認為,這是由於編譯器期望生成體積小的代碼,於是它 使用了goto來進行自由的跳轉。其實從理論上說,把這個方法分為N個階段之後,便可以讓它們完全獨立 地分開,只不過此時各狀態間便會出現許多重復的邏輯。不過,這段代碼看似復雜,其實您仔細分析便會 發現,它其實也只是將代碼拆成了上下兩部分(如代碼注釋所示):
第一部分:狀態機的控制邏輯,即根據當前狀態進行跳轉。
第二部分:主體邏輯,只不過使用goto代替了普通語句中由for/if組成的邏輯,這麼做的目的是為了 插入Label,可以讓第一部分的代碼直接跳轉到合適的地方——換句話說,由第一部分跳轉到的Label便是 yield return出現的地方。
從上面的代碼中我們還可以看出方法的“參數”及“局部變量”的轉化規則:
參數被轉化為IEnumerator類的公開字段,命名方式不變,原本的array參數直接變成array字段。
局部變量被轉化為IEnumerator類的公開字段,並運用一定的命名規則改名(主要是為了避免和自動生 成的current及state字段產生沖突)。對於局部變量localVar,將被轉化為<localVar>X__Y的形式 。
其他需要自動生成的字段為<>1__state及<>2__current,它們只是進行輔助邏輯,不再 贅述。
至此,我們已經掌握了編譯器基本的轉化規律,可以將其運用到“人肉反編譯”的過程中去。
試驗:人肉反編譯OrderedEnumerable
事實上,.NET框架中的System.Linq.OrderedEnumerable類便是一個包含yield方法的邏輯,使用.NET Reflector得到的相關代碼如下:
internal abstract class OrderedEnumerable<TElement> : IOrderedEnumerable<TElement>, ...
{
internal IEnumerable<TElement> source;
internal abstract EnumerableSorter<TElement> GetEnumerableSorter (EnumerableSorter<TElement> next);
public IEnumerator<TElement> GetEnumerator()
{
<GetEnumerator>d__0<TElement> d__ = new <GetEnumerator>d__0<TElement>(0);
d__.<>4__this = (OrderedEnumerable<TElement>) this;
return d__;
}
[CompilerGenerated]
private sealed class <GetEnumerator>d__0 : IEnumerator<TElement>, ...
{
// Fields
private int <>1__state;
private TElement <>2__current;
public OrderedEnumerable<TElement> <>4__this;
public Buffer<TElement> <buffer>5__1;
public int <i>5__4;
public int[] <map>5__3;
public EnumerableSorter<TElement> <sorter>5__2;
[DebuggerHidden]
public <GetEnumerator>d__0(int <>1__state)
{
this.<>1__state = <>1__state;
}
private bool MoveNext()
{
switch (this.<>1__state)
{
case 0:
this.<>1__state = -1;
this.<buffer>5__1 = new Buffer<TElement> (this.<>4__this.source);
if (this.<buffer>5__1.count <= 0)
{
goto
Label_00EA
;
}
this.<sorter>5__2 = this.<>4__this.GetEnumerableSorter (null);
this.<map>5__3 = this.<sorter>5__2.Sort (this.<buffer>5__1.items, this.<buffer>5__1.count);
this.<sorter>5__2 = null;
this.<i>5__4 = 0;
break;
case 1:
this.<>1__state = -1;
this.<i>5__4++;
break;
default:
goto
Label_00EA
;
}
if (this.<i>5__4 < this.<buffer>5__1.count)
{
this.<>2__current = this.<buffer>5__1.items [this.<map>5__3[this.<i>5__4]];
this.<>1__state = 1;
return true;
}
Label_00EA:
return false;
}
...
}
}
很自然,我們需要“人肉反編譯”的便是OrderedEnumerable類的GetEnumerator方法。首先,為了便 於理解代碼,我們首先還原各名稱。既然我們已經知道了局部變量及current/state的命名規則,因此這 個工作其實並不困難:
private bool MoveNext()
{
switch (__state)
{
case 0:
__state = -1;
var buffer = new Buffer<TElement>(this.source);
if (buffer.count <= 0)
{
goto
Label_00EA;
}
var sorter = this.GetEnumerableSorter(null);
var map = sorter.Sort(buffer.items, buffer.count);
sorter = null;
var i = 0;
break;
case 1:
__state = -1;
i++;
break;
default:
goto
Label_00EA;
}
if (i < buffer.count)
{
__current = buffer.items[map[i]];
__state = 1;
return true;
}
Label_00EA:
return false;
}
值得注意的是,在上面的方法中,this是由原來的<>4__this字段還原而來,它表示的是 OrderedEnumerable類型(而不是自動生成的IEnumerator類)的實例。此外,其中的局部變量您需要將其 理解為“自動在多次MoveNext調用中保持狀態的變量”—— 這和C語言中的靜態局部變量有些接近。自然 ,__state和__current變量都是自動生成用於保存狀態的變量,我們姑且保留它們。
接下來,我們將要還原state等於0時的邏輯。因為我們知道,它其實是yield方法中“第一個yield return”之前的邏輯:
private IEnumerator<TElement> GetEnumerator()
{
var buffer = new Buffer<TElement>(this.source);
if (buffer.count <= 0) yield break;
var sorter = this.GetEnumerableSorter(null);
var map = sorter.Sort(buffer.items, buffer.count);
// 省略sorter = null(為什麼?:P)
var i = 0;
if (i < buffer.count)
{
yield return buffer.items[map[i]];
}
...
}
我們發現,在buffer.count小於等於0的時候MoveNext直接返回false了,於是在GetEnumerator方法中 我們便使用 yield break直接退出。在上面的代碼中我們已經還原至第一個yield return,那麼當調用下 一個MoveNext時(即state為1)邏輯又該如何進行呢?我們再“機械”地還原一下:
private IEnumerator<TElement> GetEnumerator()
{
...
i++;
if (i < buffer.count)
{
yield return buffer.items[map[i]];
}
else
{
yield break;
}
...
}
接著,我們會發現代碼會不斷重復上面這段邏輯,因此我們可以使用一個“死循環”將其包裝起來。 至此,GetEnumerator便還原成功了:
private IEnumerator<TElement> GetEnumerator()
{
var buffer = new Buffer<TElement>(this.source);
if (buffer.count <= 0) yield break;
var sorter = this.GetEnumerableSorter(null);
var map = sorter.Sort(buffer.items, buffer.count);
var i = 0;
if (i < buffer.count)
{
yield return buffer.items[map[i]];
}
while (true)
{
i++;
if (i < buffer.count)
{
yield return buffer.items[map[i]];
}
else
{
yield break;
}
}
}
不過,又有多少人會寫這樣的代碼呢?的確,這段代碼是我們“機械翻譯”的結果。不過經過觀察, 事實上這段代碼可以被修改成如下寫法:
private IEnumerator<TElement> GetEnumerator()
{
var buffer = new Buffer<TElement>(this.source);
if (buffer.count <= 0) yield break;
var sorter = this.GetEnumerableSorter(null);
var map = sorter.Sort(buffer.items, buffer.count);
for (var i = 0; i < buffer.count; i++)
{
yield return buffer.items[map[i]];
}
}
至此就完美了。最後這步轉換我們利用了人腦的優越性,這樣“看出”一種優雅的模式也並非難事— —不過這也並非只能靠“感覺”,因為我在上面談到,編譯器會盡可能生成緊湊的代碼,這意味著它和“ 源代碼”相比不會有太多的重復。但經由我們“機械還原”之後,會發現這樣一段代碼其實是重復出現的 :
if (i < buffer.count)
{
yield return buffer.items[map[i]];
}
於是我們便可以朝著“合並代碼片斷”的方向去思考,得到最終的結果還是有規律可循的。
總結
如果您關注我最近的文章,並且在看到OrderedEnumerable這個類型之後應該會有所察覺:這篇文章只 是我在“分析Array和LINQ排序實現” 過程中的一個插曲。沒錯,這是LINQ排序實現的一小部分。 OrderedEnumerable利用了yield關鍵字,這樣我們使用.NET反編譯之後代碼的可讀性很差。為此,我便特 地研究了一下對yield進行“人肉反編譯”的做法。不過在一開始,我原本其實是想仔細分析一下yield相 關的“編譯規律”,但是我發現在《C# in Depth》一書中已經對這個話題有了非常詳盡的描述,只得作 罷。
事實上,自從ASP.NET 2.0開始,我似乎就沒有看過任何一本ASP.NET 2.0/3.0或是C# 2.0/3.0/4.0的 書了,因為我認為這些書中的所有內容都可以從MSDN文檔,互聯網(如博客)以及自己使用、分析的過程 中了解到。不過現在,《C# in Depth》似乎讓我對此類技術圖書的“偏見”有所動搖了——但只此一本 而已,估計我還是不會去買這樣的書。