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

1 類型基礎

面試出現頻率:基本上確定出現css

重要程度:10/10,身家性命般重要。一般這也是各類招聘工做的第一個要求,即「熟悉C#」的一部分。連這部分都不清楚的人,能夠說根本不知道本身天天都在幹什麼。咱們每天使用C#寫程序,但若是連C#基礎的東西都不懂,怎麼證實你「熟悉C#」呢?怎麼讓人覺的你對C#有興趣呢?html

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

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

最重要的是,若是你裝箱都不知道,面試官後面的N個連環問題立刻胎死腹中,他可能會一臉尷尬,由於「我只是用這個問題當破冰的啊,你怎麼已經倒地了」,甚至不知道該問你啥,你才知道。C#話題就此終結,和藹點的面試官,可能會問問你在簡歷上寫的其餘東西。但不管如何,你的價值已經狂跌了不止一個檔次。jquery

在老外看來,這部份內容更爲重要。不少老外,尤爲是從優越國家來的那幫人,認爲人找工做確定是爲了興趣,錢只不過是順帶得到的。他們根本不知道這世界上還有人會考慮一個自身毫無興趣,僅僅是工資高的工做。若是他們發現,你連裝箱都不知道是什麼,他們會以爲你不熟悉C#,對C#一點興趣都沒有,直接把你請出面試室,儘管你可能已經用C#寫了幾十個工程,手下可能已經有了幾個小弟。也許你會央求面試官轉換一個話題,例如問問設計模式,但我的認爲,基礎有問題的人,即便知道設計模式,作過不少項目,他寫出來的asp.net代碼多是一坨屎的概率要遠遠高於基礎沒問題,但徹底不懂asp.net的人。這也是爲何不少老外的C#書籍前幾章的內容好像都是些「毫無心義的」,「莫名其妙的」東西。CLR via C#更是其中的戰鬥機,你徹底不用看這本書,也能寫出一個後臺用asp.net MVC,前端html+css+jquery的ERP系統出來,先後端使用ajax通信,後端連數據庫,用sql查數據,作CRUD。可是你不能憑藉這個找到一個工資高的工做,由於會幹這個的人實在是太多了,以致於不夠值錢。若是你以爲這已是了不得的成就,那麼你這一輩子也就停留在這裏了。若是你還想掙得更多,那麼你就得會別人不會或者嗤之以鼻的東西。程序員

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

不少人反感基礎題,一個很大的緣由在於,問問題的人不會問。若是問法是考定義,好比問「值類型與引用類型有何區別?」 這種問題的答案一查都找獲得,也沒有什麼意義。較好的問法是,把概念問題融入到情景之中,或者構造一個連環問題。例如我遇到過的一個問題:你什麼時候會考慮使用一個結構體?我以爲一個不錯的答案是」當這個對象全部的屬性都是值類型時,例如刻畫N維座標系上的一個點」。若是面試者是如此做答,那麼你能夠繼續問「能夠用類型麼?「這個時候,實際上仍是在問你值類型與引用類型有何區別,但相比直接問就天然不少。這個問題並非概念題,而是天天工做都會要遇到的。你總須要創建自定義的對象吧,那你就得從類型,結構,接口...中選擇一個。ajax

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

參考資料:sql

  • 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也選擇不把控制權交給用戶。

相關文章
相關標籤/搜索