.NET面試題解析(05)-常量、字段、屬性、特性與委託

弱小和無知不是生存的障礙,傲慢纔是!——《三體》css

  常見面試題目:

1. const和readonly有什麼區別?html

2. 哪些類型能夠定義爲常量?常量const有什麼風險?面試

3. 字段與屬性有什麼異同?編程

4. 靜態成員和非靜態成員的區別?安全

5. 自動屬性有什麼風險?閉包

6. 特性是什麼?如何使用?異步

7. 下面的代碼輸出什麼結果?爲何?ide

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,就會出問題了,就是上面所說的不支持跨程序集版本更新。常量值更新後,全部使用該常量的代碼都必須從新編譯,這是咱們在使用常量時必需要注意的一個問題。

  • 不要隨意使用常量,特別是有可能變化的數據;
  • 不要隨便修改已定義好的常量值;

image

生氣 補充一下枚舉的本質

接着上面的const說,其實枚舉enum也有相似的問題,其根源和const同樣,看看代碼你就明白了。下面的是一個簡單的枚舉定義,她的IL代碼定義和const定義是同樣同樣的啊!枚舉的成員定義和常量定義同樣,所以枚舉其實本質上就至關是一個常量集合。

public enum EnumType : int
{
    None=0,
    Int=1,
    String=2,
}

image

吐舌笑臉 關於字段

字段自己沒什麼好說的,這裏說一個字段的內聯初始化問題吧,可能容易被忽視的一個小問題(不過好像也沒什麼影響),先看看一個簡單的例子:

public class SomeType
{
    private int Age = 0;
    private DateTime StartTime = DateTime.Now;
    private string Name = "三體";
}

定義字段並初始化值,是一種很常見的代碼編寫習慣。但注意了,看看IL代碼結構,一行代碼(定義字段+賦值)被拆成了兩塊,最終的賦值都在構造函數裏執行的。

image

那麼問題來了,若是有多個構造函數,就像下面這樣,有多半個構造函數,會形成在兩個構造函數.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() { }
}

image

上面定義的屬性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代碼

image

咱們一行定義一個委託的代碼,編譯器自動生成了一堆代碼:

  • 編譯器自動幫咱們建立了一個類ShowMessageHandler,繼承自System.MulticastDelegate(她又繼承自System.Delegate),這是一個多播委託;
  • 委託類ShowMessageHandler中包含幾個方法,其中最重要的就是Invoke方法,簽名和定義的方法簽名一致;
  • 其餘兩個版本BeginInvoke和EndInvoke是異步執行版本;

所以,也就不難猜想,當咱們調用委託的時候,其實就是調用委託對象的Invoke方法,能夠驗證一下,下面的調用代碼會被編譯爲對委託對象的Invoke方法調用:

private ShowMessageHandler ShowMessage;

//調用
this.ShowMessage("123");

image

疑惑 .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代碼:

image

共享的局部變量被提高爲委託類的一個字段了:

  • 變量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用來聲明運行時常量。均可以標識一個常量,主要有如下區別:
一、初始化位置不一樣。const必須在聲明的同時賦值;readonly便可以在聲明處賦值,也能夠在構造方法裏賦值。
二、修飾對象不一樣。const便可以修飾類的字段,也能夠修飾局部變量;readonly只能修飾類的字段 。
三、const是編譯時常量,在編譯時肯定該值,且值在編譯時被內聯到代碼中;readonly是運行時常量,在運行時肯定該值。
四、const默認是靜態的;而readonly若是設置成靜態須要顯示聲明 。
五、支持的類型時不一樣,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

相關文章
相關標籤/搜索