.NET面試題系列[3] - C# 基礎知識(1)


1 類型基礎

面試出現頻率:基本上肯定出現

重要程度:10/10,身家性命般重要。通常這也是各種招聘工作的第一個要求,即“熟悉C#”的一部分。連這部分都不清楚的人,可以說根本不知道自己每天都在干什么。我們天天使用C#寫程序,但如果連C#基礎的東西都不懂,怎么證明你“熟悉C#”呢?怎么讓人覺的你對C#有興趣呢?

很多人去面試一發現面試官開始問基礎題,就十分不爽,被淘汰了之后,還寫博客說面試官垃圾,怎么不問問項目經歷,哥可是做過不少項目的。殊不知,面試官知道你做過那些項目,但通常來說,如果那些項目不是牛逼透頂的級別(例如你參與了淘寶雙11導致數據庫並發問題的改進,或者AlphaGo的算法設計),或者正好是面試官所在公司需要的類型,則這並不是什么很厲害的事情,是個程序員就有幾個項目在身,“做過不少項目”的牛逼程度,差不多等於“活過20幾年”(我都活了20幾年了,我牛逼么?)。每個人都有的東西,有什么好問的,問你了你能確定你能答得比別人好么?但是如果你不能答出什么是裝箱,你會引發面試官以下的猜想:

  • 這人連最基礎的東西都不知道,還寫了熟悉C#,他還寫了熟悉XX,熟悉YY,看來他對那些東西可能也就了解皮毛。呵呵他還說他懂設計模式
  • 這人連最基礎的東西都不知道,說明他平常不看書。連書都不看,對技術肯定沒有什么興趣
  • 這人寫了他做過20個項目,但在我看來,他們大同小異,和做過1個項目也沒區別
  • 這人還寫了他有管理經驗,自己都這樣,小弟的水平。。。

最重要的是,如果你裝箱都不知道,面試官后面的N個連環問題馬上胎死腹中,他可能會一臉尷尬,因為“我只是用這個問題當破冰的啊,你怎么已經倒地了”,甚至不知道該問你啥,你才知道。C#話題就此終結,和善點的面試官,可能會問問你在簡歷上寫的其他東西。但無論如何,你的價值已經狂跌了不止一個檔次。

在老外看來,這部分內容更為重要。很多老外,尤其是從優越國家來的那幫人,認為人找工作肯定是為了興趣,錢只不過是順帶獲得的。他們根本不知道這世界上還有人會考慮一個自身毫無興趣,僅僅是工資高的工作。如果他們發現,你連裝箱都不知道是什么,他們會覺得你不熟悉C#,對C#一點興趣都沒有,直接把你請出面試室,盡管你可能已經用C#寫了幾十個工程,手下可能已經有了幾個小弟。也許你會央求面試官轉換一個話題,例如問問設計模式,但個人認為,基礎有問題的人,即使知道設計模式,做過很多項目,他寫出來的asp.net代碼可能是一坨屎的幾率要遠遠高於基礎沒問題,但完全不懂asp.net的人。這也是為什么很多老外的C#書籍前幾章的內容好像都是些“毫無意義的”,“莫名其妙的”東西。CLR via C#更是其中的戰斗機,你完全不用看這本書,也能寫出一個后台用asp.net MVC,前端html+css+jquery的ERP系統出來,前后端使用ajax通訊,后端連數據庫,用sql查數據,做CRUD。但是你不能憑借這個找到一個工資高的工作,因為會干這個的人實在是太多了,以至於不夠值錢。如果你覺得這已經是了不起的成就,那么你這一生也就停留在這里了。如果你還想掙得更多,那么你就得會別人不會或者嗤之以鼻的東西。

而工資高的工作,或對性能有很高的要求,或如果你寫的代碼太差,那真的會出大事,所以不能請基礎差的人(當然好公司會有層層環境把關,但如果你的代碼老出問題,你的水平弱於公司平均水平太多,他們也不會請你)。小公司尤其是外包,或者沒什么名氣的公司寫的產品,本身也沒有多少人用,崩潰了不會死人,所以代碼垃圾一點無妨,只要能按時完成任務就得。

很多人反感基礎題,一個很大的原因在於,問問題的人不會問。如果問法是考定義,比如問“值類型與引用類型有何區別?” 這種問題的答案一查都找得到,也沒有什么意義。較好的問法是,把概念問題融入到情景之中,或者構造一個連環問題。例如我遇到過的一個問題:你何時會考慮使用一個結構體?我覺得一個不錯的答案是”當這個對象所有的屬性都是值類型時,例如刻畫N維坐標系上的一個點”。如果面試者是如此作答,那么你可以繼續問“可以用類型么?“這個時候,實際上還是在問你值類型與引用類型有何區別,但相比直接問就自然很多。這個問題並不是概念題,而是每天工作都會要遇到的。你總需要建立自定義的對象吧,那你就得從類型,結構,接口...中選擇一個。

需要理解的程度:熟悉值類型和引用類型的區別,以及它們之間是可以轉換的(雖然這種轉換基本上是一定要避免的)。對棧和堆上內存的活動有着清醒的認識。

參考資料:

  • http://www.cnblogs.com/anding/p/5229756.html
  • CLR via C#

1.1 公共類型系統(CTS)

公共類型系統(CTS)是用來描述IL的,它規定了IL能做什么,能定義什么樣的變量,類中允許擁有什么成員等等。如果你寫了一個不遵循CTS的語言(以及一個編譯器),那么你的語言不能被看成是.NET平台的語言,編譯出來的中間代碼(如果有的話)不是IL。CTS和IL是所有.NET語言的爸爸。

C#的數據類型可以分為值類型和引用類型。這是因為,CTS爸爸規定數據類型可以分為值類型和引用類型,而且C#實現了這部分功能。你可以開發一個遵循CTS的語言,但不實現任何值類型。

所有類型都從System.Object派生,接口是一個特例。下面是一些主要的System.Object提供的方法:

  • Equals(obj):虛方法。如果兩個對象具有相同的引用就返回true。
    • 注意,盡管引用類型可能包含許多成員,比較引用類型時,僅僅考慮棧上的兩個對象是否指向堆上相同的對象,而不會逐個成員比較,所以對於引用類型,不需要重寫該方法。
    • System.ValueType(值類型)重寫了該方法,使得方法不比較對象指針是否指向同一個對象,而是僅僅比較值是否相等。此時,如果值類型包含很多成員(例如結構),會使用反射逐個成員比較。為了避開反射造成的性能損失,你必須重寫該方法,你只需要在其中遍歷所有結構的屬性,並一一進行比較即可。如果你自定義的結構的相等邏輯不要求所有的屬性相等才意味着相等,而只是部分屬性相等就意味着相等時,你也需要重寫該方法。
    • 值得注意的是,雖然字符串是引用類型,它也重寫了該方法,其行為和值類型一樣。
  • Equals(obj1, obj2):靜態方法,若兩個輸入變量均為null則返回true。若僅有一個是null則返回false。若都不是null則調用obj1.Equals(obj2)。故該方法無需重寫,也不是虛方法。
  • GetHashCode:在FCL中,任何對象的任何實例都對應一個哈希碼。為此,System.Object的虛方法GetHashCode能獲取任意對象的哈希碼。如果你定義的一個類型重寫了Equal方法,那么還應重寫GetHashCode方法。事實上如果你沒有這么做的話,編譯器會報告一條警告消息:重寫了Equal但不重寫GetHashCode。CLR via C#中說,一般都要重寫Object的GetHashCode方法,因為它的算法性能不高。但我對這一部分沒有深入研究。
  • ToString:虛方法。返回類型的完整名稱(this.GetType().FullName)。重寫它的可能性很大,例如你希望ToString遍歷對象的所有屬性,打印出它所有屬性的值。
  • GetType:返回對象的類型對象指針指向的類型對象。
  • Finalize:在GC決定回收這個對象之后,會調用這個方法。如果要做一些額外的例如回收對象的非托管屬性或對象,應當重寫這個方法。只有在存在非托管對象時才需要這么做。在垃圾回收中會詳細介紹。

1.2 New操作符

CLR要求所有對象都用new操作符來創建。對於值類型,你可以直接賦值,這相當於隱式的調用了new操作符。new操作符所做的事情有:

  1. 計算類型及其所有基類型中定義的實例字段需要的字節數,另外,如果是引用類型,還需要預留空間給”類型對象指針“和”同步塊索引“。如果發現棧或者堆上的空間不足,就引發OutOfMemory異常,並激發一次垃圾回收。
  2. 如果是引用類型,從堆上分配第一步算出來的字節數。
  3. 初始化”類型對象指針“和”同步塊索引“。令”類型對象指針“指向堆上該類型的類型對象。如果類型對象不存在,則創建一個。並且如果類型有靜態成員,則初始化它們,如果類型有靜態構造函數,調用靜態構造函數,初始化或者修改(因為靜態構造函數在初始化靜態成員之后進行,所以可能會造成修改)類中的靜態成員的值。如果類型對象已經存在,則不會再次調用靜態構造函數。
  4. 調用類型的實例初始化器,初始化類型的非靜態成員。

例如下面的代碼中,C#首先將a初始化為5,然后再修改成10。

1     class SomeType
2     {
3         private static int a = 5;
4 
5         static SomeType()
6         {
7             a = 10;
8         }
9     }
View Code

 

1.2(轉) CLR via C#上的例子

 CLR via C#上的這個例子可以讓我們透徹理解前一小節的內容以及內存中的各種活動。假設我們有如下的定義。

如果代碼如下圖左下角所示,則開始執行的時刻,內存中的情況如下圖:

當CLR掃描完M3方法之后,發現有兩個引用類型Employee和Manager,故計算這兩個類型及其所有基類型中定義的所有實例字段需要的字節數,在堆上建立兩個類型對象,它們的構造相同:類型對象指針(TypeHandle),同步塊索引,靜態字段集合與方法表(儲存了所有的方法)。

因為程序還沒運行到第二行,所以棧上暫時還沒有那個整型對象year。當運行完前2行時,棧中多了2個成員。一個Employee對象e被創建,但其沒有指向任何東西。

但運行完第三行后,new關鍵字在堆上新建了一個實例,並返回這個引用,使得e指向一個Manager實例,這個實例的類型對象指針指向Manager類型對象。注意,一個類型無論有多少個實例,它們在堆中的對象都指向一個類型對象。另外需要關注的是,靜態字段在類型對象中,而類型對象是唯一的,所以所有該類型的實例都指向一個類型對象,意味着一個實例更改了靜態字段的值,所有其他實例都會受影響。

 

第四句調用了靜態方法lookup。假設結果表明,Joe是公司的一名經理,則該方法將返回一個Manager對象。此時堆中將再次創建一個新的Manager對象,而e將會被指向這個新的對象。這個新的對象將會被初始化,Joe將作為其初始化的信息的一部分(不再是默認的值,例如0或者Null)。

注意此時第一個Manager對象將會變成垃圾,等待垃圾回收器的回收。兩個Manager對象指向一個Manager類型對象。

第五句代碼將調用一個Employee類型的方法,假設返回5,那么year的值將變成5。

最后一句是一個虛方法,執行虛方法時,和實方法不同。我們要看虛方法有沒有被人重寫,還要根據調用虛方法的對象(e)確定使用父類中的方法,還是子類中重寫的方法。根據上圖發現,e其實是一個指向Manager對象的東西,於是,我們執行在Manager類中重寫的那個方法。

注意如果在第四句中,Joe僅僅是一個Employee而不是Manager的話,那么堆中將不會有第二個Manager對象,而取而代之為一個新的Employee對象。最后一句也會執行在Employee中的方法,而不是Manager中的方法。

1.3 類型對象

一個類型無論有多少個實例,它們在堆中的對象的類型對象指針都指向同一個類型對象。之所以只有一個類型對象,是因為不需要有多於一個(所有相同類型的定義都相同,都有相同的方法表)。所以,類型對象是儲存類型靜態成員最恰當的地方。類型對象由CLR在堆中的一個特殊地方(加載堆)創建(在第一次使用前),其中包括了類型的靜態字段和方法表。創建完之后,就不會改變,通過這個事實,可以驗證靜態字段的全局(被所有同類型的實例共享)性。

類型對象是反射的重要操作對象。如果你要處理一個謎之對象,你不知道他有什么方法,那么你只能通過訪問它的類型對象,你才知道這個謎一般的對象究竟包括什么方法。然后你就可以調用這些方法。GetType方法會返回對象指向的類型對象(包括靜態成員和方法表)。

加載堆不受GC控制,所以靜態字段和屬性也不受GC控制。

1 int a = 123;                                          // 創建int類型實例a
2 int b = 20;                                           // 創建int類型實例b
3 var atype = a.GetType();                        // 獲取對象實例a的類型Type
4 var btype = b.GetType();                        // 獲取對象實例b的類型Type
5 Console.WriteLine(System.Object.Equals(atype,btype));           //輸出:True
6 Console.WriteLine(System.Object.ReferenceEquals(atype, btype)); //輸出:True
View Code


這意味着,內存中只有一個Int32類型對象,否則ReferenceEquals是不可能輸出True的。

注意,類型對象也有類型對象指針,這是因為類型對象本質上也是對象。所有的類型對象的“類型對象指針”都指向System.Type類型對象。特別的,System.Type類型對象本身也是一個對象,內部的“類型對象指針”指向它自己。

1.4 什么是基元類型?

屬於BCL而非任何某個語言的類型叫做基元類型(Primitive Type)。你可以在mscorlib.dll中找到它們。例如:

IL 類型                      C# 關鍵字           VB.NET關鍵字

System.Byte              byte                   Byte

Sytem.Int16              short                  Short

System.Int64             int                     Integer

特別的,string映射到基元類型String。所以它們並沒有任何區別。

1.5 值類型與引用類型有何區別?

C#的數據類型可以分為值類型和引用類型,它們的區別主要有:

  1. 所有值類型隱式派生自System.ValueType。該類確保值類型全部分配在棧上(結構體除外,結構體如果含有引用類型,則那部分也會分配在堆上)。所有引用類型隱式派生自System.Object。引用類型初始化在棧和堆上。
  2. 引用類型的初值為null。值類型則是0。因為字符串的初值為null,故字符串為引用類型。因為接口是一種特殊的抽象類,所以接口是引用類型。因為委托是密封類,所以委托是引用類型。
  3. 棧中會有一個變量名和變量類型,指向堆中的對象實例的地址。值類型僅有棧中的變量名和類型,不包括指向實例的指針。
  4. 值類型不能有繼承,引用類型則可以。典型的例子是結構體,他是值類型,結構體不能被繼承。但結構體里面可以包括引用類型。值類型也可以有自己的方法,例如Int.TryParse方法。但方法是隱式的密封方法。
  5. 值類型的生命周期是其定義域。當值類型離開其定義域后將被立刻銷毀。引用類型則會進入垃圾回收分代算法。我們不知道何時才會銷毀。
  6. 當我們創建了某個引用類型的實例后,再復制一個新的時,將只會復制指針。例如:

A a = new A();

A a2 = a;

此時在堆中只有一個A的實例,而a和a2都指向它。所以如果我們更改了a中某個成員的值,a2中相應的成員也會更改。(這稱為淺復制,與之對應的深復制則是要逐一復制對象所有成員的值,C#沒有深復制的方法,要自己實現)值類型則完全不同,復制值類型將進行逐字段的復制,而沒有指針參與。所以值類型是相互獨立的。更改其中一個對另外一個不會有影響。

 

1.6 類和結構的主要區別?結構對象可能分配在堆上嗎?何時考慮使用結構體?

類和結構是C#兩個最主要的研究對象:

  1. 結構是值類型,它繼承自System.ValueType,而類是引用類型。
  2. 因為值類型不能被繼承,故結構不能被繼承。
  3. 結構可以有自己的方法,一個典型的例子為.NET中的結構體Int32含有方法Parse,TryParse等等。
  4. 結構可以實現接口。
  5. 雖然結構是值類型,這不意味着結構中不能包括引用類型(但如果一個結構里面包含引用類型,考慮使用類)。結構體如果含有引用類型,則那部分也會分配在堆上。
  6. 結構體的構造函數必須初始化它的所有成員。結構的構造函數不會被自動調用。

當試圖表現例如點(X維坐標上的),形狀(長,寬,面積等屬性)等全部為值類型組成的對象時,考慮使用結構體。例如,如果聲明一個 1000 個 Point 對象組成的數組,為了引用每個對象,則需分配更多內存(堆上的1000個實例);這種情況下,使用結構可以節約資源。當數組不用時,如果是使用結構體,則1000個對象將馬上銷毀,如果是使用類,則還要等GC,無形中提升了GC壓力。

1.6.1 在.NET的基礎類庫中,舉出一個是類和一個是結構的例子

Console是一個類。

Int32是一個結構。其只含有兩個常數的,Int32類型的字段(最小值和最大值),和若干方法。

這兩者均位於基礎類庫mscorlib中。

 

1.6.2 實例構造函數(類型)

類型的實例構造函數不能被繼承。它負責將類型的實例字段初始化。對於靜態字段,由靜態構造函數負責。

如果類型沒有定義任何構造函數,則編譯器將定義一個沒有參數的構造函數。其會簡單地調用基類的無參構造函數。特別的,由於System.Object沒有任何實例字段,所以它的構造函數什么也不做。

可以聲明多個不同的構造函數。可以利用this關鍵字來調用其它構造函數。

 

1.6.3 實例構造函數(結構)

結構體的構造函數必須初始化它的所有成員。結構的構造函數不會被自動調用。

不能顯式地為結構聲明無參數的構造函數。

 

1.6.4 靜態構造函數

靜態構造函數是一個特殊的構造函數,它會在這個類型第一次被實例化引用任何靜態成員之前,CLR在堆上創建類型對象時執行,它具有以下特點:

  1. 靜態構造函數既沒有訪問修飾符,也沒有參數
  2. 在創建第一個實例或引用任何靜態成員之前,將自動調用靜態構造函數來初始化類(的類型對象)。這個靜態構造函數只會執行一次。
  3. 無法直接調用靜態構造函數。它的訪問修飾符是private(不需要寫明)。
  4. 在程序中,用戶無法控制何時執行靜態構造函數。
  5. 靜態構造函數不應該調用基類型的靜態構造函數。這是因為類型不可能有靜態字段是從基類型分享或繼承的。

如果我們不了解堆上的內存分配方式,對靜態構造函數的理解會十分困難。為什么是在創建第一個實例之前?為什么不能直接調用?為什么不能有參數?我們完全無法理解,只能通過死記硬背的方式記住這些性質。但如果你知道靜態成員在類型對象中,並不存在於任何的實例中,可能你就會理解這些性質。

當我們清楚的了解了類型對象以及CLR對類型對象的處理方式時,理解靜態構造函數以及類型的靜態成員就顯得十分自然了。當創建第一個實例之前,堆上沒有類型對象,所以要調用靜態構造函數,當引用靜態成員之前,堆上也沒有類型對象,而靜態成員屬於類型對象,所以也要調用靜態構造函數,這兩種情況的最終結果,都是堆上最終出現了一個類型對象。因為類型對象只需要建立一次,所以這個靜態構造函數也只能運行一次。

為什么靜態構造函數既沒有訪問修飾符,也沒有參數?這是因為靜態構造函數只負責初始化靜態成員,只負責維護類型對象,它和類型的實例對象沒有關系,所以你加入任何參數(你試圖為非靜態的字段或屬性賦值?這是不可能的,因為根本就沒有實例)都是沒有意義的。

無法直接調用靜態構造函數:現在顯然十分容易理解了,因為類型對象只能有一個,如果可以隨便被調用,則可能會創造出好幾個類型對象,破壞靜態字段的全局性。CLR也選擇不把控制權交給用戶。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM