“可選參數”趣事探軼

 

上一篇博文中提到了“可選參數”這個C# 4.0中新增的語言特性,但是寫過之後還是不滿足,心裏還是有一些疑問沒有得到解釋。於是又做了一些探索,過程中竟然發現這麼一個小小的語言特性背後隱藏着的有趣問題還真不少。這次就把探索過程中的發現和疑問記錄下來。

“可選參數”的實現

Cnblogs上有一篇蔣金楠的文章中提到一句:“缺省參數最終體現爲兩個特殊的自定義特性OptionalAttribute和DefaultParameterValueAttribute ”。爲了驗證這個說法的正確性,我自己做了一些試驗。

要研究語言特性的實現原理最好的方法莫過於反編譯出IL代碼來一探究竟了。所以,那就順着這條線索走吧。

首先用C#代碼寫一個很簡單的測試方法:

上一篇博文中提到過這種寫法跟直接使用OptionalAttribute和DefaultValueAttribute這兩個attribute的效果是一樣的。

這兩段代碼編譯出來的IL除了名字之外別無二致,下面就以第一個方法爲例,它的IL是這樣的:

同時其生成的Metadata是這樣的:

說老實話,上面這兩段“天書”我並沒有完全讀懂,但是還是發現有一些異常,覺得有些東西不太對頭,爲什麼這麼說呢?因爲一般的attribute編譯之後的結果通常不是這樣的。比如下面這個例子:

先自定義一個只能應用到參數上的attribute:

然後定義一個被該attribute修飾的方法:

這個方法編譯之後的IL如下:

可以看到上面代碼中標紅的部分是TestMethod的IL中沒有的。而且,它的Metadata和TestMethod的也是不同的:

這個方法的Metadata的最後多了一段CustomAttribute的描述,其flags也爲空,不像TestMethod的flags後面跟有[Optional] [HasDefault]這樣的標誌。

因爲我沒有讀過ECMA 335的文檔,所以下面只是做一個不太謹慎的推測:OptionalAttribute和DefaultParameterValueAttribute這兩個attribute和其他的attribute不同,他們有自己對應的專有的flags。調用TestMethod的代碼在被編譯時,編譯器會去讀取存儲於元數據中的默認值,並把讀取到的值嵌入到IL中去。

由於在TestMethod的C#代碼中、編譯出的IL代碼中,及其元數據中都不見OptionalAttribute和DefaultParameterValueAttribute 的蹤跡,所以我認爲“缺省參數最終體現爲兩個特殊的自定義特性OptionalAttribute和DefaultParameterValueAttribute ”這種說法是有待商榷的。

背後的陷阱

“可選參數”看起來方便又好用,但是使用它是不是真的是多快好省的絕佳選擇呢?實際上不是的,它的背後隱藏着至少兩個陷阱(我只發現了兩個)。

第一個陷阱:版本更迭的問題

就以上面提到的TestMethod爲例,寫一個方法來調用它:

這裏在調用時沒有傳入參數,也就是說相當於傳入了默認的參數“A”。Caller編譯出來的IL是這樣的:

請注意標紅的兩行,Caller的IL中實際是把“A”這個值寫死了的。也就是說如果有一個包含“可選參數”的非強命名程序集在版本升級時修改了參數的默認值,其他引用它的程序集如果沒有重新編譯就無法獲得到新的默認參數值,在運行時仍然會傳入老版本中定義的值。

第二個陷阱:跨語言調用

並不是所有的語言都被強制要求支持“可選參數”這一特性。對於不支持這一特性的語言來說,完全可以忽略掉元數據中包含的默認值而強制要求這一語言的用戶去顯式的提供參數值。而這樣就會導致代碼的運行時行爲不一致。

C#4.0之前都所有版本都是不支持“可選參數”的。也就是說如果在VS2010中用C#4.0的語法和.NET Framework 2.0的框架編一個含有“可選參數”的程序集,然後在VS2008中的項目中引用這個程序集的話,則只能顯式的提供參數值。

針對以上兩點,我覺得在使用“可選參數”時應該遵循以下的原則:在public API(包括公開類型的公開成員和公開類型的受保護成員)中儘量不要用“可選參數”,而是使用方法重載,以避免API行爲不一致。在程序集內部的私有API中,盡情享用吧。

關於CLS-Compliant

微軟一站式示例代碼庫的文檔中提到說“可選參數”不是CLS-Compliant的。我覺得這種說法是錯誤的。最簡單的驗證方式就是加上CLSCompliantAttribute來試試看。

在含有TestMethod(這裏要保證TestMethod是公開類型中的公開方法,因爲CLSCompliant只針對public API)的項目的AssemblyInfo.cs中加上這麼一行:

然後編譯,編譯器沒有給出任何警告。而如果是在public API中使用了unit這一“臭名昭著”的類型的話,編譯器會毫不猶豫的給出一個警告。比如這樣的一個方法:

在編譯時就會得到一個警告:Argument type 'uint' is not CLS-compliant。

而且MSDN的文檔中也提到了雖然“可選參數”沒有被收錄到CLS的規範中,但是CLS是可以“容忍”它的存在的。

Reflector中可能的Bug

以上所有反編譯都是用IL Dasm來做的,而如果用最新版的Reflector(就是隻能試用14天的那個版本)來查看反編譯出的C#(把版本設爲任何非None的值)代碼的話,會發現它會把TestMethod解釋爲使用了OptionalAttribute和DefaultParameterValueAttribute。我懷疑這是因爲無論是使用“可選參數”還是直接使用OptionalAttribute和DefaultParameterValueAttribute,編譯出的結果都是一樣的,Reflector無從判斷源代碼中使用的是哪一種,索性就假定爲是第二種了。

存疑

雖然OptionalAttribute沒有出現在TestMethod的C#代碼中,在編譯出來的IL和元數據中也不見蹤影,但是它還是出現在了編譯出的程序集的TypeRefs中,而DefaultValueAttribute卻沒有出現。這是爲什麼呢?

參考

MSDN上的:

http://social.msdn.microsoft.com/Forums/en-US/csharplanguage/thread/d1be12e0-6325-427a-8e25-02fbd8396b1b/#18b08278-28a9-43dc-b3d4-e4694ca0260d

http://social.msdn.microsoft.com/Forums/en-US/csharplanguage/thread/31731806-dd83-4483-89b4-30001af14ab7/#352d019c-950c-42de-88f6-b0fecdf34351

http://social.msdn.microsoft.com/Forums/en-US/csharplanguage/thread/86f6d205-21b8-45e3-b5ec-3e9d5c1f9feb/

StackOverflow上的:

http://stackoverflow.com/questions/5456989/is-the-new-feature-of-c-4-0-optional-parameters-cls-compliant

http://stackoverflow.com/questions/5497514/what-does-opt-mean-in-msil

http://stackoverflow.com/questions/5522438/why-does-a-custom-attribute-appear-both-in-il-and-metadata

 

請問CSDN的工作人員一個問題,爲什麼用Live Writer發佈的文章一開始排版,格式都是正確的,只要在CSDN的Web Editor裏面編輯一次就全亂了呢?

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章