弱小和無知不是生存的障礙,傲慢才是!——《三體》
常見面試題目:
1. const和readonly有什么區別?
2. 哪些類型可以定義為常量?常量const有什么風險?
3. 字段與屬性有什么異同?
4. 靜態成員和非靜態成員的區別?
5. 自動屬性有什么風險?
6. 特性是什么?如何使用?
7. 下面的代碼輸出什么結果?為什么?
List<Action> acs = new List<Action>(5); for (int i = 0; i < 5; i++) { acs.Add(() => { Console.WriteLine(i); }); } acs.ForEach(ac => ac());
8. C#中的委托是什么?事件是不是一種委托?
字段與屬性的恩怨
常量
常量的基本概念就不細說了,關於常量的幾個特點總結一下:
- 常量的值必須在編譯時確定,簡單說就是在定義是設置值,以后都不會被改變了,她是編譯常量。
- 常量只能用於簡單的類型,因為常量值是要被編譯然后保存到程序集的元數據中,只支持基元類型,如int、char、string、bool、double等。
- 常量在使用時,是把常量的值內聯到IL代碼中的,常量類似一個占位符,在編譯時被替換掉了。正是這個特點導致常量的一個風險,就是不支持跨程序集版本更新;
關於常量不支持跨程序集版本更新,舉個簡單的例子來說明:
public class A { public const int PORT = 10086; public virtual void Print() { Console.WriteLine(A.PORT); } }
上面一段非常簡單代碼,其生產的IL代碼如下,在使用常量變量的地方,把她的值拷過來了(把常量的值內聯到使用的地方),與常量變量A.PORT沒有關系了。假如A引用了B程序集(B.dll文件)中的一個常量,如果后面單獨修改B程序集中的常量值,只是重新編譯了B,而沒有編譯程序集A,就會出問題了,就是上面所說的不支持跨程序集版本更新。常量值更新后,所有使用該常量的代碼都必須重新編譯,這是我們在使用常量時必須要注意的一個問題。
- 不要隨意使用常量,特別是有可能變化的數據;
- 不要隨便修改已定義好的常量值;
補充一下枚舉的本質
接着上面的const說,其實枚舉enum也有類似的問題,其根源和const一樣,看看代碼你就明白了。下面的是一個簡單的枚舉定義,她的IL代碼定義和const定義是一樣一樣的啊!枚舉的成員定義和常量定義一樣,因此枚舉其實本質上就相當是一個常量集合。
public enum EnumType : int { None=0, Int=1, String=2, }
關於字段
字段本身沒什么好說的,這里說一個字段的內聯初始化問題吧,可能容易被忽視的一個小問題(不過好像也沒什么影響),先看看一個簡單的例子:
public class SomeType { private int Age = 0; private DateTime StartTime = DateTime.Now; private string Name = "三體"; }
定義字段並初始化值,是一種很常見的代碼編寫習慣。但注意了,看看IL代碼結構,一行代碼(定義字段+賦值)被拆成了兩塊,最終的賦值都在構造函數里執行的。
那么問題來了,如果有多個構造函數,就像下面這樣,有多半個構造函數,會造成在兩個構造函數.ctor中重復產生對字段賦值的IL代碼,這就造成了不必要的代碼膨脹。這個其實也很好解決,在非默認構造函數后加一個“:this()”就OK了,或者顯示的在構造函數里初始化字段。
public class SomeType { private DateTime StartTime = DateTime.Now; public SomeType() { } public SomeType(string name) { } }
屬性的本質
屬性是面向對象編程的基本概念,提供了對私有字段的訪問封裝,在C#中以get和set訪問器方法實現對可讀可寫屬性的操作,提供了安全和靈活的數據訪問封裝。我們看看屬性的本質,主要手段還是IL代碼:
public class SomeType { public int Index { get; set; } public SomeType() { } }
上面定義的屬性Index被分成了三個部分:
- 自動生成的私有字段“<Index>k__BackingField”
- 方法:get_Index(),獲取字段值;
- 方法:set_Index(int32 'value'),設置字段值;
因此可以說屬性的本質還是方法,使用面向對象的思想把字段封裝了一下。在定義屬性時,我們可以自定義一個私有字段,也可以使用自動屬性“{ get; set; } ”的簡化語法形式。
使用自動屬性時需要注意一點的是,私有字段是由編譯器自動命名的,是不受開發人員控制的。正因為這個問題,曾經在項目開發中遇到一個因此而產生的Bug:
這個Bug是關於序列化的,有一個類,定義很多個(自動)屬性,這個類的信息需要持久化到本地文件,當時使用了.NET自帶的二進制序列化組件。后來因為一個需求變更,把其中一個字段修改了一下,需要把自動屬性改為自己命名的私有字段的屬性,就像下面實例這樣。測試序列化到本地沒有問題,反序列化也沒問題,但最終bug還是被測試出來了,問題在與反序列化以前(修改代碼之前)的本地文件時,Index屬性的值丟失了!!!
private int _Index; public int Index { get { return _Index; } set { _Index = value; } }
因為屬性的本質是方法+字段,真正的值是存儲在字段上的,字段的名稱變了,反序列化以前的文件時找不到對應字段了,導致值的丟失!這也就是使用自動屬性可能存在的風險。
委托與事件
什么是委托?簡單來說,委托類似於 C或 C++中的函數指針,允許將方法作為參數進行傳遞。
- C#中的委托都繼承自System.Delegate類型;
- 委托類型的聲明與方法簽名類似,有返回值和參數;
- 委托是一種可以封裝命名(或匿名)方法的引用類型,把方法當做指針傳遞,但委托是面向對象、類型安全的;
委托的本質——是一個類
.NET中沒有函數指針,方法也不可能傳遞,委托之所可以像一個普通引用類型一樣傳遞,那是因為她本質上就是一個類。下面代碼是一個非常簡單的自定義委托:
public delegate void ShowMessageHandler(string mes);
看看她生產的IL代碼
我們一行定義一個委托的代碼,編譯器自動生成了一堆代碼:
- 編譯器自動幫我們創建了一個類ShowMessageHandler,繼承自System.MulticastDelegate(她又繼承自System.Delegate),這是一個多播委托;
- 委托類ShowMessageHandler中包含幾個方法,其中最重要的就是Invoke方法,簽名和定義的方法簽名一致;
- 其他兩個版本BeginInvoke和EndInvoke是異步執行版本;
因此,也就不難猜測,當我們調用委托的時候,其實就是調用委托對象的Invoke方法,可以驗證一下,下面的調用代碼會被編譯為對委托對象的Invoke方法調用:
private ShowMessageHandler ShowMessage; //調用 this.ShowMessage("123");
.NET的閉包
閉包提供了一種類似腳本語言函數式編程的便捷、可以共享數據,但也存在一些隱患。
題目列表中的第7題,就是一個.NET的閉包的問題。
List<Action> acs = new List<Action>(5); for (int i = 0; i < 5; i++) { acs.Add(() => { Console.WriteLine(i); }); } acs.ForEach(ac => ac()); // 輸出了 5 5 5 5 5,全是5?這一定不是你想要的吧!這是為什么呢?
上面的代碼中的Action就是.NET為我們定義好的一個無參數無返回值的委托,從上一節我們知道委托實質是一個類,理解這一點是解決本題的關鍵。在這個地方委托方法共享使用了一個局部變量i,那生成的類會是什么樣的呢?看看IL代碼:
共享的局部變量被提升為委托類的一個字段了:
- 變量i的生命周期延長了;
- for循環結束后字段i的值是5了;
- 后面再次調用委托方法,肯定就是輸出5了;
那該如何修正呢?很簡單,委托方法使用一個臨時局部變量就OK了,不共享數據:
List<Action> acss = new List<Action>(5); for (int i = 0; i < 5; i++) { int m = i; acss.Add(() => { Console.WriteLine(m); }); } acss.ForEach(ac => ac()); // 輸出了 0 1 2 3 4
至於原理,可以自己探索了!
題目答案解析:
1. const和readonly有什么區別?
const關鍵字用來聲明編譯時常量,readonly用來聲明運行時常量。都可以標識一個常量,主要有以下區別:
1、初始化位置不同。const必須在聲明的同時賦值;readonly即可以在聲明處賦值,也可以在構造方法里賦值。
2、修飾對象不同。const即可以修飾類的字段,也可以修飾局部變量;readonly只能修飾類的字段 。
3、const是編譯時常量,在編譯時確定該值,且值在編譯時被內聯到代碼中;readonly是運行時常量,在運行時確定該值。
4、const默認是靜態的;而readonly如果設置成靜態需要顯示聲明 。
5、支持的類型時不同,const只能修飾基元類型或值為null的其他引用類型;readonly可以是任何類型。
2. 哪些類型可以定義為常量?常量const有什么風險?
基元類型或值為null的其他引用類型,常量的風險就是不支持跨程序集版本更新,常量值更新后,所有使用該常量的代碼都必須重新編譯。
3. 字段與屬性有什么異同?
- 屬性提供了更為強大的,靈活的功能來操作字段
- 出於面向對象的封裝性,字段一般不設計為Public
- 屬性允許在set和get中編寫代碼
- 屬性允許控制set和get的可訪問性,從而提供只讀或者可讀寫的功能 (邏輯上只寫是沒有意義的)
- 屬性可以使用override 和 new
4. 靜態成員和非靜態成員的區別?
- 靜態變量使用 static 修飾符進行聲明,靜態成員在加類的時候就被加載(上一篇中提到過,靜態字段是隨類型對象存放在Load Heap上的),通過類進行訪問。
- 不帶有static 修飾符聲明的變量稱做非靜態變量,在對象被實例化時創建,通過對象進行訪問 。
- 一個類的所有實例的同一靜態變量都是同一個值,同一個類的不同實例的同一非靜態變量可以是不同的值 。
- 靜態函數的實現里不能使用非靜態成員,如非靜態變量、非靜態函數等。
5. 自動屬性有什么風險?
因為自動屬性的私有字段是由編譯器命名的,后期不宜隨意修改,比如在序列化中會導致字段值丟失。
6. 特性是什么?如何使用?
特性與屬性是完全不相同的兩個概念,只是在名稱上比較相近。Attribute特性就是關聯了一個目標對象的一段配置信息,本質上是一個類,其為目標元素提供關聯附加信息,這段附加信息存儲在dll內的元數據,它本身沒什么意義。運行期以反射的方式來獲取附加信息。使用方法可以參考:http://www.cnblogs.com/anding/p/5129178.html
7. 下面的代碼輸出什么結果?為什么?
List<Action> acs = new List<Action>(5); for (int i = 0; i < 5; i++) { acs.Add(() => { Console.WriteLine(i); }); } acs.ForEach(ac => ac());
輸出了 5 5 5 5 5,全是5!因為閉包中的共享變量i會被提升為委托對象的公共字段,生命周期延長了
8. C#中的委托是什么?事件是不是一種委托?
什么是委托?簡單來說,委托類似於 C或 C++中的函數指針,允許將方法作為參數進行傳遞。
- C#中的委托都繼承自System.Delegate類型;
- 委托類型的聲明與方法簽名類似,有返回值和參數;
- 委托是一種可以封裝命名(或匿名)方法的引用類型,把方法當做指針傳遞,但委托是面向對象、類型安全的;
事件可以理解為一種特殊的委托,事件內部是基於委托來實現的。
版權所有,文章來源:http://www.cnblogs.com/anding
個人能力有限,本文內容僅供學習、探討,歡迎指正、交流。
.NET面試題解析(00)-開篇來談談面試 & 系列文章索引
參考資料:
書籍:CLR via C#
書籍:你必須知道的.NET