轉換使表達式可以當做一個明確的類型來加以處理。轉換使得所給定類型的表達式以不同類型來處理,或使得沒有某個類型的表達式獲得該類型。轉換可以是顯式或隱式的,而這決定了是否需要顯式地強制轉換。比方說,從類型 int
向類型 long
的轉換是隱式的,所以 int
類型表達式可以隱式地當做 long
的來處理。反過來轉換,從類型 long
轉換為 int
是顯式的,需要顯式的強制轉換(explicit cast)。
int a = 123;long b = a; // 從 int 到 long 隱式轉換int c = (int) b; // 從 long 到 int 顯式轉換
某些轉換由語言來定義。程序同樣可以定義它們子集的轉換(第六章第四節)。
以下轉換被分類為隱式轉換:
隱式換磚(implicit conversions)可發生于多種情況下,包括函數成員調用(第七章第 5.4 節)、強值轉換表達式(cast exPRessions,第七章第 7.6 節)以及賦值(第七章第十七節)。
預定義隱式轉換(pre-defined implicit conversions)總是成功的,永不會導致異常拋出。正確設計用戶定義隱式轉換(user-defined implicit conversions)也能表現(exhibit)出這些特征(characteristics)。
對于轉換的目的來說,object
和 dynamic
類型被視為等價。
然而,動態轉換(dynamic conversions,第六章第 1.8 節以及第六章第 2.6 節)僅適用于類型為 dynamic
(第四章第七節)的表達式。
標識轉換(identity conversion)可將任意類型轉換為其相同類型。這種轉換之所以存在,是因為要讓已是所需類型的實體能被認為是可轉換(為該類型)的。
由于 object
和 dynamic
被視為等價,所以在 object
和 dynamic
之間以及在即將出現的所有 dynamic
轉為 object
的轉換具有相同構造類型的之間,存在一個標識轉換。
隱式數值轉換(implicit numeric conversions)包括:
sbyte
到 short
、int
、long
、float
、double
或 decimal
;byte
到 short
、ushort
、int
、uint
、long
、ulong
、float
、double
或 decimal
;short
到 int
、long
、float
、double
或 decimal
;ushort
到 int
、uint
、long
、ulong
、float
、double
或 decimal
;int
到 long
、float
、double
或 decimal
;uint
到 long
、ulong
、float
、double
或 decimal
;long
到 float
、double
或 decimal
;ulong
到 float
、double
或 decimal
;char
到 ushort
、int
、uint
、long
、ulong
、float
、double
或 decimal
;float
到 double
。從 int、uint、long 或 ulong 轉換為 float,從 long 或 ulong 轉換為 double 會丟失精度(loss of precision),但不會導致數量級的丟失(loss of magnitude)。其它的隱式數制轉換不會丟失任何信息。
不存在任何向 char
類型的隱式轉換,所以任何數值類型的值都不會自動轉換為字符類型。
隱式枚舉轉換允許將 decimal-integer-literal 0 轉換為任何枚舉類型(enum-type)和任何基礎類型為枚舉類型(enum-type)的非空值類型(nullable-type)。在后一種情況下,這個轉換工作通過轉換為基礎枚舉類型并對結果進行封裝(第四章第 1.10 節)計算所得。
對不可為空值類型(non-nullable value types)的預定義隱式轉換操作也可以用于其可空類型的版本上。對于每個從非可空值類型 S
到非可空值類型 T
的預定義隱式標識和數值轉換,都存在如下可空的隱式轉換:
S?
到 T?
的隱式轉換;S
到 T?
的隱式轉換。基于從 S 到 T 的基礎轉換來計算隱式可空轉換的過程如下:
S?
到 T?
:T?
類型的 null 值;S?
解包為 S
,然后進行 S
到 T
的基礎轉換,最后將 T
包裝(第四章第 1.10 節)為 T?
。S
到 T?
,則轉換計算過程為從 S
到 T
的基礎轉換,然后從 T
包裝為 T?
。從空值文本(null literal)到可空類型(nullable type)存在隱式轉換。這種轉換能產生所給定可空類型的空值(null value,第四章第 1.10 節)。
隱式引用轉換(implicit reference conversions)是:
reference-type
到 object
或 dynamic
;class-type
S 到任何 class-type
T(前提是 S 是從 T 派生的);class-type
S 到任何 interface-type
T(前提是 S 是 T 的實現)interface-type
S 任何 interface-type
T(前提是 S 是從 T 派生的);array-type
S 到元素類型為 TE 的 array-type
T(前提是下列所有條件均為 true):reference-type
;array-type
到 System.Array
及其實現的接口;S[]
到 System.Collections.Generic.IList<T>
及其基接口(前提是存在從 S 到 T 的隱式標識或引用轉換);delegate-type
到 System.Delegate
及其實現的接口;reference-type
;reference-type
到 reference-type
T(前提是其具有到 reference-type
T0 的隱式標識或引用轉換,且 T0 具有到 T 的標識轉換);reference-type
到接口或委托類型 T(前提是具有到接口或委托類型 T0 的隱式標識或引用轉換,且 T0 為可變化轉換(variance-convertible,第十三章第 1.3.2 節)到 T);隱式引用轉換是在 reference-type
之間的轉換,可以證明這種轉換總能成功,故而不需要在「運行時」對其進行檢查。
引用轉換,不管是顯式還是隱式,都不會改變被轉換的對象的引用標識(referential identity)。換而言之,雖然引用轉換可以改變引用的類型,但不會改變所引用的對象的類型或值
裝箱轉換(boxing conversion)允許值類型隱式轉換為引用類型。從任何非可空值類型到 object
和 dynamic
、System.ValueType
以及 non-nullable-value-type
實現的任何 interface-type
都存在裝箱轉換。此外,enum-type
能被轉換為 System.Enum
類型。
存在從可空類型到引用類型的裝箱轉換,當且僅當該可空類型的不可空值類型存在向引用類型裝箱轉換。
如果值類型具有到接口類型 I0 的裝箱轉換,且 I0 具有到接口類型 I 的標識轉換,則值類型具有慈寧到 I 的裝箱轉換。
如果值類型具有到接口類型或委托類型 I0 的裝箱轉換,且 I0 可變化轉換(第十三章第 1.3.2 節)為接口類型 I,則值類型具有到接口類型 I 的裝箱轉換。
對非可空值類型的值的裝箱可以包括以下操作:分配一個對象實例,然后將值類型的值復制進該實例。結構可裝箱為類型 System.ValueType
,因為它是所有結構的基類型(第十一章第 3.2 節)。
對非可空類型的值的裝箱按以下步驟進行:
關于裝箱轉換的介紹請閱讀第四章第 3.1 節。
存在從 dynamic 類型表達式到任何類型 T 的隱式動態轉換(implicit dynamic conversion)。轉換是動態綁定的(dynamically bound,第七章第 2.2 節),這意味著在「運行時」能發現從表達式的「運行時」類型到類型 T 的隱式轉換。如果未發現任何轉換,則拋出「運行時」異常。
注意這種隱式轉換似乎違背了第六章第一節開頭部分的建議,隱式轉換不應該導致異常。但這不是轉換自身導致的異常,而是轉換時動詞「發現」(finding)所導致的異常?!高\行時」異常的風險是使用動態綁定所固有的。如果不需要動態綁定轉化,表達式首先轉換為一個 object,然后轉換為所需的類型。
下例說明了隱式動態轉換:
object o = “object”dynamic d = “dynamic”;string s1 = o; // 「編譯時」失敗,不存在轉換string s2 = d; // 編譯且「運行時」成功int i = d; // 編譯但「運行時」失敗,不存在轉換
對 s2
和 i
的賦值都使用了隱式動態轉換(implicit dynamic conversions),所綁定的操作會一直掛起,直到「運行時(run-time)」才執行。在「運行時」,可以看到從 d
的「運行時」類型(string)到目標類型的隱式轉換。轉換會找到 string 而不是 int。
隱式常量表達式轉換(implicit constant expression conversions)允許以下轉換:
int
類型的 constant-expression
(第七章第十九節)能被轉換為 sbyte
, byte
, short
, ushort
, uint
或 ulong
,所提供的轉換結果在目標類型的合法區間內。long
類型的 constant-expression
能被轉換為 ulong
,所提供的結果為非負數。對于給定類型形參 T 存在下列隱式轉換:
如果已知 T 是引用類型(第十章第 1.5 節),上述轉換都被劃歸為隱式引用轉換(第六章第 1.6 節)。如果不知道是不是引用類型,則劃歸為裝箱轉換(boxing conversions,第六章第 1.7 節),
用戶定義隱式轉換(user-defined implicit conversion)由可選的標準隱式轉換(optional standard implicit conversion)、執行用戶定義隱式轉換的操作符以及另一個可選的基礎隱式轉換等三部分組成。運行用戶定義隱式轉換的確切規則詳見第六章第 4.4 節。
匿名函數(anonymous function)與方法組(method groups)自身不具有類型(do not have types in and of themselves),然可隱式轉換為委托類型或表達式樹類型。匿名函數的轉換詳見第六章第五節,方法組轉換詳見第六章第六節。
下列轉換被劃歸為顯式轉換(explicit conversions):
顯式轉換可出現在強制轉換表達式(cast expressions,第七章第 7.6 節)內。
顯式轉換集包括所有隱式轉換,這意味著允許使用冗余的強制轉換表達式(redundant cast expressions)。
顯式轉換(排除隱式轉換的那部分)不能保證總能轉換成功,轉換可能會丟失信息,并且轉換前后類型顯著不同(sufficiently different)。
顯示數值轉換(explicit numeric conversions)是指從 numeric-type
到另一個 numeric-type
的轉換,這種轉換不能用已知的隱式數值轉換(第六章第 1.2 節)來實現,它包括:
由于顯式轉換包括所有隱式轉換和顯式數值轉換,所以其總可以使用強制轉換表達式(cast expression,第七章第 7.6 節)將任意 numeric-type
轉換為其它任意 numeric-type
。
顯式數值轉換可能會丟失信息或導致異常拋出。顯式數值轉換的處理過程如下:
System.OverflowException
異常。System.OverflowException
異常。System.OverflowException
異常。System.OverflowException
異常。System.OverflowException
異常。顯式枚舉轉換(explicit enumeration conversions)是:
sbyte
, byte
, short
, ushort
, int
, uint
, long
, ulong
, char
, float
, double
或 decimal
到任何 enum-type
;enum-type
到 sbyte
, byte
, short
, ushort
, int
, uint
, long
, ulong
, char
, float
, double
或 decimal
;enum-type
到任何其它 enum-type
。在兩個類型之間進行顯式枚舉轉換是通過處理任何參與的 enum-type
都按該 enum-type
的基礎類型處理,然后在產生的類型之間使用顯式或隱式的數值轉換。比方說,給定一個 enum-type
E,其基礎類型為 int,從 E 到 byte 的轉換會按從 int 到 byte 的顯式數值轉換(第六章第 2.1 節)來處理,而從 byte 到 E 的轉換則會按從 byte 到 int 的隱式數值轉換(第六章第 1.2 節)來處理。
顯式可空值轉換(explicit nullable conversions)允許對不可空值類型(non-nullable value types)及其可空值形式的類型執行預定義顯式轉換。對于每個從不可空值類型 S 到不可空值類型 T 的預定義顯式轉換(predefined explicit conversions)(第六章第 1.1 節,1.2 節,1.3 節,2.1 節以及 2.2 節),存在下列可空轉換:
S?
到 T?
的顯式轉換。S
到 T?
的顯式轉換。S?
到 T
的顯式轉換。基于從 S
到 T
的基礎轉換的可空轉換運算步驟如下:
S?
到 T?
的可空轉換:T?
的 null 值。S?
到 S
的解包,然后進行從 S
到 T
的基礎轉換,最后從 T
包裝為 T?
。S
到 T?
的可空轉換,那么轉換的運算過程是將 S
基礎轉換為 T
,然后將 T
包裝為 T?
。S?
到 T
的可空轉換,那么轉換的運算過程是將 S?
解包為 S
,然后從 S
基礎轉換為 T
。注意,如果對一個為 null 值的可空值進行解包會引發異常。
顯式引用轉換(explicit reference conversions)是:
object
和 dynamic
到任何其它 reference-type
class-type
S 到任何 class-type
T(前提是 S 為 T 的基類)class-type
S 到任何 interface-type
T(前提是 S 不是密封的(sealed)且 S 沒有實現 T)interface-type
S 到任何 class-type
T(前提是 T 不是密封的或 T 實現了 S)interface-type
S 到任何 interface-type
T(前提是 S 不是派生自 T 的)array-type
S 到元素類型為 TE 的 `array-type
T(前提是以下條件均為 true):System.Array
和它所實現的接口道任何 array-type
S[]
到 System.Collections.Generic.IList<T>
及其基接口(前提是具有從 S 到 T 的顯式引用轉換)System.Collections.Generic.IList<S>
及其基接口到單維度數組類型 T[]
(前提是具有從 S 到 T 的顯示標識或引用轉換)System.Delegate
及其所實現的接口道任何 delegate-type
顯式引用轉換是需要在「運行時」檢查其正確的 reference-type
之間進行的轉換。
為了顯式引用轉換在「運行時」成功,源操作數的值必須為空(null),或源操作數所引用對象的實際類型必須是一個能通過隱式引用轉換(第六章第 1.6 節)或裝箱轉換(第六章第 1.7 節)轉換為目標類型的類型。如果顯式引用轉換失敗,會拋出 System.InvalidCastException
異常。
引用轉換,無論是顯示還是隱式,都不會改變被轉換對象的引用標識(referential identity)。換句話說,雖然引用轉換可以改變所引用的類型,但從不會改變所引用對象的類型或值。
拆箱轉換(unboxing conversion)引用類型顯式轉換為值類型。存在從類型 object
、dynamic
和 System.ValueType
到任何 non-nullable-value-type
以及從任何 interface-type
到任何實現 interface-type
的 non-nullable-value-type
的拆箱轉換。而且,類型 System.Enum
可以拆箱為任何 enum-type
。
存在從引用類型到 nullable-type
的拆箱轉換,前提是存在從該引用類型到 nullable-type
的基礎類型 non-nullable-value-type
的拆箱轉換。
如果值類型 S
具有從接口類型 I
的拆箱轉換,且 I0 具有從接口類型到 I 的標識轉換,則其具有來自 I 的拆箱轉換。
如果值類型 S
具有來自接口類型或委托類型 I0 的拆箱轉換,且 I0 可變化轉換為 I 或 I 可變換轉換為 I0(第十三章第 1.3.2 節),則其具有來自 I 的拆箱轉換。
拆箱操作包括以下步驟:一是檢查對象實例是否為給定值類型的裝箱值,二是將該值復制出該實例。對一個 nullable-type
類型的 null 引用將產生該類型的 null 值。結構可以從類型 System.ValueType
拆箱,因為該類型是所有結構的基類(第十一章第 3.2 節)。
更多拆箱轉換的介紹可查看第四章第 3.2 節。
存在從 dynamic 到任意類型 T 的顯式動態轉換(explicit dynamic conversion)。轉換時動態綁定(第七章第 2.2 節)的,這意味著在「運行時」時,可以看到從表達式的「運行時」類型到 T 的顯式轉換。如果沒有轉換發生,那么將拋出「運行時」異常。
如果轉換不需要動態綁定,表達式可以先轉換為 object,然后轉為所需類型。
如下例所定義:
class C{ int i; public C(int i) { this.i = i; } public static explicit Operator C(string s) { return new C(int.Parse(s)); }}
下例列舉了顯式動態轉換:
object o = "1";dynamic d = "2";var c1 = (C)o; // 編譯,但顯式引用轉換失敗var c2 = (C)d; // 編譯,用戶定義轉換成功
從 o
到 C
的最佳轉換發生于「編譯時」的顯式引用轉換。這在「運行時」失敗,是因為 1
實際上不是 C。然而,從 d
到 C
的轉換作為顯式動態轉換(explicit dynamic conversion),在「運行時」之初一直被掛起,從 d
的「運行時」類型——string——到 c
的用戶定義轉換出現并成功。
對于給定的類型形參 T 存在下列顯式轉換:
interface-type
I(前提條件是目前尚未存在從 T 到 I 的隱式轉換)。在「運行時」,如果 T 為值類型,則其轉換將執行以先裝箱轉換、爾后顯式引用轉換。不然,其轉換將執行以顯式引用轉換或標識轉換。如果已知 T 為引用類型,則上述轉換將盡數歸類為顯式引用轉換(第六章第 2.4 節)。如果已知 T 不是引用類型,則上述轉換盡數歸類為拆箱轉換(第六章第 2.5 節)。
上述規則不允許從未受約束的類型形參直接顯式轉換為非接口類型(non-interface type),其目的是為了避免混淆,并使轉換語義清晰。如下例所聲明:
class X<T>{ public static long F(T t) { return (long)t; // Error }}
如果允許從 t
直接轉換為 int,極有可能會認為 x<int>.F(7)
將返回 7L
。然而并不是如此,因為僅當綁定時(binding-time)已知類型為數字類型時,才會考慮標準的數字轉換(standard numeric conversions)。為了語義清晰,上例必須這樣寫:
class X<T>{ public static long F(T t) { return (long)(object)t; // Ok, but will only work when T is long }}
這段代碼現在能夠編譯,但當「運行時」執行 X<int>.F(7)
時會拋出異常,這是因為不能將已裝箱的 int
直接轉換為 long
。
用戶定義顯式轉換(user-defined explicit conversion)包括以下三部分:可選的標準顯式轉換、執行用戶定義的隱式或顯式轉換操作、另一個可選的基礎顯式轉換。關于計算用戶定義顯式轉換的確切規則詳見第六章第 4.5 節。
標準轉換(standard conversions)是作為用戶定義轉換(user-defined conversion)的一部分出現的預定義轉換(pre-defined conversions)
下列隱式轉換被劃入標準隱式轉換(standard implicit conversions):
基礎隱式轉換不包括用戶定義隱式轉換(user-defined implicit conversions)。
標準顯式轉換(standard explicit conversions)包括所有基礎隱式轉換,以及由那些與已知的標準隱式轉換反向的轉換的子集(subset)所組成。換句話說,如果標準隱式轉換存在從 A 到 B 的轉換,那么標準顯示轉換就存在了從 A 到 B 以及從 B 到 A 的轉換。
C# 允許由用戶定義轉換(user-defined conversions)所擴展的預定義隱式與顯示轉換。用戶定義轉換是通過在類類型與結構類型中聲明轉換運算符(conversion operators,第十章第 10.3 節)而引入的。
C# 只允許某些用戶定義轉換的聲明。具體來說,不可重定義已存在的隱式或顯式轉換。
對于給定源類型 S 和目標類型 T,如果 S 或 T 均為可空類型,設 S0 及 T0 引用其基礎類型,否則 S0 及 T0 分別等于 S 與 T。只有當以下條件為真時,類型或結構允許聲明從源類型 S 到目標類型 T 的轉換:
interface-type
適用于用戶定義轉換的限制將在第十章第 10.3 節中進一步討論。
給定一個從 non-nullable
值類型 S 到 non-nullable
值類型 T 的用戶定義轉換運算符,存在從 S? 到 T? 的提升轉換操作符(lifted conversion operator)。提升轉換操作符執行從 S? 到 S 的解包、然后是從 S 到 T 的用戶定義轉換,接著是從 T 到 T? 的包裝(null 值 S? 直接轉換為 null 值 T? 的除外)。
提升轉換操作符與其基礎用戶定義轉換運算符具有相同的顯式或隱式類別。術語「用戶定義轉換(user-defined conversion)」適用于用戶定義轉換運算符與提升轉換操作符的使用。
用戶定義轉換將一個值從其所屬之類型(即「源類型 source type」)e
轉換為另一個類型(即「目標類型 target type」)。用戶定義轉換的運算集中于查找符合特定源類型與目標類型的最精確的用戶定義轉換符。次確定過程分為以下部分:
nullable-type
,則改為使用其基礎類型。當最精準用戶定義轉換操作符被明確了之后,確切的執行步驟分為三步:
用戶定義轉換的執行不會調用另一個用戶定義轉換或提升轉換操作符。換句話來說,從類型 S 到類型 T 的轉換將不會首選調用從 S 到 X 的用戶定義轉換,而后再調用從類型 X 到類型 T 的用戶定義轉換。
用戶定義轉換或顯式轉換的確切定義將在后述章節給出。這些定義使用以下術語:
interface-types
,那么我們可以說 A 被 B 包含(be encompassed by),或者說 B 包含 A(encompass)。從類型 S 到類型 T 的用戶定義隱式轉換(user-defined implicit conversion)的處理過程如下:
從類型 S 到類型 T 的用戶定義顯式轉換(user-defined explicit conversion)的過程如下:
anonymous-method-expression
或 lambda-expression
都被歸類到了匿名函數(anonymous function,第七章第十五節)。此表達式不具有類型,但可隱式轉換為委托類型或表達式樹類型。具體而言,匿名函數 F 可以與委托類型 D 兼容(compatible with):
anonymous-function-signature
,則 D 與 F 具有相同數量的形參個數。anonymous-function-signature
,那則 D 可以具有零或多個任意類型的形參,但 D 的任何形參都沒有 out
參數修飾符。ref
與 out
形參。Task
,則當 F 中每一個形參都被給定為 D 中對應參數的類型時,F 的主體是有效表達式(valid expression,請參考第七章),該表達式將允許作為 statement-expression
(第八章第六節)。Task
,則將 F 中每一個形參都被給定為 D 中對應參數的類型時,F 的主體是有效語句塊(valid statement block,請參考第八章第二節),該語句塊沒有 return
語句指定了表達式。下文使用任務類型的簡寫 Task
和 Task<T>
(第十章第十四節)。
如果 F 能與委托類型 D 兼容,則 Lambda 表達式 F 能與表達式樹類型 Expression<D>
兼容。注意,此處不適用于匿名方法,而僅適用于 Lambda 表達式。
某些 Lambda 表達式不能被轉換為表達式樹類型——即使存在轉換,該過程也會在「編譯時」失敗。這種情況會發生在符合以下條件的時候:
在下面例子中使用了泛型委托類型 Func<A, R>
,該函數采用一個類型為 A 的實參并返回類型為 R 的值:
delegate R Func<A,R>(A arg);
在下面賦值中,
Func<int,int> f1 = x => x + 1; // OkFunc<int,double> f2 = x => x + 1; // OkFunc<double,int> f3 = x => x + 1; // 錯誤Func<int, Task<int>> f4 = async x => x + 1; // Ok
每一個匿名函數的形參和返回值類型都由匿名函數所賦予的變量的類型來確定。
第一個賦值成功地把匿名函數轉換為委托類型 Func<int, int>
,因為當 x
指定的類型是 int 的時候,x + 1
是一個可以隱式轉換為 int 類型的有效表達式。
同樣地,第二個賦值成功地把匿名函數轉換為委托類型 Func<int, double>
,這是因為 x + 1
的結果(類型為 int)可以隱式轉換為類型 double。
然而,第三個賦值會出現「編譯時錯誤」,這是因為 x
給定的是 double 類型,x + 1
的結果(類型為 double)不能隱式轉換為 int。
第四個賦值成功地把匿名異步函數轉換為委托類型 Func<int, Task<int>>
,這是因為 x + 1
的結果(類型為 int)可以隱式轉換為任務類型 Task<int>
的結果類型 int。
匿名函數可能會影響重載策略(overload resolution),并參與類型推斷(type inference),關于這一點請參見第七章第五節。
匿名函數到委托類型的轉換將產生委托實例,這個委托實例引用匿名函數以及被捕獲的處于活動狀態的外部變量的集合(可以為空)。當委托被調用,將執行匿名函數體。使用委托所引用的被捕獲的外部變量集執行方法主體中的代碼。
自匿名函數所產生的委托的調用列表只含一項,確切目標對象與委托的目標方法并未被明確指定。具體來說,沒有具體指定該委托的目標對象是空(null)、所閉包的函數成員的 this
值,還是其它某個對象。
將具有相同的被捕獲外層變量實例集(可能為空集)的語義上相同的匿名函數轉換到委托類型,允許(但不要求)返回相同的委托實例。術語「語義上相同」表示在任何情況下,只要給定相同的參數,匿名函數的執行就會產生相同的結果。這條規定允許優化如下面這樣的代碼:
delegate double Function(double x);class Test{ static double[] Apply(double[] a, Function f) { double[] result = new double[a.Length]; for (int i = 0; i < a.Length; i++) result[i] = f(a[i]); return result; } static void F(double[] a, double[] b) { a = Apply(a, (double x) => Math.Sin(x)); b = Apply(b, (double y) => Math.Sin(y)); ... }}
由于兩個匿名函數委托存在相同的(空集)被捕獲外層變量集,且這兩個匿名函數語義上相同,所以編譯器被允許使用這兩個委托引用同一個目標方法。實際上,編譯器甚至被允許從這兩個匿名函數表達式返回同一個委托實例(delegate instance)。
將匿名函數轉換為表達式樹類型會產生一個表達式樹(expression tree,第四章第六節)。準確地講,匿名函數轉換計算會導致構造對象結構——表示匿名函數本身的結構。表達式樹的精確結構以及創建該目錄樹的確切過程為定義的實現(implementation defined)。
本節從其它 C# 構造的角度描述可能的匿名函數轉換實現方法。此處所描述的實現基于 Microsoft C# 編譯器 所使用的相同原理(same principles),但這不意味著強制實現,也不是唯一實現方式。此處僅簡單介紹到表達式樹的轉換,因為它們的標準語義超出了本規范的大綱范圍。
本節其余部分舉了幾個不同特點匿名函數的例子。在每個例子中,提供了到僅使用其他 C# 構造的代碼的相應轉換。在這些例子中,設標識符 D
表示下面委托類型:
public delegate void D();
匿名函數的最簡形式是不捕獲外層變量(outer variables)的形式:
class Test{ static void F() { D d = () => { Console.WriteLine("test"); }; }}
這可以轉換為引用編譯器生成的靜態方法,而匿名方法就在該靜態方法內:
class Test{ static void F() { D d = new D(__Method1); } static void __Method1() { Console.WriteLine("test"); }}
在下例中,匿名函數引用 this
實例成員:
class Test{ int x; void F() { D d = () => { Console.WriteLine(x); }; }}
這可以轉換為編譯器生成的實例方法(包含該匿名方法的代碼):
class Test{ int x; void F() { D d = new D(__Method1); } void __Method1() { Console.WriteLine(x); }}
在這個例子中,匿名函數捕獲一個本地局部變量:
class Test{ void F() { int y = 123; D d = () => { Console.WriteLine(y); }; }}
局部變量的生命周期現在至少被延長到匿名函數委托的生命周期。這可以通過將局部變量「提升」到編譯器生成的類的字段來實現。局部變量的實例化(第七章第 15.2 節)將對應為編譯器生成的類創建實例,且訪問局部變量則對應訪問編譯器生成的類的實例中的字段。而且匿名函數成為編譯器生成的類的實例方法:
class Test{ void F() { __Locals1 __locals1 = new __Locals1(); __locals1.y = 123; D d = new D(__locals1.__Method1); } class __Locals1 { public int y; public void __Method1() { Console.WriteLine(y); } }}
最后,下面這個匿名函數捕獲 this
以及兩個具有不同生命周期的局部變量:
class Test{ int x; void F() { int y = 123; for (int i = 0; i < 10; i++) { int z = i * 2; D d = () => { Console.WriteLine(x + y + z); }; } }}
這里,編譯器將對所捕獲的局部變量的每一個語句塊創建了一個類,這樣不同語句塊中的局部變量將具有獨立的生命周期。__Locals2
的實例——編譯器為內部語句塊創建的類——包含局部變量 z
以及引用 __Locals1
的實例字段。__Locals1
的實例——編譯器為外部語句塊創建的類——包含局部變量 y
以及引用包容函數成員 this
的字段。對于這些數據結構,可以通過 __Locals2
的實例來獲得所有被捕獲的外層變量,匿名函數的代碼從而可以實現為該類的實例方法。
class Test{ void F() { __Locals1 __locals1 = new __Locals1(); __locals1.__this = this; __locals1.y = 123; for (int i = 0; i < 10; i++) { __Locals2 __locals2 = new __Locals2(); __locals2.__locals1 = __locals1; __locals2.z = i * 2; D d = new D(__locals2.__Method1); } } class __Locals1 { public Test __this; public int y; } class __Locals2 { public __Locals1 __locals1; public int z; public void __Method1() { Console.WriteLine(__locals1.__this.x + __locals1.y + z); } }}
此處用于捕獲局部變量的技術也可以用于將匿名函數轉換為表達式樹:對變意義所創建的對象的引用能存儲在表達式樹中,并對局部變量的訪問可以表示為對這些對象的字段的訪問。這種方法(approach)的優勢在于允許「提升的(lifted)」局部變量在委托和表達式樹之間共享。
存在從方法組(method group,第七章第一節)到兼容委托類型的隱式轉換(第六章第一節)。對于給定的委托類型 D 以及歸類為方法組的表達式 E,如果 E 包含至少一個能以其正常形式(第七章第 5.3.1 節)應用于使用 D 的形參類型與修飾符構造的實參列表的方法,則存在從 E 到 D 的隱式轉換。具體為:
從方法組 E 到委托類型 D 的轉換時「編譯時」應用在下面的部分中描述。注意,存在從 E 到 D 的隱式轉換并不保證轉換產生的「編譯時」應用會不帶錯誤地成功。
E(A)
形式的方法調用(method invocation,第七章第 6.5.1 節),僅選擇單個方法 M,并進行下列變更(modifications):formal-parameter-list
的形參對應的類型與修飾符(ref 或 out)。注意,以下情況中,此過程可能會導致所創建到擴展方法(extension method)的委托:第七章第 6.5.1 節的算法未能找到實例方法,但在以擴展方法調用(第七章第 6.5.2 節)的形式處理 E(A)
的調用時卻取得成功。故而創建委托將捕獲該方法的第一個實參。
下面例子展示了方法組的轉換:
delegate string D1(object o);delegate object D2(string s);delegate object D3();delegate string D4(object o, params object[] a);delegate string D5(int i);class Test{ static string F(object o) {...} static void G() { D1 d1 = F; // Ok D2 d2 = F; // Ok D3 d3 = F; // Error – not applicable D4 d4 = F; // Error – not applicable in normal form D5 d5 = F; // Error – applicable but not compatible }}
對于 d1 的賦值隱式地將方法組 F 轉換為 D1 類型的值。
對于 d2 的賦值展示如何創建到具有派生程度較?。孀儯┑男螀㈩愋秃团缮潭容^大(協變)的返回類型的方法的委托。
對于 d3 的賦值展示當方法不適用時如何不不存在轉換。
對于 d4 的賦值展示方法如何必須以其正常形式應用。
對于 d5 的賦值展示如何允許委托和方法的形參與返回值類型僅對引用類型不同。
與其它隱式和顯式轉換一樣,強制轉換操作符可以用于顯式執行方法組轉換。因此下面這個例子
object obj = new EventHandler(myDialog.OkClick);
可以寫成
object obj = (EventHandler)myDialog.OkClick;
方法組可能影響重載策略,并參與類型推斷,具體參見第七章第五節。
方法組轉換的「運行時」運算如下所述:
reference-type
,則由實例表達式運算所得的值將為目標對象。如果所選的方法是實例方法且目標對象為空(null),則拋出 System.NullReferenceException
異常并不再執行后續步驟。value-type
,則執行裝箱操作(boxing operation,第四章第 3.1 節)以將值變為對象,然后成為目標對象。System.OutOfMemoryException
異常,并不執行接下來的步驟。新聞熱點
疑難解答