天天看點

.NET面試題解析(03)-string與字元串操作

轉自:  

常見面試題目:

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.下面的代碼輸出結果?為什麼?

.NET面試題解析(03)-string與字元串操作
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));      
.NET面試題解析(03)-string與字元串操作

  深入淺出字元串操作

string是一個特殊的引用類型,使用上有點像值類型。之是以特殊,也主要是因為string太常用了,為了提高性能及開發友善,對string做了特殊處理,給予了一些專用特性。為了彌補string在字元串連接配接操作上的一些性能不足,便有了StringBuilder。

.NET面試題解析(03)-string與字元串操作
 認識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類型的兩個重要的特性:恒定性與駐留性

.NET面試題解析(03)-string與字元串操作
 String的恒定性(不變性)

字元串是不可變的,字元串一經建立,就不會改變,任何改變都會産生新的字元串。比如下面的代碼,堆上先建立了字元串s1=”a”,加上一個字元串“b”後,堆上會存在三個個字元串執行個體,如下圖所示。

string s1 = "a";
string s2 = s1 + "b";      

.NET面試題解析(03)-string與字元串操作

上文中的”任何改變都會産生新的字元串“,包括字元串的一些操作函數,如str1.ToLower,Trim(),Remove(int startIndex, int count),ToUpper()等,都會産生新的字元串,是以在很多程式設計實踐中,對于字元串忽略大小的比較:

if(str1.ToLower()==str2.ToLower()) //這種方式會産生新的字元串,不推薦
if(string. Compare(str1,str2,true)) //這種方式性能更好      

.NET面試題解析(03)-string與字元串操作
 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代碼如下

.NET面試題解析(03)-string與字元串操作

在上面的代碼中,出現兩個字元串常量,“123”和“abc”,這個兩個常量字元串在IL代碼中都是通過IL指令

ldstr

建立的,隻有該指令建立的字元串才會被駐留,其他方式産生新的字元串都不會被駐留,也就不會共享字元串了,會被GC正常回收。

那該如何來驗證字元串是否駐留呢,string類提供兩個靜态方法:

  • String.Intern(string str) 可以主動駐留一個字元串;
  • String.IsInterned(string str);檢測指定字元串是否駐留,如果駐留則傳回字元串,否則傳回NULL
.NET面試題解析(03)-string與字元串操作

請看下面的示例代碼

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      

.NET面試題解析(03)-string與字元串操作
 認識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更合适。

.NET面試題解析(03)-string與字元串操作
 高效的使用字元串

  • 在使用線程鎖的時候,不要鎖定一個字元串對象,因為字元串的駐留性,可能會引發不可以預料的問題;
  • 了解字元串的不變性,盡量避免産生額外字元串,如:
if(str1.ToLower()==str2.ToLower()) //這種方式會産生新的字元串,不推薦
if(string. Compare(str1,str2,true)) //這種方式性能更好      
  • 在處理大量字元串連接配接的時候,盡量使用StringBuilder,在使用StringBuilder時,盡量設定一個合适的長度初始值;
  • 少量字元串連接配接建議使用String.Concat和String.Join代替。

  題目答案解析:

引用類型。

2.在字元串連加進行中,最好采用什麼方式,理由是什麼?

少量字元串連接配接,使用String.Concat,大量字元串使用StringBuilder,因為StringBuilder的性能更好,如果string的話會建立大量字元串對象。

  • 少量字元串時,盡量不要用,StringBuilder本身是有一定性能開銷的;
  • 大量字元串連接配接使用StringBuilder時,應該設定一個合适的容量;

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代碼如下:

.NET面試題解析(03)-string與字元串操作

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);
}      

.NET面試題解析(03)-string與字元串操作
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));      
.NET面試題解析(03)-string與字元串操作

輸出結果全是True,因為他們都指向同一個字元串執行個體,使用object聲明和string聲明在這裡并沒有差別(string是引用類型)。

使用object聲明和string聲明到底有沒有差別呢?,有點疑惑,一個朋友在面試時面試官有問過這個問題,那個面試官說sa、a是有差別的,且不相等。對于此疑問,歡迎交流。