看來閱讀一個開發人員的blog是獲取知識的一個捷徑,特別是當那位開發人員負責的產品是你天天都用的基礎設施之一,例如說……編譯器。在閱讀Eric Lippert的blog時,我無意中了解到了很多我以前所不熟悉的知識,例如說一些語言特性,一些編程思想之類;但更有趣的,我了解到了很多他所負責的產品中的詭異地方。
開篇花絮:
假如我們現在有一個枚舉類型E,其中有一個枚舉值的名字是x。
你或許知道這個表達式是對的:
C#代碼
0 | E.x
但是你或許不知道這個表達式(根據語言規范應該)是錯的:
C#代碼
0 | 0 | E.x
對此感到好奇的請到原文查看詳情:The Root Of All Evil, Part One
錯誤在於,C# 2.0的規范中說明“字面量0”可以被轉化為任意枚舉類型。是“字面量0”,而不是“編譯時常量0”。
這這這...Aargh, it's driving me nuts! (模仿Eric的語氣
如果你把下面的代碼放到.NET Framework 3.5 Beta 2中編譯測試的話,會看到編譯器完全沒對上面提及的第二種情況作出警告:
C#代碼
enum E {
x = 1
}
class Program {
public static void Main(string[] args) {
E e = 0 | 0 | E.x;
}
}
編譯器會抱怨局部變量e沒有被使用過(也就潛在意味著這個變量沒有作用,是多余的),但並沒對這裡我們關心的問題給出警告。正好剛裝上了.NET Framework 3.5的RTM,測試結果仍然一樣。Mono 1.2.5.1的在這點上的行為與前述一致。
在Unified C# 3.0 Specification的1.10 Enum中,規定了
引用
In order for the default value of an enum type to be easily available, the literal 0 implicitly converts to any enum type.
與前幾個版本的規定沒怎麼改變,仍然是說“字面量0”而不是“編譯時常量0”可以被轉換為任意枚舉類型。
於是.NET Framework與Mono都“很無奈”的在這點上無法與規范保持一致了。=_=||
C#裡派生類的方法裡的匿名delegate調用基類的方法會產生無法驗證的代碼
原文:Why are base class calls from anonymous delegates nonverifiable?
前面開篇花絮裡提到的是沒有熟思而做的優化帶來的後果,而下面要關注的問題就稍微復雜一些了。
考慮這段代碼片段:
引用
C#代碼
using System;
public delegate void D( );
public class Alpha {
public virtual void Blah( ) {
Console.WriteLine( "Alpha.Blah" );
}
}
public class Bravo : Alpha {
public override void Blah( ) {
Console.WriteLine( "Bravo.Blah" );
base.Blah( );
}
public void Charlie( ) {
int x = 123;
D d = delegate {
this.Blah( );
base.Blah( );
Console.WriteLine( x );
};
d( );
}
}
class Program {
// do nothing, just to make the compiler happy
// else we'd compiler with /target:library
public static void Main(string[] args) { }
}
用.NET Framework 3.5 Beta 2附帶的C#編譯器(csc.exe)編譯上面的代碼,會得到以下警告:
引用
Microsoft (R) Visual C# 2008 Compiler Beta 2 version 3.05.20706.1 for Microsoft (R) .NET Framework version 3.5
版權所有 (C) Microsoft Corporation。保留所有權利。
test1.cs(23,13): warning CS1911: 從匿名方法、lambda表達式、查詢表達式或迭代器通過“base”關鍵字訪問成員“Alpha.Blah()”會導致代碼無法驗證。請考慮將這種訪問移入針對包含類型的輔助方法中。
剛裝了.NET Framework 3.5的RTM,測試結果一樣。至於Mono 1.2.5.1更有趣,完全沒有報錯。
這裡有什麼問題呢?Charlie()方法裡用this/base去訪問自身/基類的成員,不是很正常的麼。問題出在C#中應對閉包生成的代碼。
在C# 2.0中,引入了匿名delegate的概念,因而可以定義嵌套方法;在C# 3.0中,更進一步引入了Lambda Expression,同樣可以用於定義嵌套方法。這裡,嵌套的方法的作用域遵守詞法作用域,也就是說內部方法可以訪問外部包圍作用域的變量,包括外部的“this”。外部包圍作用域就對嵌套內部方法形成了“閉包”。
由於當一個嵌套方法生成(實例化)後,它的生命周期與它的外部方法不一定相同。它從外部環境中“捕獲”到的變量,就像是從外部“逃逸”出來了一樣。上面的例子中,Charlie()方法裡x和this都成為了逃逸變量。
這些逃逸變量必須與嵌套方法的生命周期相同,即使外部方法已經返回也不能被立即銷毀;因此這些逃逸變量也不能在棧上分配。這樣,就需要為逃逸變量另外分配空間,常見的做法是在堆上分配。
Unified C# 3.0 Specification中,
引用
7.14.4 Outer variables
Any local variable, value parameter, or parameter array whose scope includes the lambda-expression or anonymous-method-expression is called an outer variable of the anonymous function. In an instance function member of a class, the this value is considered a value parameter and is an outer variable of any anonymous function contained within the function member.
7.14.4.1 Captured outer variables
When an outer variable is referenced by an anonymous function, the outer variable is said to have been captured by the anonymous function. Ordinarily, the lifetime of a local variable is limited to execution of the block or statement with which it is associated (§5.1.7). However, the lifetime of a captured outer variable is extended at least until the delegate or expression tree created from the anonymous function becomes eligible for garbage collection.
不同語言為閉包分配空間的具體方式不同。C#中,編譯器會將逃逸變量提升為成員變量,並將匿名delegate提升為一個成員方法——不過並不是提升到原本的類中,而是一個由編譯器構造的私有內部類中。上面的代碼,會被編譯器變成類似以下的形式:
引用
C#代碼
public class Bravo : Alpha {
public override void Blah() {
Console.WriteLine("Bravo.Blah");
base.Blah();
}
// compiler generated inner class
private class __locals {
public int __x;
public Bravo __this;
public void __method() {
this.__this.blah();
// on the next line, no such "__nonvirtual__" in C#
__nonvirtual__ ((Alpha)this.__this).Blah());
Console.WriteLine(this.__x);
}
}
public void Charlie() {
__locals locals = new __locals();
locals.__x = 123;
locals.__this = this;
D d = new D(locals.__method);
d();
}
}
當然這只是偽代碼。C#中並沒有"__nonvirtual__"關鍵字。一般來說,C#中的方法調用都是通過callvirt的IL指令完成的;而通過base關鍵字所做的方法調用則不遵循虛函數要使用最具體版本的規則,因而使用的是call的IL指令來完成。這裡所謂"__nonvirtual__"就是要表現這個意思。
可以看到,原本代碼中匿名delegate裡對base的訪問,實際上被生成到了另外一個類(私有內部類)的方法中,而那個類的"base"其實應該是System.Object……於是就有問題了。關鍵字“base”本來應該只能在同一個繼承系的派生類中使用,這樣生成的代碼就像是讓“base”的作用范圍洩露了一般。沒錯,編譯出來的代碼確實是能運行,卻變得不可驗證(unverifiable)。
但這並不是使用.NET Framework的程序員的錯;他們只是想在正確的地方正確的使用base而已。所以.NET Framework的應對方法是給出一個警告信息,提醒程序員修改代碼來避開這個問題。不幸的是,Mono並沒有提供任何警告提示。用Mono 1.2.5.1編譯上面的代碼,並用.NET Framework的PEVerify來驗證,會看到下面的錯誤信息:
引用
Microsoft (R) .NET Framework PE Verifier. Version 3.5.20706.1
Copyright (c) Microsoft Corporation. All rights reserved.
[IL]: Error: [F:\FX\share\testClosure.exe : Bravo+<>c__CompilerGenerated0::<Charlie>c__1][offset 0x00000011] The 'this' parameter to the call must be the calling method's 'this' parameter.
1 Error Verifying testClosure.exe
錯誤所對應的IL代碼為:
Java代碼
IL_0011: call instance void Alpha::Blah()
也就是原本的base.Blah()。
一個有趣的觀察:雖然C#的語言規范中沒有說明具體該如何為閉包生成代碼,但.NET Framework與Mono所做的幾乎是一樣的。這大概是因為Mono要盡量保持與.NET Framework的兼容吧。
前面的例子是用匿名delegate,在C# 3.0中換成Lambda Expression也一樣:
C#代碼
public void Charlie( ) { // int x = 123;
int x = 123;
D d = ( ) => {
this.Blah( );
base.Blah( );
Console.WriteLine( x );
};
d( );
}
上面提到了編譯器會生成不可驗證代碼的狀況。不過要是把前面例子中Charlie()裡的x給去掉,變成這樣的話:
C#代碼
public void Charlie( ) {
D d = delegate {
this.Blah( );
base.Blah( );
};
d( );
}
那麼.NET Framework的C#編譯器能發現唯一的逃逸變量是this,於是不會生成一個私有的內部類,而是直接將那個匿名delegate生成為Bravo的一個私有成員方法。也就是生成類似這樣的代碼:
C#代碼
public class Bravo : Alpha {
public override void Blah() {
Console.WriteLine("Bravo.Blah");
base.Blah();
}
// compiler generated method
public void __method() {
this.blah();
// on the next line, no such "__nonvirtual__" in C#
__nonvirtual__ ((Alpha)this).Blah());
}
public void Charlie() {
D d = new D(this.__method);
d();
}
}
換句話說,當逃逸變量只有this時,編譯器並不會生成不可驗證的變量。不過為了外表上行為的一致性,.NET Framework的C#編譯器仍然會給出跟上面一樣的警告。
Mono方面則是沒有做這樣的優化,仍然會與前面所說的狀況一樣,生成不可驗證的代碼。