本章將介紹.net中處理字符和字符串的機制c++
在.NET Framewole中,字符老是表示成16位Unicode代碼值,這簡化了國際化應用程序的開發。git
每一個字符都表示成System.Char結構(一個值類型) 的一個實例。System.Char類型提供了兩個公共只讀常量字段:MinValue(定義成"\0")和MaxValue(定義成'\uffff')。正則表達式
爲char實例調用靜態GetUnicodeCategory方法,這個方法返回的是System.Globalization.UnicodeCategory枚舉類型的一個值。這個值支出該字符是控制字符、貨幣符號、小寫符號、大寫符號、標點符號、數學符號,仍是其餘字符(Unicode定義的字符)。算法
爲了簡化開發,Char類型還提供了幾個靜態方法,好比IsDigit,IsLotter,IsUpper,IsLower,isNumber等。其中大多數方法都在內部調用了GetUnicodeCategory,並簡單的返回true和false。注意,全部這些方法要麼獲取單個字符做爲參數,要麼獲取一個string以及目標字符在這個string中的索引做爲參數。編程
另外可調用ToLowerInvariant和ToUpperInvariant,以忽略語言文化(culture)的方式,將一個字符轉換成小寫和大寫形式。做爲另外一種替代方案,ToLower和ToUpper方法將字符轉換成小寫和大寫形式,可是轉換時要使用與調用線程關聯的靜態CurrenCulture屬性來得到。ToLower和ToUpper之因此須要語言文化信息,是由於字母大小寫的轉換是依賴於語言文化的。不一樣的語言文化,大小寫的形式也不盡相同(土耳其的大寫字母I上面會多一個點,是U+0130)。c#
除了這些靜態方法,char類型還有本身的實例方法。其中,equals方法在兩個char實例表明同一個16位Unicode碼位的前提下返回true。CompareTo方法返回兩個char實例的忽略語言文化的比較結果。數組
可使用三種技術實現各類數值類型與Char實例的相互轉換:網絡
1) 轉型(強制類型轉換)app
要將一個Char轉換成爲一個數值(好比Int32),最簡單的方法就是轉型。在三種技術中,效率也是最高的,由於編譯器會生成中間語言(IL)指令來執行轉型,沒必要調用任何方法。此外,有的語言(好比c#)容許指定轉換時使用checked仍是unchecked代碼。dom
2) 使用Convert類型
System.Convert類型提供了幾個靜態方法來實現Char和數值類型的相互轉型。因此這些方法都是以checked方式進行轉換,因此一旦發現轉型會形成數據丟失,就會拋出OverflowException異常。
3) 使用IConvertible接口
Char類型和FCL的全部數值類型都實現的ICOnvertible接口。該接口定義了像ToUInt16和ToChar這些的方法。這種技術效率最差,由於在值類型上調用一個接口方法,要求對實例進行裝箱—— Char和全部數值類型都是值類型。若是某個類型不能轉換,或者轉換形成數據的丟失,IConvertible的方法會拋出System.InvalidCastException異常。
下面演示這三種方法的調用:
public static void Main() { Char c; Int32 n; // 使用C#轉型技術實現,強制類型轉換 c = (Char)65; Console.WriteLine(c); // 顯示 "A" n = (Int32)c; Console.WriteLine(n); // 顯示 "65" c = unchecked((Char)(65536 + 65)); Console.WriteLine(c); // 顯示 "A" // 使用Convert進行轉換 c = Convert.ToChar(65); Console.WriteLine(c); // 顯示 "A" n = Convert.ToInt32(c); Console.WriteLine(n); // Displays "65" // 顯示Convert的範圍檢查 try { c = Convert.ToChar(70000); // 對 16-bits 來講過大 Console.WriteLine(c); // 不知心 } catch (OverflowException) { Console.WriteLine("Can't convert 70000 to a Char."); } // 使用IConvertible進行轉換 c = ((IConvertible)65).ToChar(null); Console.WriteLine(c); // 顯示 "A" n = ((IConvertible)c).ToInt32(null); Console.WriteLine(n); // 顯示 "65" }
在任何應用程序中,system.string都是用得最多的類型之一。一個string表明一個不可變(immutable)的順序字符集。string類型直接派生自object,因此是引用類型。所以,string對象老是存在於堆上,永遠不會跑到線程棧。
String類型還實現了幾個接口,IComparable、ICloneable等。
許多編程語言(包括C#)都將String視爲一個基元類型——也就是說,編譯器容許在源代碼中直接表示文本常量字符型。編譯器將這些文本常量字符串放到模塊的元數據中,並在運行時加載和引用它們。
在C#中,不能使用new操做符從字面值字符串構造一個String對象
public static void Main(string[] args) { String s =new String("Hi"); //錯誤 Console.WriteLine(s); }
相反,必須使用簡化的語法表示:
class Program { private static void Main(string[] args) { String s = "Hi"; Console.WriteLine(s); } }
編譯上述代碼,並檢查它的IL,會看到一下內容:
.method private hidebysig static void Main(string[] args) cil managed { .entrypoint .maxstack 1 .locals init ( [0] string str) L_0000: nop L_0001: ldstr "Hi" L_0006: stloc.0 L_0007: ldloc.0 L_0008: call void [mscorlib]System.Console::WriteLine(string) L_000d: nop L_000e: ret }
IL指令的newobj用於構造一個對象的新實例。然而,上述IL代碼並無出現newobj之類,只有一個特殊的ldstr(即load string)指令,它用從元數據得到的一個文本常量字符串構造一個String對象。這證實CLR事實是用一種特殊的方式構造文本常量String對象。
C#提供了一些特殊的語法來幫助開發人員在源代碼中輸入文本常量字符串。對於換行符、回車符和退格這樣的特殊字符,C#採用了C/C++的轉義機制:
//包含回車符和換行符的字符串 String s ="Hi\r\nthere"
可是,通常不建議這麼作。由於在不一樣的平臺解釋是不一樣的,推薦使用System.Environment中定義的NewLine屬性。NewLine屬性是依賴於平臺的,他會一句底層平臺返回恰當的字符串。
可使用c#的+操做符將幾個字符串鏈接成一個。
string s=」HI」+」 」+」there」;
在上述diamante中,因爲全部字符串都是字面值,因此c#編譯器能在編譯時鏈接它們,最終將一個字符串放到模塊的元數據中。對於非字面值字符串使用+操做符,鏈接則在運行時進行。運行時鏈接不要使用+操做符,由於這樣會在堆上建立多個字符串對象,而堆是須要垃圾回收的,從而影響性能。相反,應儘可能使用System.Text.StringBuilder類型。
最後,C#還提供了一種特殊的字符串聲明方式(@"xxx")。採用這種方式,引號之間的全部字符都會被視爲字符串的一部分。這種特殊聲明稱爲"逐字字符串",一般用於指定文件或目錄的路徑,或者配合正則表達式使用。
string對象最重要的一點就是不可變(immutable)。也就是說,字符串一經建立就不能更改,不能變長、變短或修改其中任何字符。使字符串不可變有下面幾點好處:
1.它容許在字符串上執行任何操做,而不實際的更改字符串。
2.在操做或訪問字符串時不會發生線程同步問題。
3.CLR可經過一個String對象共享多個徹底一致的String內容。這樣能減小系統中的字符串屬性,從而節省內存,這就是"字符串留用"技術的目的。
考慮到性能方面的緣由,String類型和CLR是緊密集成的。具體的說,CLR知道String類型中定義的字段是如何佈局的,會直接訪問這些字段。可是,爲了得到這種性能和直接訪問的好處,String只能是密封類。換言之,不能把它做爲本身類型的基類。若是容許string做爲基類來定義本身的類型,就能添加本身的字段,而這回破壞clr對於string類型的各類預設。
判斷字符串相等性或對字符串進行排序時,強烈建議調用下面列出的方法之一:
bool Equals (string value, StringComparison comparisonType) static bool Equals (string a, string b, StringComparison comparisonType) static int Compare (string strA, string strB, StringComparison comparisonType) static int Compare (String strA, String strB, bool ignoreCase, CultureInfo culture) static int Compare (string strA, string strB, CultureInfo culture, CompareOptions options) static int Compare (string strA, int indexA, string strB, int indexB, int length, StringComparison comparisonType) static int Compare (string strA, int indexA, string strB, int indexB, int length, CultureInfo culture, CompareOptions options) static int Compare (String strA, int indexA, String strB, int indexB, int length, bool ignoreCase, CultureInfo culture)
排序時應該老是執行區分大小寫的比較。緣由是假如只是大小寫不一樣的兩個字符串被視爲相等,那麼每次對它們進行排序許,它們均可能按照不一樣的順序排列,從而形成用戶的迷惑。
上述代碼中的comparisonType參數要求獲取由System.StringComparison枚舉類型定義的某個值。這個枚舉類型是這樣定義的:
public enum StringComparison { //使用區域敏感排序規則和當前區域比較字符串。 CurrentCulture, //使用區域敏感排序規則、當前區域來比較字符串,同時忽略被比較字符串的大小寫。 CurrentCultureIgnoreCase, //使用區域敏感排序規則和固定區域比較字符串。 InvariantCulture, //使用區域敏感排序規則、固定區域來比較字符串,同時忽略被比較字符串的大小寫。 InvariantCultureIgnoreCase, //使用序號排序規則比較字符串。 Ordinal, //使用序號排序規則並忽略被比較字符串的大小寫,對字符串進行比較。 OrdinalIgnoreCase }
另外,前面有兩個方法要求傳遞一個CompareOptions參數。這個參數要獲取有CompareOptions枚舉類型定義的一個值:
public enum CompareOptions { None = 0, //指示字符串比較必須忽略大小寫。 IgnoreCase = 1, //指示字符串比較必須忽略不佔空間的組合字符,好比音調符號。 IgnoreNonSpace = 2, //指示字符串比較必須忽略符號,如空白字符、標點符號、貨幣符號、百分號、數學符號、「&」符等等 IgnoreSymbols = 4, //指示字符串比較必須忽略 Kana 類型 IgnoreKanaType = 8, //指示字符串比較必須忽略字符寬度 IgnoreWidth = 16, //指示字符串比較必須使用字符串排序算法。 StringSort = 0x20000000, //指示必須使用字符串的連續 Unicode UTF-16 編碼值進行字符串比較(使用代碼單元進行代碼單元比較),這樣能夠提升比較速度,但不能區分區域性 Ordinal = 0x40000000, //字符串比較必須忽略大小寫,而後執行序號比較。 OrdinalIgnoreCase = 0x10000000 }
接受一個CompareOptions實參的方法要求你必須顯式傳遞一個語言文化。若是傳遞了Ordinal或OrdinalIgnoreCase 標誌,這些Comoare方法會忽略指定的語言文化。
許多程序都將字符串用於內部編程目的,好比路徑名、文件名、URL、註冊表項/值等等。這些字符串一般只在程序內部使用,不會向用戶顯示。出於編程目的而比較字符串時,應該老是使用StringComparison.Ordinal或者CompareOptions.OrdinalIgnoreCase。這是字符串比較時最快的一種方式,由於在執行比較時,不須要考慮語言文化信息。
另外一方面,若是想以一種語言文化正確的方式來比較字符串(一般顯示給用戶),應該使用StringComparison.CurrentCulture或者StringComparison.CurrentCultureIgnoreCase。
提示:StringComparison.InvariantCulture和StringComparison.InvariantCultureIgnoreCase平時最好不要用。雖然這兩個值能保證比較是語言文化的正確性,但用它們比較用於內部編程目的的字符串,花費的事件要比執行一次序號比較長的多。
提示:執行序號比較以前,若是(想更改字符串中的字符的大小寫,應該使用String的ToUpperInvariant和ToLowerInvariant方法。對字符串進行正規化時,強烈建議使用ToUpperInvariant方法,而不要使用ToLowerInvariant方法,應爲Microsoft對執行大寫比較的代碼進行了優化。事實上,執行不須要區分大小寫的比較以前,FCL會自動將字符串正規化爲大寫形式。之因此不用ToUpper和ToLower方法,是由於它們對語言文化敏感。
檢查字符串的相等性是許多應用程序的常見操做——這個任務可能驗證損害性能。執行序號(ordinal)相等性檢查時,CLR快速測試兩個字符串是否包含相同數量的字符。若是是否認,字符串確定不相等;若是確定,字符串可能相等。而後,CLR必須比較每一個單獨的字符才能肯定。值得注意的是,在執行須要注意語言文化的比較是,CLR始終都要比較全部單獨的字符,由於兩個字符串即便長度不一樣,也多是相等的。
除此以外,若是在內存中複製同一個字符串的多個實例,會形成內存的浪費,由於字符串是"不可變"的。若是隻在內存中保留字符串的一個實例,那麼將顯著提升內存的利用率。須要引用字符串的全部變量只需指向單獨一個字符串對象。
若是引用程序常常對字符串進行區分大小寫、序號式比較,或者事先知道許多字符串對象都有相同的值,就能夠利用CLR的"字符串留用"機制來顯著提升性能。CLR初始化時會建立一個內部哈希表。在這個表中,鍵(key)是字符串,而值(value)是對託管堆中String對象的引用。哈希表最開始是空的,String類提供了兩個方法,便於你訪問這個內部哈希表:
//檢索系統對指定 System.String 的引用 public static string Intern(string str) //檢索對指定 System.String 的引用 public static string IsInterned(string str)
第一個方法Intern獲取一個String,得到它的哈希碼,並在內部哈希表中檢查是否有匹配的。若是存在一個徹底相同的字符串,就返回對這個字符串已經存在的String對象的一個引用。若是不存在,就建立字符串的副本,將副本添加到內部哈希表中,並返回對這個副本的一個引用。若是應用程序再也不保持對string對象的引用,垃圾回收器就可釋放那個字符串的內存。注意垃圾回收器不能釋放內部哈希表引用的字符串,由於哈希表正在容納對它們的引用。除非卸載appdomain或進程終止,不然內部哈希表引用的string對象不能被釋放。
和Intern方法同樣,IsInterned方法也獲取一個String,並在內部哈希表中查找它。若是哈希表中有一個匹配的字符串,IIsInterned就返回對這個留用的字符串對象的一個引用。然而,若是哈希表中沒有一個相匹配的字符串,IsInterned會返回null;它不會將字符串添加到哈希表中。
程序集加載時,CLR默認會留用程序集的元數據中描述的全部文本常量(literal)字符串。Microsoft知道可能由於額外的哈希表查找會形成性能顯著降低,因此如今是能夠禁用這個"特性"的。即便指定了CLR不留用那個程序集中的字符串,可是CLR也可能選擇對字符串進行留用,但不該該依賴於CLR的這種"自主"行爲。事實上,除非本身顯式調用String的Intern方法,不然永遠都不要以"字符串已留用"爲前提來寫本身的代碼。如下代碼演示了字符串留用:
String s1 = "Hello"; String s2 = "Hello"; Console.WriteLine(Object.ReferenceEquals(s1, s2));// 'False' s1 = String.Intern(s1); s2 = String.Intern(s2); Console.WriteLine(Object.ReferenceEquals(s1, s2));// 'True'
在對ReferenceEquals方法的第一個調用中,s1s2中的"Hello"字符串對象的引用是不一樣的,因此應該顯示False。然而,若是在CLR的4.5版本上運行,會發現顯示True。由於這個版本的CLR選擇了忽略C#編譯器插入的特性和標誌。程序集集加載到appDomain中時,clr對字面量(literal)字符串進行留用,結果是s1和s2引用堆中的同一個hello字符串。如前所述,你的代碼永遠不要依賴這個行爲,由於將來版本clr有可能進行變動。
編譯源代碼時,編譯器必須處理每一個字面值字符串,並在託管模塊的元數據上嵌入。銅一個字符串在源代碼中屢次出現,把它們都嵌入元數據會使生成的文件無謂增大。
爲了解決這個問題,許多編譯器(包括C#編譯器)都只在模塊的元數據中將字面值字符串寫入一次。引用該字符串的全部代碼都會被修改,以引用元數據中的同一個字符串。編譯器這種將單個字符串的多個實例合併爲一個實例的作法,能夠顯著減小模塊大小。c/c++編譯器多年來一直採用這個技術。
雖然字符串比較對於排序和測試相等性頗有用,但有時只是想檢查一下字符串中的字符。string類型爲此提供了幾個屬性和方法,包括Length,chars,getEnumerator,Contains,IndexOf等。
還能夠利用string類型提供的一些方法來賦值整個字符串或者它的一部分,好比clone,copy,substring等。
除了這些方法,string還提供了多個用於處理字符串的靜態方法和實例方法,好比insert,remove,padleft,replace,split,join,toUpper,trim,concat,format等,使用全部這些方法時都請牢記一點,它們返回的都是新的字符串對象。
因爲String類型是一個不可變的字符串,因此FCL提供了另外一個名爲System.Text.StringBuilder的類型,可利用它高效率得對字符串和字符進行動態代理,最後基於處理結果建立一個String。可將StringBuilder想象成建立string對象的特殊構造器。你的方法通常應獲取string參數而非StringBuilder參數。
從邏輯上說,StringBuilder對象包含一個字段,該字段引用了有Char結構構成的一個數組。可利用StringBuilder的各個成員來操做這個字符數組,高效率的縮短或更改字符串中的字符。若是字符串變大,超過已分配的字符數組的大小,StringBuilder會自動分配一個新的、更大的數組,複製字符,並開始使用新數組。前一個數組會被垃圾回收。
用StringBuilder對象構造好字符串後,調用StringBuilder的tostring方法便可將StringBuilder的字符串數組轉換成String。這樣會在堆上新建String對象。
和string類不一樣,clr不以爲StringBuilder類有什麼特別。此外,大多數語言(包括c#)都不將StringBuilder類視爲基元類型。
使用StringBuilder的方法要注意,大多數方法返回的都是對同一個StringBuilder對象的引用,因此幾個操做能鏈接到一塊兒完成。
在.NET Framework中能夠調用ToString方法來和獲取任何對象的字符串表示。System.Object定義了一個public、virtual、無參的ToString方法,因此在任何類型的一個實例上都能調用該方法。在語義上,ToString返回表明對象當前值的一個字符串,並且這個字符串應該根據調用線程當前的語言文化進行格式化。
System.Object實現的ToString只是返回對象所屬類型的全名。這個值用處不大,但對許多不能提供有意義的字符串的類型來講,這也是一個合理的默認值。例如,一個FileStream或HashTable對象的字符串表示應該是什麼呢?
任何類型要想提供合理的方式獲取對象當前值的字符串表示,就應該重寫ToString方法。FCL內建的許多核心類型(byte,int32,Uint64,Double等)都重寫了ToString,能返回複合語言文化的字符串。
可使用string.Format方法,對於stringBuilder可使用AppendFormat。
能解析一個字符串的任何類型都提供了一個名爲Parse的public static方法。該方法獲取一個String對象,並返回類型的一個實例。從某種意義上說,Parse扮演了一個工廠的角色。在FCL中,全部數值類型,DateTime、TimeSpan以及一些其餘類型均提供了Parse方法。
win32開發人員常常要寫代碼將unicode字符和字符串轉換成「多字節字符集」(multi-Byte Character Set,MBCS)格式。我我的就常常寫這樣的代碼,這個過程很繁瑣,還容易出錯。在clr中,全部字符都表示成16位Uncode碼值,而全部字符串都由16位Unicode碼值構成,這簡化了運行時的字符和字符串處理。
但偶爾想要將字符串保存到文件中,或者經過網絡傳輸。若是字符串中的大多數字符都是英語用戶的,那麼保存或傳輸一系列16位值,效率就顯得不那麼理想,由於寫入的半數字節都只有零構成。相反,更有效的作法是將16位值編碼成壓縮的字節數組,之後再將字節數組解碼回16位值的數組。
用system.IO.BinaryWriter或者system.IO.StreamWriter類型將字符串發送給文件或網絡流時,一般要進行編碼。對應地,system.IO.BinaryReader或者system.IO.StreamReader類型從文件或網絡流中讀取字符串時,一般要進行解碼。不顯式指定一種編碼方案,全部這些類型都默認使用UTF-8。
FCL提供了一些類型來簡化字符編碼和解碼。兩種最經常使用的編碼方案是UTF-16和UTF-8。
UTF-16將每一個16位字符編碼成2個字節。不會對字符產生任何影響,也不發生壓縮---性能很是出色。UTF-16編碼也稱爲Unicode編碼。
UTF-8將部分字符編碼成1個字節,部分編碼成2個字節,部分編碼成3個字節或4個字節。值在0x0080之下的字符壓縮成1個字節,適合表示美國使用字符。0x00800~0x07FF的字符轉換成2個字節,適合歐洲和中東語言。0x0800以及之上的字符轉換成3個字節,適合東亞語言。最後代理項對錶示成4個字節。0x0080編碼方法很是流行,但若是要編碼的許多字符都具備0x0800或者之上的值,效率不如UTF-16
ASCII編碼方案將16位字符編碼從ASCII字符:也就是說,值小於0x0080的16位字符被轉換成單字節。值超過0x007F的任何字符都不能被轉換,不然字符的值會丟失。
假定如今要經過system.net.Sockets.NetworkStream對象來讀取一個UTF-16編碼字符串。字節流一般以數據塊(data chunk)的形式傳輸。換言之,可能先從流中讀取5個字節,在讀取7個字節。UTF-16的每一個字符都由兩個字節構成。因此,調用Encoding的GetString方法並傳遞第一個5字節數組,返回的字符串只包含2個字符。再次調用GetString並傳遞接着的7個字節,將返回只包含3個字節的字符串。顯然,這樣會存儲錯誤的值。
之因此會形成數據損壞,是因爲全部Encoding派生都不維護多個方法調用之間的狀態。要編碼或解碼以數據塊形式傳輸的字符/字節,必須進行一些額外的工做來維護方法調用之間的狀態,從而防止丟失數據。
字節塊解碼首先要獲取一個Encoding派生對象引用,在調用其GetDecoder方法。Decoder的全部派生類都提供了兩個重要方法:getChars和getCharCount。顯然,這些方法的做用是對字節數組進行編碼,調用其中一個方法時,它會盡量多地解碼字節數組。假如字節數組包含的字節不足以完成一個字符,剩餘的字節會保存到Decoder對象內部。下次調用其中一個方法時,Decoder對象會利用以前剩餘的字節再加上傳給它的新字節數組。這樣一來,就能夠確保對數據塊進行正確編碼。從流中讀取字節時Decoder對象的做用很大。
從Encoding派生的類型可用於無狀態編碼和解碼。而從decoder派生的類型只能用於解碼。
編碼轉碼實例
string str = "在下阪本,有何貴幹@@"; Char[] chars = str.ToCharArray(); Debug.Log("String="+new string(chars)); //得到Encoder實例; Encoder encoder = Encoding.UTF8.GetEncoder(); //計算字符序列化須要的字節數組長度; byte[] bytes = new byte[encoder.GetByteCount(chars,0,chars.Length,true)]; //經過GetBytes轉爲字節序列; encoder.GetBytes(chars, 0, chars.Length, bytes, 0, true); Debug.Log(BitConverter.ToString(bytes)); Debug.Log("Encoding.UTF8.GetString=" + Encoding.UTF8.GetString(bytes)); //得到Decoder實例; Decoder decoder = Encoding.UTF8.GetDecoder(); //計算字節數組對應的字符數組長度; int charSize = decoder.GetCharCount(bytes, 0, bytes.Length); Char[] chs = new char[charSize]; //進行字符轉換; int charLength = decoder.GetChars(bytes, 0, bytes.Length, chs, 0); Debug.Log("Decoder Bytes to String =" + new string(chs));