程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> .NET面試題解析(04)-類型、方法與繼承

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

編輯:關於.NET
  系列文章目錄地址: .NET面試題解析(00)-開篇來談談面試 & 系列文章索引

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

  常見面試題目:

1. 所有類型都繼承System.Object嗎?

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

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

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();      //按預期應該輸出 B1、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回收?

  類型基礎知識梳理

微笑 類型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,然後使用不同的方式改變了父類方法的行為。當定義了b1、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關鍵字在虛方法繼承中的阻斷作用,中斷某一虛方法的繼承傳遞。

因此類型B1、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();      //按預期應該輸出 B1、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指向子類,如下圖,,按照就近查找執行原則,正常輸出B1、B2

B1 b1 = new B1();

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

image

而當執行以下代碼時,引用指針類型都為父類A,引用指針類型的offset指向父類,如下圖,按照就近查找執行原則,輸出B1、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. 接口和類有什麼異同?

不同點:

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

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

3、接口可以多繼承,類只能單繼承。

4、類有分部類的概念,定義可在不同的源文件之間進行拆分,而接口沒有。

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

相同點:

1、接口、類和結構都可以從多個接口繼承。

2、接口類似於抽象基類:繼承接口的任何非抽象類型都必須實現接口的所有成員。

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

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

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

2、表達的概念:接口用於規范,更強調契約,抽象類用於共性,強調父子。抽象類是一類事物的高度聚合,那麼對於繼承抽象類的子類來說,對於抽象類來說,屬於"Is A"的關系;而接口是定義行為規范,強調“Can Do”的關系,因此對於實現接口的子類來說,相對於接口來說,是"行為需要按照接口來完成"。

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

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

5、新增方法的影響:在抽象類中,新增一個方法的話,繼承類中可以不用作任何處理;而對於接口來說,則需要修改繼承類,提供新定義的方法。

6、接口可以作用於值類型(枚舉可以實現接口)和引用類型;抽象類只能作用於引用類型。

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

5. 重載與覆蓋的區別?

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

覆寫:在類的繼承中使用,通過覆寫子類方法可以改變父類虛方法的實現。

主要區別

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

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

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

    A ab1 = new B1(); A ab2 = new B2();
    ab1.Print(); ab2.Print();   //這裡應該輸出什麼呢?輸出B1、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

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

  參考資料:

書籍:CLR via C#

書籍:你必須知道的.NET

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

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

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

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

 

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

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