面試出現頻率:主要考察裝箱和拆箱。對於有筆試題的場合也可能會考一些基本的類型轉換是否合法。html
重要程度:10/10面試
CLR最重要的特性之一就是類型安全性。在運行時,CLR老是知道一個對象是什麼類型。對於基元類型之間的相互轉換,能夠顯式或者隱式執行,例如將一個int轉換爲long。但若是將精度較大的類型轉化爲精度較小的類型,必須顯式執行,且可能會丟失精度,但不會發生異常。能夠利用checked關鍵字強制擲出OverflowException異常。c#
CLR容許將一個對象轉化爲它的任何基類型。C#不要求任何特殊語法便可將一個對象轉換爲它的任何基類型。然而,將對象轉換爲它的某個派生類型時,C#要求開發人員只能進行顯式轉換,由於這樣的轉換可能在運行時失敗。安全
對基元類型進行轉換時,能夠顯式或者隱式執行。若是遇到丟失精度的狀況,C#將會向下取整(即不管如何都是捨去)。例如,對int的最大值轉換爲byte,將會獲得255。對一個小數位精度較高的數轉化爲小數位精度較低的數,則簡單的捨去多餘的小數位。ide
1 int a = int.MaxValue; 2 Console.WriteLine(a); 3 byte b = (byte) a; //255
若是去掉(byte),改成隱式執行,則會沒法經過編譯。能夠利用checked關鍵字檢查是否有溢出的狀況。工具
1 checked 2 { 3 byte b = (byte)a; //Overflow 4 Console.WriteLine(a + 1); //Overflow 5 Console.WriteLine(b); 6 }
也可使用unchecked關鍵字忽略全部的精度和溢出檢查。但因爲這就是編譯器的默認行爲,因此unchecked關鍵字不多用到。性能
能夠將一個對象轉化爲它的任何基類型。轉換時,將等號右邊的和左邊的類型進行比較。若是左邊的是基類,則安全,不然發生編譯時異常,必須進行顯式轉換。例如object a = new Manager能夠讀爲:Manager是一個object,因此這個(隱式)轉換是安全的。但反過來就錯誤。顯式轉換永遠發生運行時而不是編譯時異常。測試
例以下面的測試題,假定有以下的定義:優化
1 public class B 2 { 3 4 } 5 6 public class D : B 7 { 8 9 }
回答下面每一行代碼是能夠執行,仍是形成編譯時錯誤,或運行時錯誤: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。
拆箱與裝箱就是值類型與引用類型的轉換,其是值類型和引用類型之間的橋樑。之因此能夠這樣轉換是由於C#全部類型都源自Object(全部值類型都源於ValueType,而ValueType源於Object)。經過深刻了解拆箱和裝箱的過程,咱們能夠知道其包含了對堆上內存的操做,故其會消耗性能,由於這是徹底沒必要要的。當了解了新建對象時內存的活動以後,裝箱的內存活動就能夠很容易的推斷出來。
對於簡單的例子來講:
1 int x = 1023; 2 object o = x; //裝箱
執行完第一句後,託管堆沒有任何東西,棧上有一個整形變量。第二句就是裝箱。由於object是一個引用類型,它必須指向堆上的某個對象,而x是值類型,沒有堆上的對應對象。因此須要使用裝箱,在堆上創造一個x。裝箱包括瞭如下的步驟:
注意,不須要初始化int的類型對象,由於其在執行程序以前,編譯以後,就已經被CLR初始化了。
拆箱並非把裝箱的過程倒過來,拆箱的代價比裝箱低得多。拆箱不須要額外分配內存。
1 int i = 1; 2 object o = i; 3 var j = (byte) o;
拆箱包括瞭如下的步驟:
一般避免無謂的裝箱和拆箱,能夠經過使用泛型,令對象成爲強類型,從而也就沒有了轉換類型的可能。也能夠經過IL工具,觀察代碼的IL形式,檢查是否有關鍵字box和unbox。
可使用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 }
因爲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 }
面試出現頻率:基本上確定出現。特別是對字符串相加的性能問題的考察(由於也沒有什麼其餘好問的)。若是你指出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 }
字符串的行爲很像值類型:
咱們考慮將N個字符串鏈接起來的場景。在N極少時(小於8左右),StringBuilder的性能並不必定優於簡單的使用+運算符。因此此時,咱們不須要使用StringBuilder。
當N很大(例如超過100)時,StringBuilder的效能大大優於使用+運算符。
當N很大,但你知道N的肯定數值時,考慮使用String.Concat方法。這個方法的速度之因此快,主要有如下緣由:
不過,若是你能夠肯定最終字符串長度的值,並將其做爲初始長度分配給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
字符串的不變性指的是字符串一經賦值,其值就不能被更改。當使用代碼將字符串變量等於一個新的值時,堆上會出現一個新的字符串,而後棧上的變量指向該新字符串。沒有任何辦法更改原來字符串的值。
有時咱們不得不處理這樣的狀況,例如從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 }
而TryParse不會引起異常,它會返回一個bool值提示轉換是否成功:
1 int quantity; 2 if (int.TryParse(txtQuantity.Text, out quantity) == false) 3 { 4 quantity = 0; 5 }
代碼變得十分簡單易懂。固然,直接使用顯式轉換也是一種方法。顯式轉換和TryParse並無顯著的性能區別。
歷來沒有人問過我關於這方面的問題,我也是不久以前才學到的。簡單來講,字符串駐留是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管轄,因此要到程序結束纔會釋放。