字符串可以說是C#開發中最常用的類型了,也是對系統性能影響很關鍵的類型,熟練掌握字符串的操作非常重要。
常見面試題目:
1.字符串是引用類型類型還是值類型?
2.在字符串連接處理中,最好采用什么方式,理由是什么?
3.使用 StringBuilder時,需要注意些什么問題?
4.以下代碼執行后內存中會存在多少個字符串?分別是什么?輸出結果是什么?為什么呢?
string st1 = "123" + "abc"; string st2 = "123abc"; Console.WriteLine(st1 == st2); Console.WriteLine(System.Object.ReferenceEquals(st1, st2));
5.以下代碼執行后內存中會存在多少個字符串?分別是什么?輸出結果是什么?為什么呢?
string s1 = "123"; string s2 = s1 + "abc"; string s3 = "123abc"; Console.WriteLine(s2 == s3); Console.WriteLine(System.Object.ReferenceEquals(s2, s3));
6.使用C#實現字符串反轉算法,例如:輸入"12345", 輸出"54321"。
7.下面的代碼輸出結果?為什么?
object a = "123"; object b = "123"; Console.WriteLine(System.Object.Equals(a,b)); Console.WriteLine(System.Object.ReferenceEquals(a,b)); string sa = "123"; Console.WriteLine(System.Object.Equals(a, sa)); Console.WriteLine(System.Object.ReferenceEquals(a, sa));
深入淺出字符串操作
string是一個特殊的引用類型,使用上有點像值類型。之所以特殊,也主要是因為string太常用了,為了提高性能及開發方便,對string做了特殊處理,給予了一些專用特性。為了彌補string在字符串連接操作上的一些性能不足,便有了StringBuilder。
認識string
首先需要明確的,string是一個引用類型,其對象值存儲在托管堆中。string的內部是一個char集合,他的長度Length就是字符char數組的字符個數。string不允許使用new string()的方式創建實例,而是另一種更簡單的語法,直接賦值(string aa= “000”這一點也類似值類型)。
認識string,先從一個簡單的示例代碼入手:
public void DoStringTest() { var aa = "000"; SetStringValue(aa); Console.WriteLine(aa); } private void SetStringValue(string aa) { aa += "111"; }
上面的輸出結果為“000”。
通過前面的值類型與引用類型的文章,我們知道string是一個引用類型,既然是一個引用類型,參數傳遞的是引用地址,那為什么不是輸出“000111”呢?是不是很有值類型的特點呢!這一切的原因源於string類型的兩個重要的特性:恆定性與駐留性
String的恆定性(不變性)
字符串是不可變的,字符串一經創建,就不會改變,任何改變都會產生新的字符串。比如下面的代碼,堆上先創建了字符串s1=”a”,加上一個字符串“b”后,堆上會存在三個個字符串實例,如下圖所示。
string s1 = "a"; string s2 = s1 + "b";
上文中的”任何改變都會產生新的字符串“,包括字符串的一些操作函數,如str1.ToLower,Trim(),Remove(int startIndex, int count),ToUpper()等,都會產生新的字符串,因此在很多編程實踐中,對於字符串忽略大小的比較:
if(str1.ToLower()==str2.ToLower()) //這種方式會產生新的字符串,不推薦 if(string. Compare(str1,str2,true)) //這種方式性能更好
String的駐留性
由於字符串的不變性,在大量使用字符串操作時,會導致創建大量的字符串對象,帶來極大的性能損失。因此CLR又給string提供另外一個法寶,就是字符串駐留,先看看下面的代碼,字符串s1、s2竟然是同一個對象!
var s1 = "123"; var s2 = "123"; Console.WriteLine(System.Object.Equals(s1, s2)); //輸出 True Console.WriteLine(System.Object.ReferenceEquals(s1, s2)); //輸出 True
相同的字符串在內存(堆)中只分配一次,第二次申請字符串時,發現已經有該字符串是,直接返回已有字符串的地址,這就是駐留的基本過程。
字符串駐留的基本原理:
- CLR初始化時會在內存中創建一個駐留池,內部其實是一個哈希表,存儲被駐留的字符串和其內存地址。
- 駐留池是進程級別的,多個AppDomain共享。同時她不受GC控制,生命周期隨進程,意思就是不會被GC回收(不回收!難道不會造成內存爆炸嗎?不要急,且看下文)
- 當分配字符串時,首先會到駐留池中查找,如找到,則返回已有相同字符串的地址,不會創建新字符串對象。如果沒有找到,則創建新的字符串,並把字符串添加到駐留池中。

如果大量的字符串都駐留到內存里,而得不到釋放,不是很容易造成內存爆炸嗎,當然不會了?因為不是任何字符串都會駐留,只有通過IL指令ldstr創建的字符串才會留用。
字符串創建的有多種方式,如下面的代碼:
var s1 = "123"; var s2 = s1 + "abc"; var s3 = string.Concat(s1, s2); var s4 = 123.ToString(); var s5 = s2.ToUpper();
其IL代碼如下
在上面的代碼中,出現兩個字符串常量,“123”和“abc”,這個兩個常量字符串在IL代碼中都是通過IL指令ldstr創建的,只有該指令創建的字符串才會被駐留,其他方式產生新的字符串都不會被駐留,也就不會共享字符串了,會被GC正常回收。
那該如何來驗證字符串是否駐留呢,string類提供兩個靜態方法:
- String.Intern(string str) 可以主動駐留一個字符串;
- String.IsInterned(string str);檢測指定字符串是否駐留,如果駐留則返回字符串,否則返回NULL
請看下面的示例代碼
var s1 = "123"; var s2 = s1 + "abc"; Console.WriteLine(s2); //輸出:123abc Console.WriteLine(string.IsInterned(s2) ?? "NULL"); //輸出:NULL。因為“123abc”沒有駐留 string.Intern(s2); //主動駐留字符串 Console.WriteLine(string.IsInterned(s2) ?? "NULL"); //輸出:123abc
認識StringBuilder
大量的編程實踐和意見中,都說大量字符串連接操作,應該使用StringBuilder。相對於string的不可變,StringBuilder代表可變字符串,不會像字符串,在托管堆上頻繁分配新對象,StringBuilder是個好同志。
首先StringBuilder內部同string一樣,有一個char[]字符數組,負責維護字符串內容。因此,與char數組相關,就有兩個很重要的屬性:
- public int Capacity:StringBuilder的容量,其實就是字符數組的長度。
- public int Length:StringBuilder中實際字符的長度,>=0,<=容量Capacity。
StringBuilder之所以比string效率高,主要原因就是不會創建大量的新對象,StringBuilder在以下兩種情況下會分配新對象:
- 追加字符串時,當字符總長度超過了當前設置的容量Capacity,這個時候,會重新創建一個更大的字符數組,此時會涉及到分配新對象。
- 調用StringBuilder.ToString(),創建新的字符串。
追加字符串的過程:
- StringBuilder的默認初始容量為16;
- 使用stringBuilder.Append()追加一個字符串時,當字符數大於16,StringBuilder會自動申請一個更大的字符數組,一般是倍增;
- 在新的字符數組分配完成后,將原字符數組中的字符復制到新字符數組中,原字符數組就被無情的拋棄了(會被GC回收);
- 最后把需要追加的字符串追加到新字符數組中;
簡單來說,當StringBuilder的容量Capacity發生變化時,就會引起托管對象申請、內存復制等操作,帶來不好的性能影響,因此設置合適的初始容量是非常必要的,盡量減少內存申請和對象創建。代碼簡單來驗證一下:
StringBuilder sb1 = new StringBuilder(); Console.WriteLine("Capacity={0}; Length={1};", sb1.Capacity, sb1.Length); //輸出:Capacity=16; Length=0; //初始容量為16 sb1.Append('a', 12); //追加12個字符 Console.WriteLine("Capacity={0}; Length={1};", sb1.Capacity, sb1.Length); //輸出:Capacity=16; Length=12; sb1.Append('a', 20); //繼續追加20個字符,容量倍增了 Console.WriteLine("Capacity={0}; Length={1};", sb1.Capacity, sb1.Length); //輸出:Capacity=32; Length=32; sb1.Append('a', 41); //追加41個字符,新容量=32+41=73 Console.WriteLine("Capacity={0}; Length={1};", sb1.Capacity, sb1.Length); //輸出:Capacity=73; Length=73; StringBuilder sb2 = new StringBuilder(80); //設置一個合適的初始容量 Console.WriteLine("Capacity={0}; Length={1};", sb2.Capacity, sb2.Length); //輸出:Capacity=80; Length=0; sb2.Append('a', 12); Console.WriteLine("Capacity={0}; Length={1};", sb2.Capacity, sb2.Length); //輸出:Capacity=80; Length=12; sb2.Append('a', 20); Console.WriteLine("Capacity={0}; Length={1};", sb2.Capacity, sb2.Length); //輸出:Capacity=80; Length=32; sb2.Append('a', 41); Console.WriteLine("Capacity={0}; Length={1};", sb2.Capacity, sb2.Length); //輸出:Capacity=80; Length=73;
為什么少量字符串不推薦使用StringBuilder呢?因為StringBuilder本身是有一定的開銷的,少量字符串就不推薦使用了,使用String.Concat和String.Join更合適。
高效的使用字符串
- 在使用線程鎖的時候,不要鎖定一個字符串對象,因為字符串的駐留性,可能會引發不可以預料的問題;
- 理解字符串的不變性,盡量避免產生額外字符串,如:
if(str1.ToLower()==str2.ToLower()) //這種方式會產生新的字符串,不推薦 if(string. Compare(str1,str2,true)) //這種方式性能更好
- 在處理大量字符串連接的時候,盡量使用StringBuilder,在使用StringBuilder時,盡量設置一個合適的長度初始值;
- 少量字符串連接建議使用String.Concat和String.Join代替。
題目答案解析:
1.字符串是引用類型類型還是值類型?
引用類型。
2.在字符串連加處理中,最好采用什么方式,理由是什么?
少量字符串連接,使用String.Concat,大量字符串使用StringBuilder,因為StringBuilder的性能更好,如果string的話會創建大量字符串對象。
3.使用 StringBuilder時,需要注意些什么問題?
- 少量字符串時,盡量不要用,StringBuilder本身是有一定性能開銷的;
- 大量字符串連接使用StringBuilder時,應該設置一個合適的容量;
4.以下代碼執行后內存中會存在多少個字符串?分別是什么?輸出結果是什么?為什么呢?
string st1 = "123" + "abc"; string st2 = "123abc"; Console.WriteLine(st1 == st2); Console.WriteLine(System.Object.ReferenceEquals(st1, st2));
輸出結果:
True
True
內存中的字符串只有一個“123abc”,第一行代碼(string st1 = "123" + "abc"; )常量字符串相加會被編譯器優化。由於字符串駐留機制,兩個變量st1、st2都指向同一個對象。IL代碼如下:
5.以下代碼執行后內存中會存在多少個字符串?分別是什么?輸出結果是什么?為什么呢?
string s1 = "123"; string s2 = s1 + "abc"; string s3 = "123abc"; Console.WriteLine(s2 == s3); Console.WriteLine(System.Object.ReferenceEquals(s2, s3));
和第5題的結果肯定是不一樣的,答案留給讀者吧,文章太長了,寫的好累!
6.使用C#實現字符串反轉算法,例如:輸入"12345", 輸出"54321"
這是一道比較綜合的考察字符串操作的題目,答案可以有很多種。通過不同的答題可以看出程序猿的基礎水平。下面是網上比較認可的兩種答案,效率上都是比較不錯的。
public static string Reverse(string str) { if (string.IsNullOrEmpty(str)) { throw new ArgumentException("參數不合法"); } StringBuilder sb = new StringBuilder(str.Length); //注意:設置合適的初始長度,可以顯著提高效率(避免了多次內存申請) for (int index = str.Length - 1; index >= 0; index--) { sb.Append(str[index]); } return sb.ToString(); }
public static string Reverse(string str) { if (string.IsNullOrEmpty(str)) { throw new ArgumentException("參數不合法"); } char[] chars = str.ToCharArray(); int begin = 0; int end = chars.Length - 1; char tempChar; while (begin < end) { tempChar = chars[begin]; chars[begin] = chars[end]; chars[end] = tempChar; begin++; end--; } string strResult = new string(chars); return strResult; }
還有一個比較簡單也挺有效的方法:
public static string Reverse(string str) { char[] arr = str.ToCharArray(); Array.Reverse(arr); return new string(arr); }
7.下面的代碼輸出結果?為什么?
object a = "123"; object b = "123"; Console.WriteLine(System.Object.Equals(a,b)); Console.WriteLine(System.Object.ReferenceEquals(a,b)); string sa = "123"; Console.WriteLine(System.Object.Equals(a, sa)); Console.WriteLine(System.Object.ReferenceEquals(a, sa));
輸出結果全是True,因為他們都指向同一個字符串實例,使用object聲明和string聲明在這里並沒有區別(string是引用類型)。
使用object聲明和string聲明到底有沒有區別呢?,有點疑惑,一個朋友在面試時面試官有問過這個問題,那個面試官說sa、a是有區別的,且不相等。對於此疑問,歡迎交流。
版權所有,文章來源:http://www.cnblogs.com/anding
個人能力有限,本文內容僅供學習、探討,歡迎指正、交流。
.NET面試題解析(00)-開篇來談談面試 & 系列文章索引
參考資料:
書籍:CLR via C#
書籍:你必須知道的.NET
深入理解string和如何高效地使用string: http://www.cnblogs.com/artech/archive/2007/05/06/737130.html
C#基礎知識梳理系列九:StringBuilder:http://www.cnblogs.com/solan/archive/2012/08/06/CSharp09.html




