弱小和無知不是生存的障礙,傲慢才是!——《三體》
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#中的委托是什麼?事件是不是一種委托?
常量的基本概念就不細說了,關於常量的幾個特點總結一下:
關於常量不支持跨程序集版本更新,舉個簡單的例子來說明:
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被分成了三個部分:
因此可以說屬性的本質還是方法,使用面向對象的思想把字段封裝了一下。在定義屬性時,我們可以自定義一個私有字段,也可以使用自動屬性“{ get; set; } ”的簡化語法形式。
使用自動屬性時需要注意一點的是,私有字段是由編譯器自動命名的,是不受開發人員控制的。正因為這個問題,曾經在項目開發中遇到一個因此而產生的Bug:
這個Bug是關於序列化的,有一個類,定義很多個(自動)屬性,這個類的信息需要持久化到本地文件,當時使用了.NET自帶的二進制序列化組件。後來因為一個需求變更,把其中一個字段修改了一下,需要把自動屬性改為自己命名的私有字段的屬性,就像下面實例這樣。測試序列化到本地沒有問題,反序列化也沒問題,但最終bug還是被測試出來了,問題在與反序列化以前(修改代碼之前)的本地文件時,Index屬性的值丟失了!!!
private int _Index; public int Index { get { return _Index; } set { _Index = value; } }
因為屬性的本質是方法+字段,真正的值是存儲在字段上的,字段的名稱變了,反序列化以前的文件時找不到對應字段了,導致值的丟失!這也就是使用自動屬性可能存在的風險。
什麼是委托?簡單來說,委托類似於 C或 C++中的函數指針,允許將方法作為參數進行傳遞。
.NET中沒有函數指針,方法也不可能傳遞,委托之所可以像一個普通引用類型一樣傳遞,那是因為她本質上就是一個類。下面代碼是一個非常簡單的自定義委托:
public delegate void ShowMessageHandler(string mes);
看看她生產的IL代碼
我們一行定義一個委托的代碼,編譯器自動生成了一堆代碼:
因此,也就不難猜測,當我們調用委托的時候,其實就是調用委托對象的Invoke方法,可以驗證一下,下面的調用代碼會被編譯為對委托對象的Invoke方法調用:
private ShowMessageHandler ShowMessage; //調用 this.ShowMessage("123");
閉包提供了一種類似腳本語言函數式編程的便捷、可以共享數據,但也存在一些隱患。
題目列表中的第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代碼:
共享的局部變量被提升為委托類的一個字段了:
那該如何修正呢?很簡單,委托方法使用一個臨時局部變量就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
至於原理,可以自己探索了!
const關鍵字用來聲明編譯時常量,readonly用來聲明運行時常量。都可以標識一個常量,主要有以下區別:
1、初始化位置不同。const必須在聲明的同時賦值;readonly即可以在聲明處賦值,也可以在構造方法裡賦值。
2、修飾對象不同。const即可以修飾類的字段,也可以修飾局部變量;readonly只能修飾類的字段 。
3、const是編譯時常量,在編譯時確定該值,且值在編譯時被內聯到代碼中;readonly是運行時常量,在運行時確定該值。
4、const默認是靜態的;而readonly如果設置成靜態需要顯示聲明 。
5、支持的類型時不同,const只能修飾基元類型或值為null的其他引用類型;readonly可以是任何類型。
基元類型或值為null的其他引用類型,常量的風險就是不支持跨程序集版本更新,常量值更新後,所有使用該常量的代碼都必須重新編譯。
因為自動屬性的私有字段是由編譯器命名的,後期不宜隨意修改,比如在序列化中會導致字段值丟失。
特性與屬性是完全不相同的兩個概念,只是在名稱上比較相近。Attribute特性就是關聯了一個目標對象的一段配置信息,本質上是一個類,其為目標元素提供關聯附加信息,這段附加信息存儲在dll內的元數據,它本身沒什麼意義。運行期以反射的方式來獲取附加信息。使用方法可以參考:http://www.cnblogs.com/anding/p/5129178.html
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會被提升為委托對象的公共字段,生命周期延長了
什麼是委托?簡單來說,委托類似於 C或 C++中的函數指針,允許將方法作為參數進行傳遞。
事件可以理解為一種特殊的委托,事件內部是基於委托來實現的。
版權所有,文章來源:http://www.cnblogs.com/anding
個人能力有限,本文內容僅供學習、探討,歡迎指正、交流。
.NET面試題解析(00)-開篇來談談面試 & 系列文章索引
書籍:CLR via C#
書籍:你必須知道的.NET