開篇:在日常的.NET開發學習中,我們往往會接觸到一些較新的語法,它們相對以前的老語法相比,做了很多的改進,簡化了很多繁雜的代碼格式,也大大減少了我們這些菜鳥碼農的代碼量。但是,在開心歡樂之余,我們也不禁地對編譯器內部到底為我們做了哪些事兒而感到好奇?於是,我們就借助反編譯神器,去看看編譯器到底做了啥事!其實本篇中很多都不算新語法,對於很多人來說可能都是接觸了很久了,這里主要是針對.NET的老版本來說,是一個“相對”的新語法。
/* 新語法索引 */
1.自動屬性 Auto-Implemented Properties2.隱式類型 var3.參數默認值 和 命名參數4.對象初始化器 與 集合初始化器 { }6.擴展方法10.LINQ查詢表達式
一、自動屬性探秘:[ C# 3.0/.Net 3.x 新增特性 ]
1.1 以前的做法:先寫私有變量,再寫公有屬性
public class Student { private Int32 _id; public Int32 Id { get { return _id; } set { _id = value; } } private string _name; public string Name { get { return _name; } set { _name = value; } } private Int16 _age; public Int16 Age { get { return _age; } set { _age = value; } } }
1.2 現在的做法:聲明空屬性
public class Person { public Int32 ID { get; set; } public string Name { get; set; } public Int16 Age { get; set; } }
PS:現在看來,是不是少些很多代碼?直接聲明一個空屬性,編譯器就可以幫我們完成以前的私有成員字段和get、set方法,於是,我們可以通過Reflector反編譯工具去看看,到底是怎么完成這個操作的。
1.3 偉大的“鄉村基”—CSC(C Sharp Compiler):C#編譯器
PS:這里為何會提到鄉村基,一是因為鄉村基的簡稱就是CSC,二是因為本人比較喜歡吃鄉村基的中式快餐,所以,么么嗒!(感覺像是給鄉村基打廣告似的,不過我還是蠻喜歡鄉村基的,當然是拋開價格來說)
(1)首先我們來編譯一下上面這個小程序,然后將編譯后的exe/dll拖到反編譯神器Reflector(或者ILSpy也是贊贊噠)中
(2)找到Person類,可以看到編譯后的結果:CSC幫我們自動生成了與共有屬性對應的私有字段
我們可以從圖中看出,自動生成的字段與以前的字段有一些區別:
①在每個字段上方都加上了一個[CompilerGenerated]的特性(Attribute),顧名思義:表示其是由編譯器生成的;
②每個字段的變量名稱是有一定格式的,比如<Age>k__BackingField,那么可以看出格式為:<屬性名>k_BackingField;(BackingField顧名思義就是背后的字段)
(3)看完了自動生成的字段,再來看看屬性是怎么定義的:
①和自動生成的字段一樣,屬性也加上了[CompilerGenerated]的特性以示區別
②眾所周知,屬性就是一個get和一個set的兩個方法的封裝,那么我們之前寫的空get/set方法又是怎么被編譯生成的呢
於是,我們可以看到,在get和set方法中,也加上了[CompilerGenerated]的特性以示區別,另外還幫我們自動對應了自動生成的私有字段,這就跟我們自己手動寫的私有字段+共有屬性的方法保持了一致。所以,自動屬性是一個實用的語法糖,幫我們做了兩件事:自動生成私有字段,自動在get/set方法中匹配私有字段。
二、隱式類型—關鍵字:var [ C# 3.0/.Net 3.x 新增特性 ]
2.1 猶抱琵琶半遮面—你能猜出我是誰?
以前,我們在定義每個變量時都需要明確指出它是哪個類型。但是,當有了var之后,一切變得那么和諧,我們可以用一個var定義所有的類型。
var age = 100; age += 150; var name = ""; name = "edisonchou"; Console.WriteLine("age={0}", age); Console.WriteLine("name={0}", name);
點擊調試,發現編譯器自動幫我們匹配上了正確的類型並成功顯示出來:
那么,我們又好奇地想知道編譯器到底是否識別出來了指定的類型,於是我們再次通過反編譯工具來一看究竟:
可以看出,我們可愛的CSC正確地幫我們推斷出了正確的類型,不由得想給它點32個贊了!
但是,變量類型不可更改,因為聲明的時候已經確定類型了,例如我們在剛剛的代碼中給變量賦予不同於定義時的類型,會出現錯誤。
2.2 好刀用在刀刃上—隱式類型應用場景
在數據型業務開發中,我們會對一個數據集合進行LINQ查詢,而這個LINQ查詢的結果可能是ObjectQuery<>或IQueryable<>類型的對象。因此,在目標具體類型不明確的情況下,我們可以用var關鍵來聲明:
List<UserInfo> userList = roleService.LoadRoles(param); var data = from u in userList where u.IsDel == 0 select u;
2.3 但“愛”就是克制—隱式類型使用限制
(1)被聲明的變量是一個局部變量,而不是靜態或實例字段;
(2)變量必須在聲明的同時被初始化,編譯器要根據初始化值推斷類型;
(3)初始化不是一個匿名函數,同時初始化表達式也不能是 null;
(4)語句中只聲明一次變量,聲明后不能更改類型;(詳見上面的例子)
(5)賦值的數據類型必須是可以在編譯時確定的類型;
三、參數默認值和命名參數:[ C# 4.0/.NET 4.0 新增特性 ]
3.1 帶默認值的方法
static void Main(string[] args) { // 01.帶默認值參數函數 FuncWithDefaultPara(); // 02.省略一個默認參數調用 FuncWithDefaultPara(10086); Console.ReadKey(); } static void FuncWithDefaultPara(int id = 10010, bool gender = true) { Console.WriteLine("Id:{0},Gender:{1}", id, gender ? "Man" : "Woman"); }
點擊調試,顯示結果如下:
3.2 編譯后的方法調用
同樣,為了一探帶參數默認值方法調用的細節,我們還是借助反編譯神器查看其中的玄妙:
(1)首先,我們來看看帶默認值參數的方法被編譯后是怎么的:
可以看到,在.NET Framework中大量采用了基於Attribute的開發方式,這里為參數添加了表示默認值的特性DefaultParameterValue。
(2)其次,再來看看Main函數中的調用過程是怎么被編譯的:
可以看出,編譯器幫我們在方法調用的括號中幫我們填充了默認值。這里,我們不禁好奇,如果在調用中,不指定ID(即使用ID默認值10010)而僅僅指定Gender為false是否可以編譯通過?我們來試一下:
static void Main(string[] args) { // 01.帶默認值參數函數 FuncWithDefaultPara(); // 02.省略一個默認參數調用 FuncWithDefaultPara(10086); // 錯誤調用: FuncWithDefaultPara(false); Console.ReadKey(); }
這時,出現了以下錯誤:
於是,我們知道,CSC也還沒有那么智能,無法理解我們高深的“意圖”。那么,有木有一種方法來解決這種需求呢,於是命名參數橫空出世了。
3.3 使用命名參數
在新語法中為方法調用引入了命名參數,格式為 參數名:參數值
static void Main(string[] args) { // 01.帶默認值參數函數 FuncWithDefaultPara(); // 02.省略一個默認參數調用 FuncWithDefaultPara(10086); // 錯誤調用: //FuncWithDefaultPara(false); // 03.使用命名參數調用 FuncWithDefaultPara(gender: false); Console.ReadKey(); }
通過調試,可以得到如下結果:
通過前面的分析,我們可以分析出,使用命名參數被編譯之后還是會生成指定參數值的調用:
四、自動初始化器:[ C# 3.0/.NET 3.x 新增特性 ]
4.1 屬性初始化器
(1)在開發中,我們經常會這些為new出來的對象設置屬性:
static void InitialPropertyFunc() { Person p = new Person() { Name = "小強", Age = 18 }; Console.WriteLine("Name:{0}-Age:{1}", p.Name, p.Age); }
(2)但是,經過編譯后我們發現原來還是以前的方式:先new出來,然后一個屬性一個屬性地賦值。這里,編譯器首先生成了一個臨時對象g_initLocal0,然后為其屬性賦值,最后將g_initLocal0這個對象的地址傳給要使用的對象p。
4.2 集合初始化器
(1)在開發中,我們經常在一個集合的實例化中,就為其初始化:
static void InitialCollectionFunc() { List<Person> personList = new List<Person>() { new Person(){Name="小強",Age=10}, new Person(){Name="小王",Age=15}, new Person(){Name="小李",Age=18} }; foreach(Person person in personList) { Console.WriteLine("Name:{0}-Age:{1}", person.Name, person.Age); } }
(2)經過上面的集合初始化器我們了解到編譯器還是會編譯成原來的方式,即先new出來,為其分配了內存空間之后,再一個一個地為其屬性賦值。那么,在集合的初始化中我們也可以大膽地猜測,編譯器也是做了以上的優化工作:即先將每個對象new出來,然后一個一個地為屬性賦值,最后調用集合的Add方法將其添加到集合中。於是,我們還是反編譯一下,一探究竟:
附件下載
(1)反編譯神器之Reflector: http://pan.baidu.com/s/1evCJG
(2)所謂的新語法Demo:http://pan.baidu.com/s/1ntwqdAT