深入理解string和如何高效地使用string

作者: Artech  來源: 博客園  發布時間: 2010-09-25 10:11  閱讀: 1002 次  推薦: 0   原文鏈接   [收藏]  
摘要:無論你所使用的是哪種編程語言,我們都不得不承認這樣一個共識:string是我們使用最為頻繁的一種對象。但是string的常用性并不意味著它的簡單性,而且我認為,正是由于string的頻繁使用才會促使其設計人員在string的設計上花大量的功夫。所以正是這種你天天見面的string,蘊含了很多精妙的設計思想。

  一個月以前我寫了一篇討論字符串的駐留(string interning)的文章,我今天將會以字符串的駐留為基礎,進一步來討論.NET中的string。string interning的基本前提是string的恒定性(immutability),即string一旦被創建將不會改變。我們就先來談談string的恒定性。

  一、string是恒定的(immutable)

  和其他類型比較,string最為顯著的一個特點就是它具有恒定不變性:我們一旦創建了一個string,在managed heap 上為他分配了一塊連續的內存空間,我們將不能以任何方式對這個string進行修改使之變長、變短、改變格式。所有對這個string進行各項操作(比如調用ToUpper獲得大寫格式的string)而返回的string,實際上另一個重新創建的string,其本身并不會產生任何變化。

  String的恒定性具有很多的好處,它首先保證了對于一個既定string的任意操作不會造成對其的改變,同時還意味著我們不用考慮操作string時候出現的線程同步的問題。在string恒定的這些好處之中,我覺得最大的好處是:它成就了字符串的駐留。

  CLR通過一個內部的interning table保證了CLR只維護具有不同字符序列的string,任何具有相同字符序列的string所引用的均為同一個string對象,同一段為該string配分的內存快。字符串的駐留極大地較低了程序執行對內存的占用。

  對于string的恒定性和字符串的駐留,還有一點需要特別指出的是:string的恒定性不單單是針對某一個單獨的AppDomain,而是針對一個進程的。

  二、String可以跨AppDomain共享的(cross-appDomain)

  我們知道,在一個托管的環境下,Appdomain是托管程序運行的一個基本單元。AppDomain為托管程序提供了良好的隔離機制,保證在同一個進程中的不同的Appdomain不可以共享相同的內存空間。在一個Appdomain創建的對象不能被另一個Appdomain直接使用,對象在AppDomain之間傳遞需要有一個Marshaling的過程:對象需要通過by reference或者by value的方式從一個Appdomain傳遞到另一個Appdomain。具體內容可以參照我的另一篇文章:用Coding證明Appdomain的隔離性

  但是這里有一個特例,那就是string。Appdomain的隔離機制是為了防止一個Application的對內存空間的操作對另一個Application 內存空間的破壞。通過前面的介紹,我們已經知道了string是恒定不變的、是只讀的。所以它根本不需要Appdomain的隔離機制。所以讓一個恒定的、只讀的string被同處于一個進程的各個Application共享是沒有任何問題的。

String的這種跨AppDomain的恒定性成就了基于進程的字符串駐留:一個進程中各個Application使用的具有相同字符序列的string都是對同一段內存的引用。我們將在下面通過一個Sample來證明這一點。

  三、證明string垮AppDomain的恒定性

  在寫這篇文章的時候,我對如何證明string跨AppDomain的interning,想了好幾天,直到我偶然地想到了為實現線程同步的lock機制。

  我們知道在一個多線程的環境下,為了避免并發操作導致的數據的不一致性,我們需要對一個對象加鎖來阻止該對象被另一個線程 操作。相反地,為了證明兩個對象是否引用的同一個對象,我們只需要在兩個線程中分別對他們加鎖,如果程序執行的效果和對同一個對象加鎖的情況完全一樣的話,那么就可以證明這兩個被加鎖的對象是同一個對象。基于這樣的原理我們來看看我們的Sample:

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;

namespace Artech.ImmutableString
{
    
class Program
    
{
        
static void Main(string[] args)
        
{
            AppDomain appDomain1 
= AppDomain.CreateDomain("Artech.AppDomain1");
            AppDomain appDomain2 
= AppDomain.CreateDomain("Artech.AppDomain2");

            MarshalByRefType marshalByRefObj1 
= appDomain1.CreateInstanceAndUnwrap(
"Artech.ImmutableString""Artech.ImmutableString.MarshalByRefType"as MarshalByRefType;
            MarshalByRefType marshalByRefObj2 
= appDomain2.CreateInstanceAndUnwrap(
"Artech.ImmutableString""Artech.ImmutableString.MarshalByRefType"as MarshalByRefType;

            marshalByRefObj1.StringLockHelper 
= "Hello World";
            marshalByRefObj2.StringLockHelper 
= "Hello World";

            Thread thread1 
= new Thread(new ParameterizedThreadStart(Execute));
            Thread thread2 
= new Thread(new ParameterizedThreadStart(Execute));

            thread1.Start(marshalByRefObj1);
            thread2.Start(marshalByRefObj2);

            Console.Read();            
        }


        
static void Execute(object obj)
        

            MarshalByRefType marshalByRefObj 
= obj as MarshalByRefType;
            marshalByRefObj.ExecuteWithStringLocked();
        }

    }


    
class MarshalByRefType : MarshalByRefObject
    
{
        
Private Fields

        
Public Properties

        
Public Methods
    }

}

  我們來簡單地分析一下上面的coding.

  我們創建了一個繼承自MarshalByRefObject,因為我需要讓它具有跨AppDomain傳遞的能力。在這個Class中定義了兩個為實現線程同步的helper字段,一個是string類型的_stringLockHelper和object類型的_objectLockHelper,并為他們定義了相應的Property。此外定義了兩個方法:ExecuteWithStringLocked和ExecuteWithStringLocked,他們的操作類似:在先對_stringLockHelper和_objectLockHelper加鎖的前提下,輸出出操作執行的AppDomain和確切時間。我們通過調用Thread.Sleep模擬10s的時間延遲。

  在Main方法中,首先創建了兩個AppDomain,名稱分別為Artech.AppDomain1和Artech.AppDomain2。隨后在這兩個AppDomain中創建兩個MarshalByRefType對象,并為它們的StringLockHelper屬性賦上相同的值:Hello World。最后,我們創建了兩個新的線程,并在它們中分別調用在兩個不同AppDomain 中創建的MarshalByRefType對象的ExecuteWithStringLocked方法。我們來看看運行后的輸出結果:

  從上面的輸出結果中可以看出,兩個分別在不同線程中執行操作對應的AppDomain的name分別為Artech.AppDomain1和Artech.AppDomain2。執行的時間(確切地說是操作成功地對MarshalByRefType對象的_stringLockHelper字段進行加鎖的時間)相隔10s,也就是我們在程序中定義的時間延遲。

  為什么會出現這樣的結果呢?我們只是對兩個處于不同AppDomain的不同的MarshalByRefType對象的stringLockHelper字段進行加鎖。由于我們是同時開始他們對應的線程,照理說它們之間不會有什么關聯,顯示出來的時間應該是相同的。唯一的解釋就是:雖然這兩個在不同的AppDomain中創建的對象是兩個完全不同的對象,由于他們的stringLockHelper字段具有相同的字符序列,它們引用的是同一個string。這就證明了我們提出的跨AppDomain進行string interning的結論。

  為了進一步印證我們的結論,我們是使兩個MarshalByRefObject對象的stringLockHelper字段具有不同的值,看看結果又如何。于是我們把其中一個對象的stringLockHelper字段改為”Hello World!”(多加了一個!) 。

marshalByRefObj1.StringLockHelper = "Hello World";
marshalByRefObj2.StringLockHelper 
= "Hello World!";

  看看現在的輸出結果,現在的時間是一樣了。

  上面我們做的是對string類型字段加鎖的試驗。那么我們對其他類型的對象進行加鎖,又會出現怎么的情況呢?我們現在就來做這樣試驗:在各自的線程中調用兩個對象的ExecuteWithObjectLocked方法。我們修改Execute方法和Main()。

static void Execute(object obj)
        

            MarshalByRefType marshalByRefObj 
= obj as MarshalByRefType;
            marshalByRefObj. ExecuteWithObjectLocked ();
}
static void Main(string[] args)
        
{
            AppDomain appDomain1 
= AppDomain.CreateDomain("Artech.AppDomain1");
            AppDomain appDomain2 
= AppDomain.CreateDomain("Artech.AppDomain2");

            MarshalByRefType marshalByRefObj1 
= appDomain1.CreateInstanceAndUnwrap(
"Artech.ImmutableString""Artech.ImmutableString.MarshalByRefType"as MarshalByRefType;
            MarshalByRefType marshalByRefObj2 
= appDomain2.CreateInstanceAndUnwrap(
"Artech.ImmutableString""Artech.ImmutableString.MarshalByRefType"as MarshalByRefType;

            
object obj = new object();
            marshalByRefObj1.ObjectLockHelper 
= obj;
            marshalByRefObj2.ObjectLockHelper 
= obj;

            Thread thread1 
= new Thread(new ParameterizedThreadStart(Execute));
            Thread thread2 
= new Thread(new ParameterizedThreadStart(Execute));

            thread1.Start(marshalByRefObj1);
            thread2.Start(marshalByRefObj2);

            Console.Read();            
        }

  我們先來看看運行后的輸出結果:

  我們發現兩個時間是一樣的,那么就是說兩個對象的ObjectLockHelper引用的不是同一個對象。雖然上面的程序很簡單,我覺得里面涉及的規程卻很值得一說。我們來分析下面3段代碼。

object obj = new object();
marshalByRefObj1.ObjectLockHelper 
= obj;
marshalByRefObj2.ObjectLockHelper 
= obj;

  簡單看起來,兩個MarshalByRefObject對象的ObjectLockHelper都是引用的同一個對象obj。但是背后的情況沒有那么簡單。代碼第一行創建了一個新的對象obj,這個對象是在當前AppDomain 中創建的。二對于當前的AppDomain來說,marshalByRefObj1和marshalByRefObj2僅僅是一個Transparent proxy而已,它們包含一個在Artech.AppDomain1和Artech.AppDomain2中創立的MarshalByRefObject對象的引用。我們為它的ObjectLockHelper復制,對于Transparent proxy對象的賦值調用會傳到真正對象所在的AppDomain,由于obj是當前AppDomain的對象,它不能直接賦給另一個AppDomain的對象。所以它必須經歷一個Marshaling的過程才能被傳遞到另外一個AppDomain。實際上當復制操作完成之后,真正的ObjectLockHelper屬性對應的對象是根據原數據重建的對象,和在當前AppDomain中的對象已經沒有任何的關系。所以兩個MarshalByRefObject對象的ObjectLockHelper屬性引用的并不是同一個對象,所以對它進行加鎖對彼此不要產生任何影響。

  四、從Garbage Collection的角度來看string

  我們知道在一個托管的環境下,一個對象的生命周期被GC管理和控制。一個對象只有在他不被引用的時候,GC才會對他進行垃圾回收。而對于一個string來說,它始終被interning table引用,而這個interning table是針對一個Process的,是被該Process所有AppDomain共享的,所以一個string的生命周期相對比較長,只有所有的AppDomain都不具有對該string的引用時,他才有可能被垃圾回收。

  五、從多線程的角度來看string

  一方面由于string的恒定性,我們不用考慮多線程的并發操作產生的線程同步問題。另一方面由于字符串的駐留,我們在對一個string對象進行加鎖操作的時候,極有可能拖慢這個Application的performance,就像我們的Sample中演示的那樣。而且很有可能影響到處于同一進程的其他Application,以致造成死鎖。所以我們在使用鎖的時候,除非萬不得已,切忌對一個string進行加鎖。

  六、如何高效地使用string

  下面簡單介紹一些高效地使用string的一些小的建議:

  1. 盡量使用字符串(literal string)相加來代替字符串變量和字符創相加,因為這樣可以使用現有的string操作指令進行操作和利用字符串駐留。

  比如:

string s = "abc" + "def";

  優于:

string s = "abc";
= s + "def";

  2. 在需要的時候使用StringBuilder對string作頻繁的操作:

  由于string的恒定性,在我們對一個string進行某些操作的時候,比如調用ToUpper()或者ToLower()把某個string每個字符轉化成大寫或者小寫;調用SubString()取子串;會創建一個新的string,有時候會創建一些新的臨時string。這樣的操作會增加內存的壓力。所有在對string作頻繁操作的情況下,我們會考慮使用StringBuilder來高效地操作string。StringBuilder之所以能對string操作帶來更好的performance,是因為在它的內部維護一個字符數組,而不是一個string來避免string操作帶來的新的string的創建。

  StringBuilder是一個很好的字符累加器,我們應該充分地利用這一個功能:

StringBuilder sb = new StringBuilder();
sb.Append(str1 
+ str2);

  最好寫成:

StringBuilder sb = new StringBuilder();
sb.Append(str1);
sb.Append(str2);

  避免創建一個新的臨時string來保存str1 + str2。再比如下面的Code:

StringBuilder sb = new StringBuilder();
sb.Append(WorkOnString1());
sb.Append(WorkOnString2());
sb.Append(WorkOnString3());

  最好寫好吧WorkOnString1,WorkOnString2,WorkOnString3定義成:

WorkOnString1(StringBuilder sb)
WorkOnString2(StringBuilder sb)
WorkOnString3(StringBuilder sb)

  3. 高效地進行string的比較操作

  我們知道,對象之間的比較有比較Value和比較Reference之說。一般地對Reference進行比較的速度最快。對于string,在字符串駐留的前提下,我們可以把對Value的比較用Reference的比較來代替從而會的Performance的提升。

  此外,對于忽略大小寫的比較,我們最好使用string的static方法Compare(string strA, string strB, bool ignoreCase)。也就是說:

if(str1.ToLower()==str2.ToLower())

  最好寫成:

If(string. Compare(str1,str2,true))
0
0
 
標簽:string
 
 

文章列表

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()