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

2 類型轉換

面試出現頻率:主要考察裝箱和拆箱。對於有筆試題的場合也可能會考一些基本的類型轉換是否合法。html

重要程度:10/10面試

CLR最重要的特性之一就是類型安全性。在運行時,CLR老是知道一個對象是什麼類型。對於基元類型之間的相互轉換,能夠顯式或者隱式執行,例如將一個int轉換爲long。但若是將精度較大的類型轉化爲精度較小的類型,必須顯式執行,且可能會丟失精度,但不會發生異常。能夠利用checked關鍵字強制擲出OverflowException異常。c#

CLR容許將一個對象轉化爲它的任何基類型。C#不要求任何特殊語法便可將一個對象轉換爲它的任何基類型。然而,將對象轉換爲它的某個派生類型時,C#要求開發人員只能進行顯式轉換,由於這樣的轉換可能在運行時失敗。安全

2.1 基元類型的類型轉換

對基元類型進行轉換時,能夠顯式或者隱式執行。若是遇到丟失精度的狀況,C#將會向下取整(即不管如何都是捨去)。例如,對int的最大值轉換爲byte,將會獲得255。對一個小數位精度較高的數轉化爲小數位精度較低的數,則簡單的捨去多餘的小數位。ide

1 int a = int.MaxValue;
2 Console.WriteLine(a);
3 byte b = (byte) a;             //255
View Code

若是去掉(byte),改成隱式執行,則會沒法經過編譯。能夠利用checked關鍵字檢查是否有溢出的狀況。工具

1             checked
2             {
3                 byte b = (byte)a;             //Overflow
4                 Console.WriteLine(a + 1);     //Overflow
5                 Console.WriteLine(b);
6             }
View Code

也可使用unchecked關鍵字忽略全部的精度和溢出檢查。但因爲這就是編譯器的默認行爲,因此unchecked關鍵字不多用到。性能

2.2 引用類型之間的類型轉換

能夠將一個對象轉化爲它的任何基類型。轉換時,將等號右邊的和左邊的類型進行比較。若是左邊的是基類,則安全,不然發生編譯時異常,必須進行顯式轉換。例如object a = new Manager能夠讀爲:Manager是一個object,因此這個(隱式)轉換是安全的。但反過來就錯誤。顯式轉換永遠發生運行時而不是編譯時異常。測試

 

例以下面的測試題,假定有以下的定義:優化

1     public class B
2     {
3 
4     }
5 
6     public class D : B
7     {
8 
9     }
View Code

回答下面每一行代碼是能夠執行,仍是形成編譯時錯誤,或運行時錯誤:ui

Object o1 = new Object();

能夠執行。

 

Object o2 = new B();

能夠執行。這將會在棧上新建一個名爲o2的對象,類型爲Object。他指向堆上的B類型對象。由於Object類型是B的基類,因此類型安全。但因爲o2的類型是Object,o2將只擁有Object的那幾個方法(你能夠自行在IDE中試驗一下)。若是你執行Console.WriteLine(o2.GetType()),你會獲得[命名空間名稱].B,也就是說,GetType返回指向的類型對象的具體類型名稱

 

Object o3 = new D();

能夠執行,緣由同上。

 

Object o4 = o3;

能夠執行,能夠將其當作Object o4 = new D();

在執行完上面四句話以後,內存中的情況如圖:

若是你執行Console.WriteLine(object.ReferenceEquals(o3, o4)),會獲得true的返回值,由於它們指向同一個實例。咱們繼續往下看:

 

B b1 = new B();

能夠執行。

 

B b2 = new D();

能夠執行。緣由同第二個。

 

D d1 = new D();

能夠執行。

 

B b3 = new Object();

編譯時錯誤。不能將Object類型轉爲B。

 

D d2 = new Object();

編譯時錯誤。緣由同上。在執行完上面全部語句以後,內存中的情況如圖(省略了類型對象指針):

 

 

B b4 = d1;

能夠執行由於左邊的B是基類,d1是派生類D。

 

D d3 = b2;

編譯時錯誤。左邊的是派生類,而b2的類型是B(在棧上的類型)。

 

D d4 = (D) d1;

能夠執行。由於d1也是D類型,故沒有發生實際轉換。在執行完上面全部語句以後,內存中的情況如圖(省略了類型對象指針):

 

D d6 = (D) b1;

運行時錯誤。在顯式轉換中,b1的類型是B,不能轉換爲其派生類D。經過顯式轉換永遠不會發生編譯時錯誤。

 

B b5 = (B) o1;

運行時錯誤。在顯式轉換中,o1的類型是基類Object,不能轉換爲其派生類B。

 

2.3 什麼是拆箱和裝箱?它們對性能的損耗體如今何處?

拆箱與裝箱就是值類型與引用類型的轉換,其是值類型和引用類型之間的橋樑。之因此能夠這樣轉換是由於C#全部類型都源自Object(全部值類型都源於ValueType,而ValueType源於Object)。經過深刻了解拆箱和裝箱的過程,咱們能夠知道其包含了對堆上內存的操做,故其會消耗性能,由於這是徹底沒必要要的。當了解了新建對象時內存的活動以後,裝箱的內存活動就能夠很容易的推斷出來。

裝箱的過程

對於簡單的例子來講:

1 int x = 1023;
2 object o = x; //裝箱
View Code

執行完第一句後,託管堆沒有任何東西,棧上有一個整形變量。第二句就是裝箱。由於object是一個引用類型,它必須指向堆上的某個對象,而x是值類型,沒有堆上的對應對象。因此須要使用裝箱,在堆上創造一個x。裝箱包括瞭如下的步驟:

  1. 分配內存。這個例子中須要一個整形變量,加上託管堆上全部的對象都有的兩個額外成員(類型對象指針和同步塊索引)那麼多的內存。類型對象指針指向int類型對象。
  2. 值類型的變量複製到新分配的堆內存。
  3. 返回對象的地址。

注意,不須要初始化int的類型對象,由於其在執行程序以前,編譯以後,就已經被CLR初始化了。

拆箱的過程

拆箱並非把裝箱的過程倒過來,拆箱的代價比裝箱低得多。拆箱不須要額外分配內存。

1             int i = 1;            
2             object o = i;
3             var j = (byte) o;
View Code

拆箱包括瞭如下的步驟:

  1. 若是已裝箱實例爲null,拋出NullReference異常
  2. 若是對象不是null但類型不是原先未裝箱的值類型,則拋出InvalidCast異常,好比上面的代碼
  3. 獲取已裝箱實例中值類型字段的地址
  4. 建立一個新的值類型變量,其值使用第三步獲取到的值(複製)

一般避免無謂的裝箱和拆箱,能夠經過使用泛型,令對象成爲強類型,從而也就沒有了轉換類型的可能。也能夠經過IL工具,觀察代碼的IL形式,檢查是否有關鍵字box和unbox。

2.4 使用is或as關鍵字進行類型轉換

可使用is或as關鍵字進行類型轉換。

is將檢測一個對象是否兼容於指定的類型,並返回一個bool。它永遠不會拋出異常。若是轉型對象是null,就返回false。典型的應用is進行類型轉換的方式爲:

 1 object o = new object();
 2 class A
 3 {
 4  
 5 }
 6 
 7 if (o is A)  //執行第一次類型兼容檢查
 8 {
 9   A a = (A) o;  //執行第二次類型兼容檢查
10 }
View Code

因爲is實際上會形成兩次類型兼容檢查,這是沒必要要的。as關鍵字在必定程度上,能夠改善性能。as永遠不會拋出異常,若是轉型對象是null,就返回null。典型的應用as進行類型轉換的方式爲:

1 object o = new object();
2 class B
3 {
4 }
5 B b = o as B;  //執行一次類型兼容檢查
6 if (b != null)
7 {  
8   MessageBox.Show("b is B's instance.");
9 }
View Code

 

3. 字符串

面試出現頻率:基本上確定出現。特別是對字符串相加的性能問題的考察(由於也沒有什麼其餘好問的)。若是你指出StringBuilder是一個解決方案,並強調必定要爲其設置一個初始容量,面試官將會很高興。

重要程度:10/10。

 

字符串是引用類型。能夠經過字符串的默認值爲null來記憶這點。string是基元類型String在c#中的別名,故這二者沒有任何區別。

注意字符串在修改時,是在堆上建立一個新的對象,而後將棧上的字符串指向新的對象(舊的對象變爲垃圾等待GC回收)。字符串的值是沒法被修改的(具備不變性)。考慮使用StringBuilder來防止創建過多對象,減輕GC壓力。

字符串的==操做和.Equal是相同的,由於==已經被重寫爲比較字符串的值而不是其引用。做爲引用類型,==原本是比較引用的,但此時被重寫,這也是字符串看起來像值類型的一個緣由。

當使用StringBuilder時,若是你大概知道要操做的字符串的長度範圍,請指明它的初始長度。這能夠避免StringBuilder初始化時不斷擴容致使的資源消耗。

你常常會有機會擴展這個類,例如爲這個類擴展一個顛倒的字符串方法:

1     public static string Reverse(string s)
2     {
3         char[] charArray = s.ToCharArray();
4         Array.Reverse(charArray);
5         return new string(charArray);
6     }
View Code

 

3.1 字符串和普通的引用類型相比有什麼特別的地方嗎?

字符串的行爲很像值類型:

  1. 字符串使用等於號互相比較時,比較的是字符串的值而不是是否指向同一個引用,這和引用類型的比較不一樣,而和值類型的比較相同。
  2. 字符串雖然是引用類型,但若是在某方法中,將字符串傳入另外一方法,在另外一方法內部修改,執行完以後,字符串的值並不會改變,而引用類型不管是按值傳遞仍是引用傳遞,值都會發生變化。

3.2 關於StringBuilder的性能問題

咱們考慮將N個字符串鏈接起來的場景。在N極少時(小於8左右),StringBuilder的性能並不必定優於簡單的使用+運算符。因此此時,咱們不須要使用StringBuilder。

當N很大(例如超過100)時,StringBuilder的效能大大優於使用+運算符。

當N很大,但你知道N的肯定數值時,考慮使用String.Concat方法。這個方法的速度之因此快,主要有如下緣由:

  1. 當N肯定,每一個字符串也肯定時,最終的字符串長度就肯定了。此時,能夠一次性爲其分配這麼大塊的內存。而StringBuilder若是沒有指明初始長度,或指定了一個較小的長度,則會不斷擴容,消耗資源。擴容的動做分爲以下幾步:在內存中分配一個更大的空間,而後將現有的字符串複製過去(還餘下一些空位for further use)
  2. StringBuilder有線程安全的考慮,故會拖慢一點時間

不過,若是你能夠肯定最終字符串長度的值,並將其做爲初始長度分配給StringBuilder,則StringBuilder將不須要擴容,其性能將與String.Concat方法幾乎相同(因爲還有性能安全的考慮,故會稍微慢一點點)。

參考:

http://blog.zhaojie.me/2009/11/string-concat-perf-1-benchmark.html

http://blog.zhaojie.me/2009/12/string-concat-perf-2-stringbuilder-implementations.html

http://blog.zhaojie.me/2009/12/string-concat-perf-3-profiling-analysis.html

 

3.3 什麼是字符串的不變性?

字符串的不變性指的是字符串一經賦值,其值就不能被更改。當使用代碼將字符串變量等於一個新的值時,堆上會出現一個新的字符串,而後棧上的變量指向該新字符串。沒有任何辦法更改原來字符串的值。

 

3.4 字符串轉換爲值類型

有時咱們不得不處理這樣的狀況,例如從WPF應用的某個文本框中得到一個值,並將其轉換爲整數。以int爲例,其提供了兩個靜態方法Parse和TryParse。當轉換失敗時,Parse會擲出異常,使用Parse的異常處理比較麻煩:

 1 int quantity;
 2 try
 3 {
 4     quantity = int.Parse(txtQuantity.Text);
 5 }
 6 catch (FormatException)
 7 {
 8     quantity = 0;
 9 }
10 catch (OverflowException)
11 {
12     quantity = 0;
13 }
View Code

而TryParse不會引起異常,它會返回一個bool值提示轉換是否成功:

1 int quantity;
2 if (int.TryParse(txtQuantity.Text, out quantity) == false)
3 {
4     quantity = 0;
5 }
View Code

代碼變得十分簡單易懂。固然,直接使用顯式轉換也是一種方法。顯式轉換和TryParse並無顯著的性能區別。

3.5 字符串的駐留(interning)

歷來沒有人問過我關於這方面的問題,我也是不久以前才學到的。簡單來講,字符串駐留是CLR的JIT作代碼優化時,送給咱們的一個小禮物。CLR會維護一個字符串駐留池(內部哈希表),並在新建字符串時,探查是否已經有相同值的字符串存在。只有如下兩種狀況纔會自動探查。

1. 若是編譯器發現已經有相同值的字符串存在,則不新建字符串(在堆上),而是讓新舊兩字符串變量在棧上指向同一個堆上的字符串值。若是沒有則在駐留池中增長一個新的成員。

var s1 = "123";
var s2 = "123";
Console.WriteLine(System.Object.Equals(s1, s2));  //輸出 True
Console.WriteLine(System.Object.ReferenceEquals(s1, s2));  //輸出 True

這意味着,堆上只有一條字符串「123」(隱式駐留)。若是咱們預先知道許多字符串對象均可能有相同的值,就能夠利用這點來提升性能。字符串的駐留的另外一個體現方式是常量字符串相加的優化。下面例子輸出結果也是兩個True:

string st1 = "123" + "abc";
string st2 = "123abc";
Console.WriteLine(st1 == st2);
Console.WriteLine(System.Object.ReferenceEquals(st1, st2));

堆上的字符串只有一個 ----「123abc」。下面例子則稍有不一樣:

string s1 = "123";
string s2 = s1 + "abc";
string s3 = "123abc";
Console.WriteLine(s2 == s3);
Console.WriteLine(System.Object.ReferenceEquals(s2, s3));

第二個布爾值爲False,由於變量和常量相加的動做不會被編譯器優化

並不是每次新建字符串,或者經過某種方式生成了一條新的字符串時,其都會被駐留。例如,上面例子中,變量字符串和常量字符串相加,就沒有觸發駐留行爲,同理ToString,ToUpper等方法也不會(只有上面兩種狀況纔會)。咱們也能夠經過訪問駐留池來顯式留用字符串。咱們可使用方法string.Intern爲駐留池新增一個字符串,或者使用方法IsInterned探查字符串是否已經被駐留。

由於變量字符串和常量字符串相加沒法利用駐留行爲,因此不管咱們怎麼改進,上面的最後一行老是會輸出False。例如:

string s1 = "123";
String.Intern("123abc");
string s2 = s1 + "abc";

string s3 = "123abc";
Console.WriteLine(s2 == s3);
Console.WriteLine(System.Object.ReferenceEquals(s2, s3));

此時s2的建立根本不會搭理駐留池。同理,這樣也不行:

string s1 = "123";
String.Intern("123");
string s2 = 123.ToString();

Console.WriteLine(System.Object.ReferenceEquals(s2, s1));

一般來講,字符串駐留只有在常量字符串的分配和相加時纔有意義。並且,咱們要注意到字符串駐留的一個負面影響:駐留池的內存不受GC管轄,因此要到程序結束纔會釋放。

相關文章
相關標籤/搜索