程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> C#基礎知識 >> .NET面試題解析(05)-常量、字段、屬性、特性與委托

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

編輯:C#基礎知識
  系列文章目錄地址: .NET面試題解析(00)-開篇來談談面試 & 系列文章索引

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

  常見面試題目:

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

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

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用來聲明運行時常量。都可以標識一個常量,主要有以下區別:
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

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved