.NET面試題解析(04)-類型、方法與繼承

作技術是清苦的。一我的,一臺機器,相對無言,代碼紛飛,bug無情。須夢裏挑燈,左思右想,肝血暗耗,板凳坐穿。世界繁華競逐,而你獨釣寒江,看盡千山暮雪,聽徹寒更雨歇。——來自《技術人的慰藉html

  常見面試題目:

1. 全部類型都繼承System.Object嗎?面試

2. 解釋virtual、sealed、override和abstract的區別c#

3. 接口和類有什麼異同?ide

4. 抽象類和接口有什麼區別?使用時有什麼須要注意的嗎?函數

5. 重載與覆蓋的區別?學習

6. 在繼承中new和override相同點和區別?看下面的代碼,有一個基類A,B1和B2都繼承自A,而且使用不一樣的方式改變了父類方法Print()的行爲。測試代碼輸出什麼?爲何?測試

public void DoTest()
{
    B1 b1 = new B1(); B2 b2 = new B2();
    b1.Print(); b2.Print();      //按預期應該輸出 B一、B2

    A ab1 = new B1(); A ab2 = new B2();
    ab1.Print(); ab2.Print();   //這裏應該輸出什麼呢?
}
public class A
{
    public virtual void Print() { Console.WriteLine("A"); }
}
public class B1 : A
{
    public override void Print() { Console.WriteLine("B1"); }
}
public class B2 : A
{
    public new void Print() { Console.WriteLine("B2"); }
}

7. 下面代碼中,變量a、b都是int類型,代碼輸出結果是什麼?編碼

int a = 123;
int b = 20;
var atype = a.GetType();
var btype = b.GetType();
Console.WriteLine(System.Object.Equals(atype,btype));
Console.WriteLine(System.Object.ReferenceEquals(atype,btype));

8.class中定義的靜態字段是存儲在內存中的哪一個地方?爲何會說她不會被GC回收?spa

  類型基礎知識梳理

微笑 類型Type簡述

經過本系列前面幾篇文章,基本瞭解了值類型和引用類型,及其相互關係。以下圖,.NET中主要的類型就是值類型和引用類型,全部類型的基類就是System.Object,也就是說咱們使用FCL提供的各類類型的、自定義的全部類型都最終派生自System.Object,所以他們也都繼承了System.Object提供的基本方法。線程

System.Object能夠說是.NET中的萬物之源,若是非要較真的話,好像只有接口不繼承她了。接口是一個特殊的類型,能夠理解爲接口是普通類型的約束、規範,她不能夠實例化。(實際編碼中,接口能夠用object表示,只是一種語法支持,此見解不知是否準確,歡迎交流)

在.NET代碼中,咱們能夠很方便的建立各類類型,一個簡單的數據模型、複雜的聚合對象類型、或是對客觀世界實體的抽象。類 (class) 是最基礎的 C# 類型(注意:本文主要探討的就是引用類型,文中所述類型如沒註明都爲引用類型),支持繼承與多態。一個c# 類Class主要包含兩種基本成員:

  • 狀態(字段、常量、屬性等)
  • 操做(方法、事件、索引器、構造函數等)

利用建立的類型(或者系統提供的),能夠很容易的建立對象的實例。使用 new 運算符建立,該運算符爲新的實例分配內存,調用構造函數初始化該實例,並返回對該實例的引用,以下面的語法形式:

<類名>  <實例名> = new <類名>([構造函數的參數])

建立後的實例對象,是一個存儲在內存上(在線程棧或託管堆上)的一個對象,那能夠創造實例的類型在內存中又是一個什麼樣的存在呢?她就是類型對象(Type Object)

微笑 類型對象(Type Object)

看看下面的代碼:

int a = 123;                                                           // 建立int類型實例a
int b = 20;                                                            // 建立int類型實例b
var atype = a.GetType();                                               // 獲取對象實例a的類型Type
var btype = b.GetType();                                               // 獲取對象實例b的類型Type
Console.WriteLine(System.Object.Equals(atype,btype));                  //輸出:True
Console.WriteLine(System.Object.ReferenceEquals(atype, btype));        //輸出:True

任何對象都有一個GetType()方法(基類System.Object提供的),該方法返回一個對象的類型,類型上面包含了對象內部的詳細信息,如字段、屬性、方法、基類、事件等等(經過反射能夠獲取)。在上面的代碼中兩個不一樣的int變量的類型(int.GetType())是同一個Type,說明int在內存中有惟一一個(相似靜態的)Systen.Int32類型。

上面獲取到的Type對象(Systen.Int32)就是一個類型對象,她同其餘引用類型同樣,也是一個引用對象,這個對象中存儲了int32類型的全部信息(類型的全部元數據信息)。

關於類型類型對象(Object Type):

>每個類型(如System.Int32)在內存中都會有一個惟一的類型對象,經過(int)a.GetType()能夠獲取該對象;

>類型對象(Object Type)存儲在內存中一個獨立的區域,叫加載堆(Load Heap),加載堆是在進程建立的時候建立的,不受GC垃圾回收管制,所以類型對象一經建立就不會被釋放的,他的生命週期從AppDomain建立到結束;

>前問說過,每一個引用對象都包含兩個附加成員:TypeHandle和同步索引塊,其中TypeHandle就指向該對象對應的類型對象;

>類型對象的加載由class loader負責,在第一次使用前加載;

>類型中的靜態字段就是存儲在這裏的(加載堆上的類型對象),因此說靜態字段是全局的,並且不會釋放;

能夠參考下面的圖,第一幅圖描述了對象在內存中的一個關係, 第二幅圖更復雜,更準確、全面的描述了內存的結構分佈。

 圖片來源

image

生氣 方法表

類型對象內部的主要的結構是怎麼樣的呢?其中最重要的就是方法表,包含了是類型內部的全部方法入口,關於具體的細節和原理這裏很少贅述(太多了,能夠參考文末給的參考資料),本文只是初步介紹一下,主要目的是爲了解決第6題。

public class A
{
    public virtual void Print() { Console.WriteLine("A"); }
}
public class B1 : A
{
    public override void Print() { Console.WriteLine("B1"); }
}
public class B2 : A
{
    public new void Print() { Console.WriteLine("B2"); }
}

仍是以第6題的代碼爲例,上面的代碼中,定義兩個簡單的類,一個基類A,,B1和B2繼承自A,而後使用不一樣的方式改變了父類方法的行爲。當定義了b一、b2兩個變量後,內存結構示意圖以下:

B1 b1 = new B1();
B2 b2 = new B2();

image

方法表的加載

  • 方法表的加載時父類在前子類在後的,首先加載的是固定的4個來自System.Object的虛方法:ToString, Equals, GetHashCode, and Finalize;
  • 而後加載父類A的虛方法;
  • 加載本身的方法;
  • 最後是構造方法:靜態構造函數.cctor(),對象構造函數.ctor();

方法表中的方法入口(方法表槽 )還有不少其餘的信息,好比會關聯方法的IL代碼以及對應的本地機器碼等。其實類型對象自己也是一個引用類型對象,其內部一樣也包含兩個附件成員:同步索引塊和類型對象指針TypeHandel,具體細節、原理有興趣的能夠本身深刻了解。

方法的調用:當執行代碼b1.Print()時(此處只關注方法調用,忽略方法的繼承等因素),經過b1的TypeHandel找到對應類型對象,而後找到方法表槽,而後是對應的IL代碼,第一次執行的時候,JIT編譯器須要把IL代碼編譯爲本地機器碼,第一次執行完成後機器碼會保留,下一次執行就不須要JIT編譯了。這也是爲何說.NET程序啓動須要預熱的緣由。

眨眼 .NET中的繼承本質

方法表的建立過程是從父類到子類自上而下的,這是.NET中繼承的很好體現,當發現有覆寫父類虛方法會覆蓋同名的父方法,全部類型的加載都會遞歸到System.Object類。

  • 繼承是可傳遞的,子類是對父類的擴展,必須繼承父類方法,同時能夠添加新方法。
  • 子類能夠調用父類方法和字段,而父類不能調用子類方法和字段。 
  • 子類不光繼承父類的公有成員,也繼承了私有成員,只是不可直接訪問。
  • new關鍵字在虛方法繼承中的阻斷做用,中斷某一虛方法的繼承傳遞。

所以類型B一、B2的類型對象進一步的結構示意圖以下:

  • 在加載B1類型對象時,當加載override B1.Print(「B1」)時,發現有覆寫override的方法,會覆蓋父類的同名虛方法Print(「A」),就是下面的示意圖,簡單來講就是在B1中Print只有一個實現版本;
  • 加載B2類型對象時,new關鍵字表示要隱藏基類的虛方法,此時B2中的Print(「B2」)就不是虛方法了,她是B2中的新方法了,簡單來講就是在B2類型對象中Print有2個實現版本;

image

 

B1 b1 = new B1();

B2 b2 = new B2();
b1.Print(); b2.Print();      //按預期應該輸出 B一、B2

A ab1 = new B1(); 
A ab2 = new B2();
ab1.Print(); ab2.Print();   //這裏應該輸出什麼呢?

上面代碼中紅色高亮的兩行代碼,用基類(A)和用自己B1聲明到底有什麼區別呢?相似這種代碼在實際編碼中是很常見的,簡單的歸納一下:

  • 不管用什麼作引用聲明,哪怕是object,等號右邊的[ = new 類型()]都是沒有區別的,也就說說對象的建立不受影響的,b1和ab1對象在內存結構上是一致的;
  • 他們的的差異就在引用指針的類型不一樣,這種不一樣在編碼中智能提示就直觀的反應出來了,在實際方法調用上也與引用指針類型有直接關係;
  • 綜合來講,不一樣引用指針類型對於對象的建立(new操做)不影響;但對於對象的使用(如方法調用)有影響,這一點在上面代碼的執行結果中體現出來了!

上面調用的IL代碼:

image

對於虛方法的調用,在IL中都是使用指令callvirt,該指令主要意思就是具體的方法在運行時動態肯定的:

callvirt使用虛擬調度,也就是根據引用類型的動態類型來調度方法,callvirt指令根據引用變量指向的對象類型來調用方法,在運行時動態綁定,主要用於調用虛方法。

不一樣的類型指針在虛擬方法表中有不一樣的附加信息做爲標誌來區別其訪問的地址區域,稱爲offset。不一樣類型的指針只能在其特定地址區域內進行執行。編譯器在方法調用時還有一個原則:

執行就近原則:對於同名字段或者方法,編譯器是按照其順序查找來引用的,也就是首先訪問離它建立最近的字段或者方法。

所以執行如下代碼時,引用指針類型的offset指向子類,以下圖,,按照就近查找執行原則,正常輸出B一、B2

B1 b1 = new B1();

 B2 b2 = new B2();
b1.Print(); b2.Print();      //按預期應該輸出 B一、B2

image

而當執行如下代碼時,引用指針類型都爲父類A,引用指針類型的offset指向父類,以下圖,按照就近查找執行原則,輸出B一、A。

A ab1 = new B1(); 
A ab2 = new B2(); ab1.Print(); ab2.Print(); //這裏應該輸出什麼呢?

image

  .NET中的繼承

大笑 什麼是抽象類

抽象類提供多個派生類共享基類的公共定義,它既能夠提供抽象方法,也能夠提供非抽象方法。抽象類不能實例化,必須經過繼承由派生類實現其抽象方法,所以對抽象類不能使用new關鍵字,也不能被密封。

基本特色:

  • 抽象類使用Abstract聲明,抽象方法也是用Abstract標示;
  • 抽象類不能被實例化;
  • 抽象方法必須定義在抽象類中;
  • 抽象類能夠繼承一個抽象類;
  • 抽象類不能被密封(不能使用sealed);
  • 同類Class同樣,只支持單繼承;

一個簡單的抽象類代碼:

public abstract class AbstractUser
{
    public int Age { get; set; }
    public abstract void SetName(string name);
}

IL代碼以下,類和方法都使用abstract修飾:

image

大笑 什麼是接口?

接口簡單理解就是一種規範、契約,使得實現接口的類或結構在形式上保持一致。實現接口的類或結構必須實現接口定義中全部接口成員,以及該接口從其餘接口中繼承的全部接口成員。

基本特色:

  • 接口使用interface聲明;
  • 接口相似於抽象基類,不能直接實例化接口;
  • 接口中的方法都是抽象方法,不能有實現代碼,實現接口的任何非抽象類型都必須實現接口的全部成員:
  • 接口成員是自動公開的,且不能包含任何訪問修飾符。
  • 接口自身可從多個接口繼承,類和結構可繼承多個接口,但接口不能繼承類。

下面一個簡單的接口定義:

public interface IUser
{
    int Age { get; set; }
    void SetName(string name);
}

下面是IUser接口定義的IL代碼,看上去是否是和上面的抽象類AbstractUser的IL代碼差很少!接口也是使用.Class ~ abstract標記,方法定義同抽象類中的方法同樣使用abstract virtual標記。所以能夠把接口看作是一種特殊的抽象類,該類只提供定義,沒有實現

image

另一個小細節,上面說到接口是一個特殊的類型,不繼承System.Object,經過IL代碼其實能夠證明這一點。不管是自定義的任何類型仍是抽象類,都會隱式繼承System.Object,AbstractUser的IL代碼中就有「extends [mscorlib]System.Object」,而接口的IL代碼並無這一段代碼。

大笑 關於繼承

關於繼承,太概念性了,就不細說了,主要仍是在平時的搬磚過程當中多思考、多總結、多體會。在.NET中繼承的主要兩種方式就是類繼承和接口繼承,二者的主要思想是不同的:

  • 類繼承強調父子關係,是一個「IS A」的關係,所以只能單繼承(就像一我的只能有一個Father);
  • 接口繼承強調的是一種規範、約束,是一個「CAN DO」的關係,支持多繼承,是實現多態一種重要方式。

更準確的說,類能夠叫繼承,接口叫「實現」更合適。更多的概念和區別,能夠直接看後面的答案,更多的仍是要本身理解。

  題目答案解析:

1. 全部類型都繼承System.Object嗎?

基本上是的,全部值類型和引用類型都繼承自System.Object,接口是一個特殊的類型,不繼承自System.Object。

2. 解釋virtual、sealed、override和abstract的區別

  • virtual申明虛方法的關鍵字,說明該方法能夠被重寫
  • sealed說明該類不可被繼承
  • override重寫基類的方法
  • abstract申明抽象類和抽象方法的關鍵字,抽象方法不提供實現,由子類實現,抽象類不可實例化。

3. 接口和類有什麼異同?

不一樣點:

一、接口不能直接實例化。

二、接口只包含方法或屬性的聲明,不包含方法的實現。

三、接口能夠多繼承,類只能單繼承。

四、類有分部類的概念,定義可在不一樣的源文件之間進行拆分,而接口沒有。(這個地方確實不對,接口也能夠分部,謝謝@xclin163的指正)

五、表達的含義不一樣,接口主要定義一種規範,統一調用方法,也就是規範類,約束類,類是方法功能的實現和集合

相同點:

一、接口、類和結構均可以從多個接口繼承。

二、接口相似於抽象基類:繼承接口的任何非抽象類型都必須實現接口的全部成員。

三、接口和類均可以包含事件、索引器、方法和屬性。

4. 抽象類和接口有什麼區別?

一、繼承:接口支持多繼承;抽象類不能實現多繼承。

二、表達的概念:接口用於規範,更強調契約,抽象類用於共性,強調父子。抽象類是一類事物的高度聚合,那麼對於繼承抽象類的子類來講,對於抽象類來講,屬於"Is A"的關係;而接口是定義行爲規範,強調「Can Do」的關係,所以對於實現接口的子類來講,相對於接口來講,是"行爲須要按照接口來完成"。

三、方法實現:對抽象類中的方法,便可以給出實現部分,也能夠不給出;而接口的方法(抽象規則)都不能給出實現部分,接口中方法不能加修飾符。

四、子類重寫:繼承類對於二者所涉及方法的實現是不一樣的。繼承類對於抽象類所定義的抽象方法,能夠不用重寫,也就是說,能夠延用抽象類的方法;而對於接口類所定義的方法或者屬性來講,在繼承類中必須重寫,給出相應的方法和屬性實現。

五、新增方法的影響:在抽象類中,新增一個方法的話,繼承類中能夠不用做任何處理;而對於接口來講,則須要修改繼承類,提供新定義的方法。

六、接口能夠做用於值類型(枚舉能夠實現接口)和引用類型;抽象類只能做用於引用類型。

七、接口不能包含字段和已實現的方法,接口只包含方法、屬性、索引器、事件的簽名;抽象類能夠定義字段、屬性、包含有實現的方法。

5. 重載與覆蓋的區別?

重載:當類包含兩個名稱相同但簽名不一樣(方法名相同,參數列表不相同)的方法時發生方法重載。用方法重載來提供在語義上完成相同而功能不一樣的方法。

覆寫:在類的繼承中使用,經過覆寫子類方法能夠改變父類虛方法的實現。

主要區別

一、方法的覆蓋是子類和父類之間的關係,是垂直關係;方法的重載是同一個類中方法之間的關係,是水平關係。
二、覆蓋只能由一個方法,或只能由一對方法產生關係;方法的重載是多個方法之間的關係。
三、覆蓋要求參數列表相同;重載要求參數列表不一樣。
四、覆蓋關係中,調用那個方法體,是根據對象的類型來決定;重載關係,是根據調用時的實參表與形參表來選擇方法體的。

6. 在繼承中new和override相同點和區別?看下面的代碼,有一個基類A,B1和B2都繼承自A,而且使用不一樣的方式改變了父類方法Print()的行爲。測試代碼輸出什麼?爲何?

public void DoTest()
{
    B1 b1 = new B1(); B2 b2 = new B2();
    b1.Print(); b2.Print();      //按預期應該輸出 B一、B2

    A ab1 = new B1(); A ab2 = new B2();
    ab1.Print(); ab2.Print();   //這裏應該輸出什麼呢?輸出B一、A
}
public class A
{
    public virtual void Print() { Console.WriteLine("A"); }
}
public class B1 : A
{
    public override void Print() { Console.WriteLine("B1"); }
}
public class B2 : A
{
    public new void Print() { Console.WriteLine("B2"); }
}

7. 下面代碼中,變量a、b都是int類型,代碼輸出結果是什麼?

int a = 123;
int b = 20;
var atype = a.GetType();
var btype = b.GetType();
Console.WriteLine(System.Object.Equals(atype,btype));          //輸出True
Console.WriteLine(System.Object.ReferenceEquals(atype,btype)); //輸出True

8.class中定義的靜態字段是存儲在內存中的哪一個地方?爲何會說她不會被GC回收?

隨類型對象存儲在內存的加載堆上,由於加載堆不受GC管理,其生命週期隨AppDomain,不會被GC回收。

 

版權全部,文章來源:http://www.cnblogs.com/anding

我的能力有限,本文內容僅供學習、探討,歡迎指正、交流。

.NET面試題解析(00)-開篇來談談面試 & 系列文章索引

  參考資料:

書籍:CLR via C#

書籍:你必須知道的.NET

Interface繼承至System.Object?:http://www.cnblogs.com/whitewolf/archive/2012/05/23/2514123.html

關於CLR內存管理一些深層次的討論[下篇]

[你必須知道的.NET]第十五回:繼承本質論

深刻.NET Framework內部, 看看CLR如何建立運行時對象的

 

後記:本文寫的有點難產,可能仍是技術不夠熟練,對於文中的「繼承中的方法表」那一部分理解的還不夠透徹,也花了很多時間(包括畫圖),一直猶豫要不要發出來,懼怕理解有誤,最終仍是發出來了,歡迎交流、指正!

相關文章
相關標籤/搜索