CLR via C#, 4th -- 【基本类型】 -- 第14章字符、字符串和文本处理
14.1 字符
在NET Framework中,字符总是表示成16位Unicode代码值,这简化了国际化应用程序的开发。每个字符都是System.Char结构(一个值类型)的实例。System.Char类型很简单,提供了两个公共只读常量字段:MinValue(定义成'\0')和MaxValue(定义成'\ufff')
可以使用三种技术实现各种数值类型与Char实例的相互转换。
- 转型(强制类型转换)
将Char转换成数值(比如Int32)最简单的办法就是转型。这是三种技术中效率最高的,因为编译器会生成中间语言(IL)指令来执行转换,而且不必调用方法。 - 使用Convert类型
System.Convert类型提供了几个静态方法来实现Char和数值类型的相互转换。所有这些转换都以checked方式执行,发现转换将造成数据丢失就抛出OverfowException异常。 - 使用IConvertible接口
Char类型和FCL中的所有数值类型都实现了IConvertible接口。该接口定义了像ToUInt16和ToChar这样的方法。这种技术效率最差,因为在值类型上调用接口方法要求对实例进行装箱——Char和所有数值类型都是值类型。如果某个类型不能转换(比如Char转换成Boolean),或者转换将造成数据丢失,IConvertible的方法会抛出System.InvalidCastException异常。
using System; public static class Program { public static void Main() { Char c; Int32 n; // Convert number <> character using C# casting c = (Char) 65; Console.WriteLine(c); // Displays "A" n = (Int32) c; Console.WriteLine(n); // Displays "65" c = unchecked((Char) (65536 + 65)); Console.WriteLine(c); // Displays "A" // Convert number <> character using Convert c = Convert.ToChar(65); Console.WriteLine(c); // Displays "A" n = Convert.ToInt32(c); Console.WriteLine(n); // Displays "65" // This demonstrates Convert's range checking try { c = Convert.ToChar(70000); // Too big for 16 bits Console.WriteLine(c); // Doesn't execute } catch (OverflowException) { Console.WriteLine("Can't convert 70000 to a Char."); } // Convert number <> character using IConvertible c = ((IConvertible) 65).ToChar(null); Console.WriteLine(c); // Displays "A" n = ((IConvertible) c).ToInt32(null); Console.WriteLine(n); // Displays "65" } }
14.2 System.String类型
14.2.1 构造字符串
C#不允许使用new操作符从字面值字符串构造String对象
using System; public static class Program { public static void Main() { String s = new String("Hi there."); // < Error Console.WriteLine(s); } }
相反,必须使用以下简化语法:
using System; public static class Program { public static void Main() { String s = "Hi there."; Console.WriteLine(s); } }
System.Environment.NewLine
System.Environment类型定义了只读NewLine属性。该属性返回由回车符和换行符构成的字符串.NewLine属性对平台敏感,会根据底层平台来返回恰当的字符串。
String s = "Hi" + Environment.NewLine + "there.";
对非字面值字符串使用+操作符,连接则在运行时进行。运行时连接不要使用+操作符,因为这样会在堆上创建多个字符串对象,而堆是需要垃圾回收的,对性能有影响。相反,应该使用System.Text.StringBuilder类型
// Three literal strings concatenated to form a single literal string String s = "Hi" + " " + "there.";
C#提供了一种特殊的字符串声明方式。采取这种方式,引号之间的所有字符会都被视为字符串的一部分。这种特殊声明(@)称为“逐字字符串”(verbatim string),通常用于指定文件或目录的路径,或者与正则表达式配合使用。
// Specifying the pathname of an application String file = "C:\\Windows\\System32\\Notepad.exe"; // Specifying the pathname of an application by using a verbatim string String file = @"C:\Windows\System32\Notepad.exe";
在字符串之前添加@符号使编译器知道这是逐字字符串。编译器会将反斜杠字符视为字面值(iteral)而非转义符,使文件路径在源代码中更易读。
14.2.2字符串是不可变的
String对象最重要的一点就是不可变(immutable),也就是说,字符串一经创建便不能更改,不能变长、变短或修改其中的任何字符。使字符串不可变有几方面的好处。首先,它允许在一个字符串上执行各种操作,而不实际地更改字符串。
字符串不可变还意味着在操纵或访问字符串时不会发生线程同步问题。
CLR知道String类型中定义的字段如何布局,会直接访问这些字段。但为了获得这种性能和直接访问的好处,String只能是密封类。
14.2.3 比较字符串
“比较”或许是最常见的字符串操作。一般因为两个原因要比较字符串:判断相等性或者排序(通常是为了显示给用户看)。
Boolean Equals(String value, StringComparison comparisonType) static Boolean Equals(String a, String b, StringComparison comparisonType) static Int32 Compare(String strA, String strB, StringComparison comparisonType) static Int32 Compare(string strA, string strB, Boolean ignoreCase, CultureInfo culture) static Int32 Compare(String strA, String strB, CultureInfo culture, CompareOptions options) static Int32 Compare(String strA, Int32 indexA, String strB, Int32 indexB, Int32 length, StringComparison comparisonType) static Int32 Compare(String strA, Int32 indexA, String strB, Int32 indexB, Int32 length, CultureInfo culture, CompareOptions options) static Int32 Compare(String strA, Int32 indexA, String strB, Int32 indexB, Int32 length, Boolean ignoreCase, CultureInfo culture) Boolean StartsWith(String value, StringComparison comparisonType) Boolean StartsWith(String value, Boolean ignoreCase, CultureInfo culture) Boolean EndsWith(String value, StringComparison comparisonType) Boolean EndsWith(String value, Boolean ignoreCase, CultureInfo culture)
comparisonType参数(上述大多数方法都有)要求获取由StringComparison枚举类型定义的某个值。该枚举类型的定义如下所示:
public enum StringComparison { CurrentCulture = 0, CurrentCultureIgnoreCase = 1, InvariantCulture = 2, InvariantCultureIgnoreCase = 3, Ordinal = 4, OrdinalIgnoreCase = 5 }
另外,前面有两个方法要求传递一个options参数,它是CompareOptions枚举类型定义的值之一:
[Flags] public enum CompareOptions { None = 0, IgnoreCase = 1, IgnoreNonSpace = 2, IgnoreSymbols = 4, IgnoreKanaType = 8, IgnoreWidth = 0x00000010, Ordinal = 0x40000000, OrdinalIgnoreCase = 0x10000000, StringSort = 0x20000000 }
接受CompareOptions实参的方法要求显式传递语言文化。传递Ordinal或OrdinallgnoreCase标志,这些Compare方法会忽略指定的语言文化。
出于编程目的而比较字符串时,应该总是使用StringComparison.Ordinal或者StringComparison.OrdinallgnoreCase。忽略语言文化是字符串比较最快的方式。
另一方面,要以语言文化正确的方式来比较字符串(通常为了向用户显示),就应该使用StringComparison.CurrentCulture或者StringComparison.CurrentCulturelgnoreCase.
- StringComparison.InvariantCulture和String Comparison.InvariantCulturelgnoreCase平时最好不要用。虽然这两个值能保证比较时的语言文化正确性,但用来比较内部编程所需的字符串,所花的时间远超出序号(StringComparison.Ordinal)比较.
- 要在序号比较前更改字符串中的字符的大小写,应该使用String的 ToUpperlnvariant或 ToLowerlnvariant方法。强烈建议用ToUpperInvariant方法对字符串进行正规化(normalizing),而不要用ToLowerInvariant,因为Microsoft对执行大写比较的代码进行了优化。
- 除了前面列出的之外,String类型还为Equals,StartsWith,EndsWith和Compare方法定义了其他几个重载版本。但是,Microsoft建议避免使用这些额外的版本(也就是本书没有列出的版本),除此之外,String的其他比较方法-CompareTo(IComparable接口所要求的).CompareOrdinal以及==和!=操作符——也应避免使用。之所以要避免使用这些方法和操作符,是因为调用者不显式指出以什么方式执行字符串比较,而你无法从方法名看出默认比较方式.例如,CompareTo默认执行对语言文化敏感的比较,而Equals执行普通的序号(ordinal)比较。如果总是显式地指出以什么方式执行字符串比较,代码将更容易阅读和维护。
System.Globalization.Culturelnfo
- CurrentUICulture属性 该属性获取要向用户显示的资源。
- CurrentCulture属性 不适合使用CurrentUICulture属性的场合就使用该属性
using System; using System.Globalization; public static class Program { public static void Main() { String s1 = "Strasse"; String s2 = "Straße"; Boolean eq; // CompareOrdinal returns nonzero. eq = String.Compare(s1, s2, StringComparison.Ordinal) == 0; Console.WriteLine("Ordinal comparison: '{0}' {2} '{1}'", s1, s2, eq ? "==" : "!="); // Compare Strings appropriately for people // who speak German (de) in Germany (DE) CultureInfo ci = new CultureInfo("deDE"); // Compare returns zero. eq = String.Compare(s1, s2, true, ci) == 0; Console.WriteLine("Cultural comparison: '{0}' {2} '{1}'", s1, s2, eq ? "==" : "!="); } }
调用String的Compare方法时,如果调用者指定了语言文化,就使用指定的语言文化:如果没有指定,就使用调用线程的CurrentCulture属性值。
using System; using System.Text; using System.Windows.Forms; using System.Globalization; using System.Threading; public sealed class Program { public static void Main() { String output = String.Empty; String[] symbol = new String[] { "<", "=", ">" }; Int32 x; CultureInfo ci; // The code below demonstrates how strings compare // differently for different cultures. String s1 = "coté"; String s2 = "côte"; // Sorting strings for French in France. ci = new CultureInfo("frFR"); x = Math.Sign(ci.CompareInfo.Compare(s1, s2)); output += String.Format("{0} Compare: {1} {3} {2}", ci.Name, s1, s2, symbol[x + 1]); output += Environment.NewLine; // Sorting strings for Japanese in Japan. ci = new CultureInfo("jaJP"); x = Math.Sign(ci.CompareInfo.Compare(s1, s2)); output += String.Format("{0} Compare: {1} {3} {2}", ci.Name, s1, s2, symbol[x + 1]); output += Environment.NewLine; // Sorting strings for the thread's culture ci = Thread.CurrentThread.CurrentCulture; x = Math.Sign(ci.CompareInfo.Compare(s1, s2)); output += String.Format("{0} Compare: {1} {3} {2}", ci.Name, s1, s2, symbol[x + 1]); output += Environment.NewLine + Environment.NewLine; // The code below demonstrates how to use CompareInfo.Compare's // advanced options with 2 Japanese strings. One string represents // the word "shinkansen" (the name for the Japanese highspeed // train) in hiragana (one subtype of Japanese writing), and the // other represents the same word in katakana (another subtype of // Japanese writing). s1 = ""; // ("\u3057\u3093\u304B\u3093\u305b\u3093") s2 = ""; // ("\u30b7\u30f3\u30ab\u30f3\u30bb\u30f3") // Here is the result of a default comparison ci = new CultureInfo("jaJP"); x = Math.Sign(String.Compare(s1, s2, true, ci)); output += String.Format("Simple {0} Compare: {1} {3} {2}", ci.Name, s1, s2, symbol[x + 1]); output += Environment.NewLine; // Here is the result of a comparison that ignores // kana type (a type of Japanese writing) CompareInfo compareInfo = CompareInfo.GetCompareInfo("jaJP"); x = Math.Sign(compareInfo.Compare(s1, s2, CompareOptions.IgnoreKanaType)); output += String.Format("Advanced {0} Compare: {1} {3} {2}", ci.Name, s1, s2, symbol[x + 1]); MessageBox.Show(output, "Comparing Strings For Sorting"); } }
14.2.4 字符串留用
执行序号(ordinal)相等性检查时,CLR快速测试两个字符串是否包含相同数量的字符。答案否定,字符串肯定不相等;答案肯定,字符串则可能相等。每个单独的字符才能最终确认。而执行对语言文化敏感的比较时,CLR必须比较所有单独的字符,因为两个字符串即使长度不同也可能相等。
如果应用程序经常对字符串进行区分大小写的序号比较,或者事先知道许多字符串对象都有相同的值,就可利用CLR的字符串留用(string interning)机制来显著提升性能。CLR初始化时会创建一个内部哈希表。在这个表中,键(key)是字符串,而值(value)是对托管堆中的String对象的引用。哈希表最开始是空的(理应如此)。
public static String Intern(String str); public static String IsInterned(String str);
第一个方法Intern获取一个String,获得它的哈希码,并在内部哈希表中检查是否有相匹配的。如果存在完全相同的字符串,就返回对现有String对象的引用。如果不存在完全相同的字符串,就创建字符串的副本,将副本添加到内部哈希表中,返回对该副本的引用。
和Intern方法一样,IsInterned方法也获取一个String,并在内部哈希表中查找它。如果哈希表中有匹配的字符串,IsInterned就返回对这个留用(interned)字符串对象的引用。但如果没有,IsInterned会返回null,不会将字符串添加到哈希表中。
程序集加载时,CLR默认留用程序集的元数据中描述的所有字面值(literal)字符串,Microsoft知道可能因为额外的哈希表查找而显著影响性能,所以现在能禁用此功能。如果程序集用System.Runtime.CompilerServices.CompilationRelaxationsAttribute进行了标记,并指定了System.Runtime.CompilerServices.CompilationRelaxations.NoStringInterning标志值,那么根据ECMA规范,CLR可能选择不留用那个程序集的元数据中定义的所有字符串。注意,为了提升应用程序性能,C#编译器在编译程序集时总是指定上述两个特性和标志。
String s1 = "Hello"; String s2 = "Hello"; Console.WriteLine(Object.ReferenceEquals(s1, s2)); // Should be 'False' s1 = String.Intern(s1); s2 = String.Intern(s2); Console.WriteLine(Object.ReferenceEquals(s1, s2)); // 'True'
在第二个ReferenceEquals方法调用之前,"Hello"字符串被显式留用,s1现在引用已留用的"Hello"。然后,通过再次调用Intern,s2引用和s1一样的"Hello"字符串。所以第二个ReferenceEquals调用保证结果是True,无论程序集在编译时是否设置了特性和标志。
private static Int32 NumTimesWordAppearsEquals(String word, String[] wordlist) { Int32 count = 0; for (Int32 wordnum = 0; wordnum < wordlist.Length; wordnum++) { if (word.Equals(wordlist[wordnum], StringComparison.Ordinal)) count++; } return count; }
private static Int32 NumTimesWordAppearsIntern(String word, String[] wordlist) { // This method assumes that all entries in wordlist refer to interned strings. word = String.Intern(word); Int32 count = 0; for (Int32 wordnum = 0; wordnum < wordlist.Length; wordnum++) { if (Object.ReferenceEquals(word, wordlist[wordnum])) count++; } return count; }
14.2.5字符串池
编译源代码时,编译器必须处理每个字面值(literal)字符串,并在托管模块的元数据中嵌入。同一个字符串在源代码中多次出现,把它们都嵌入元数据会使生成的文件无谓地增大。
为了解决这个问题,许多编译器(包括C#编译器)只在模块的元数据中只将字面值字符串写入一次。引用该字符串的所有代码都被修改成引用元数据中的同一个字符串。编译器将单个字符串的多个实例合并成一个实例,能显著减少模块的大小。但这并不是新技术,CC编译器多年来一直在采用这个技术(Microsoft的CC++编译器称之为“字符串池”)。
14.2.6 检查字符串中的字符和文本元素
为了正确处理文本元素,应当使用System.Globalization.Stringlnfo类型。
using System; using System.Text; using System.Globalization; using System.Windows.Forms; public sealed class Program { public static void Main() { // The string below contains combining characters String s = "a\u0304\u0308bc\u0327"; SubstringByTextElements(s); EnumTextElements(s); EnumTextElementIndexes(s); } private static void SubstringByTextElements(String s) { String output = String.Empty; StringInfo si = new StringInfo(s); for (Int32 element = 0; element < si.LengthInTextElements; element++) { output += String.Format( "Text element {0} is '{1}'{2}", element, si.SubstringByTextElements(element, 1), Environment.NewLine); } MessageBox.Show(output, "Result of SubstringByTextElements"); } private static void EnumTextElements(String s) { String output = String.Empty; TextElementEnumerator charEnum = StringInfo.GetTextElementEnumerator(s); while (charEnum.MoveNext()) { output += String.Format( "Character at index {0} is '{1}'{2}", charEnum.ElementIndex, charEnum.GetTextElement(), Environment.NewLine); } MessageBox.Show(output, "Result of GetTextElementEnumerator"); } private static void EnumTextElementIndexes(String s) { String output = String.Empty; Int32[] textElemIndex = StringInfo.ParseCombiningCharacters(s); for (Int32 i = 0; i < textElemIndex.Length; i++) { output += String.Format( "Character {0} starts at index {1}{2}", i, textElemIndex[i], Environment.NewLine); } MessageBox.Show(output, "Result of ParseCombiningCharacters"); } }
14.2.7 其他字符串操作
TABLE 14-1 M ethods for Copying Strings
|
Member |
Method Type |
Description |
|
Clone |
Instance |
返回对同一个对象(this)的引用。能这样做是因为String对象不可变(immutable),该方法实现了string的ICloneable接口 |
|
Copy |
Static |
返回指定字符串的新副本。该方法很少用,它的存在只是为了帮助一些需要把字符串当作token来对待的应用程序。通常,包含相同字符内容的多个字符串会被“留用”(intern)为单个字符串。该方法创建新字符串对象,确保即使字符串包含相同字符内容,引用(指针)也有所不同 |
|
CopyTo |
Instance |
将字符串中的部分字符复制到一个字符数组中 |
|
Substring |
Instance |
返回代表原始字符串一部分的新字符串 |
|
ToString |
Instance |
返回对同一个对象(this)的引用 |
除了这些方法,String还提供了多个用于处理字符串的静态方法和实例方法,比如Insert,Remove,PadLeft,Replace,Split,Join,ToLower,ToUpper,Trim,Concat,Format等。使用所有这些方法时都请牢记一点,它们返回的都是新的字符串对象。
14.3 高效率构造字符串
由于String类型代表不可变字符串,所以FCL提供了System.Text.StringBuilder类型对字符串和字符进行高效动态处理,并返回处理好的String对象。可将StringBuilder想像成创建String对象的特殊构造器。你的方法一般应获取String参数而非StringBuilder参数.
从逻辑上说,StringBuilder对象包含一个字段,该字段引用了由Char结构构成的数组。可利用StringBuilder的各个成员来操纵该字符数组,高效率地缩短字符串或更改字符串中的字符。如果字符串变大,超过了事先分配的字符数组大小,StringBuilder会自动分配一个新的、更大的数组,复制字符,并开始使用新数组。前一个数组被垃圾回收。
用StringBuilder对象构造好字符串后,调用StringBuilder的ToString方法即可将StringBuilder的字符数组“转换”成String。这样会在堆上新建String对象,其中包含调用ToString时存在于StringBuilder中的字符串。之后可继续处理StringBuilder中的字符串。以后可再次调用ToString把它转换成另一个String对象。
14.3.1 构造StringBuilder对象
- 最大容量
一个Int32值,指定了能放到字符串中的最大字符数,默认值是Int32.MaxValue(约20亿). - 容量
一个Int32值,指定了由StringBuilder维护的字符数组的长度。默认为16.
向字符数组追加字符时,StringBuilder会检测数组会不会超过设定的容量。如果会,StringBuilder会自动倍增容量字段,用新容量来分配新数组,并将原始数组的字符复制到新数组中。随后,原始数组可以被垃圾回收。数组动态扩容会损害性能。要避免就要设置一个合适的初始容量。 - 字符数组
一个由Char结构构成的数组,负责维护“字符串”的字符内容。
14.3.2 StringBuilder的成员
和String不同,StringBuilder代表可变(mutable)字符串。
StringBuilder只有以下两种情况才会分配新对象。
- 动态构造字符串,其长度超过了设置的“容量”。
- 调用StringBuilder的ToString方法。
TABLE 14-2 StringBuilder Members
|
Member |
Method Type |
Description |
|
MaxCapacity |
Read-only property |
返回字符串能容纳的最大字符数(最大容量) |
|
Capacity |
Read/write property |
获取或设置字符数组的长度(容量),将容量设得比字符串长度小或者比MaxCapacity大将抛出ArgumentOutORangeException异常 |
|
EnsureCapacity |
Method |
保证字符数组至少具有指定的长度(容量),如果传给方法的值大于StringBuilder的当前容量,当前容量会自动增大。如果当前容量已大于传给该属性的值,则不发生任何变化 |
|
Length |
Read/write property |
获取或设置“字符串”中的字符数。它可能小于字符数组的当前容量。将这个属性设为0,会将StringBuilder的内容重置为空字符串 |
|
ToString |
Method |
这个方法的无参版本返回代表StringBuilder的字符数组的一个String |
|
Chars |
Read/write indexer |
获取或设置字符数组指定索引位置的字符。在C#中,这是一个索引器(有参属性),要用数组语法()来访问 |
|
Clear |
property |
清除StringBuilder对象的内容,等同于把它的Length属性设为0 |
|
Append |
Method |
在字符数组末尾追加一个对象:如有必要,数组会进行扩充。使用常规格式和与调用线程关联的语言文化将对象转换成字符串 |
|
Insert |
Method |
在字符数组中插入一个对象;如有必要,数组会进行扩充。使用常规格式和与调用线程关联的语言文化将对象转换成字符串 |
|
AppendFormat |
Method |
在字符数组末尾追加指定的零个或多个对象;如有必要,数组会进行扩充。使用调用者提供的格式化和语言文化信息,将这些对象转换成字符串。AppendFormat是处理StringBuilder对象时最常用的方法之一 |
|
AppendLine |
Method |
在字符数组末尾追加一个行中止符或者一个带行中止符的字符串;如有必要,会增大数组的容量 |
|
Replace |
Method |
将字符数组中的一个字符替换成另一个字符,或者将一个字符串替换成另一个字符串 |
|
Remove |
Method |
从字符数组中删除指定范围的字符 |
|
Equals |
Method |
只有两个StringBuilder对象具有相同最大容量、字符数组容量和字符内容才返回true |
|
CopyTo |
Method |
将StringBuilder的字符内容的一个子集复制到一个Char数组中 |
使用StringBuilder的方法要注意,大多数方法返回的都是对同一个StringBuilder对象的引用,所以几个操作能连接到一起完成
StringBuilder sb = new StringBuilder(); String s = sb.AppendFormat("{0} {1}", "Jeffrey", "Richter"). Replace(' ', '').Remove(4, 3).ToString(); Console.WriteLine(s); // "Jeff Richter"
String和StringBuilder类提供的方法并不完全对应。
// Construct a StringBuilder to perform string manipulations. StringBuilder sb = new StringBuilder(); // Perform some string manipulations by using the StringBuilder. sb.AppendFormat("{0} {1}", "Jeffrey", "Richter").Replace(" ", " "); // Convert the StringBuilder to a String in // order to uppercase all the characters. String s = sb.ToString().ToUpper(); // Clear the StringBuilder (allocates a new Char array). sb.Length = 0; // Load the uppercase String into the StringBuilder, // and perform more manipulations. sb.Append(s).Insert(8, "Marc "); // Convert the StringBuilder back to a String. s = sb.ToString(); // Display the String to the user. Console.WriteLine(s); // "JEFFREYMarcRICHTER"
14.4 获取对象的字符串表示:ToString
System.Object实现的ToString只是返回对象所属类型的全名。
14.4.1 指定具体的格式和语言文化
无参ToString方法有两个问题。
- 首先,调用者无法控制字符串的格式。
- 其次,调用者不能方便地选择一种特定语言文化来格式化字符串。
FCL的所有基类型( Byte, SByte, Int16/UInt16, Int32/UInt32, Int64/UInt64, Single , Double , Decimal, and DateTime )都实现了这个接口。此外,还有另一些类型(比如Guid)也实现了它。最后,每个枚举类型定义都自动实现IFormattable接口,以便从枚举类型的实例获取一个字符串符号。
IFormattable的ToString方法获取两个参数。
- 第一个是format,这个特殊字符串告诉方法应该如何格式化对象。
- 第二个是formatProvider,是实现了System.IFormatProvider接口的一个类型的实例。该类型为ToString方法提供具体的语言文化信息。
格式化数字时,ToString方法检查为formatProvider参数传递的值。如果传递null.Tostring读取System.Globalization.CultureInfo.CurrentCulture属性来判断与调用线程关联的语言文化。该属性返回System.Globalization.Culturelnfo类型的一个实例。
14.4.2将多个对象格式化成一个字符串
String的静态Format方法获取一个格式字符串。在格式字符串中,大括号中的数字指定了可供替换的参数。
如果使用StringBuilder而不是String来构造字符串,可以调用StringBuilder的AppendFormat方法。它的原理与String的Format方法相似,只是会格式化字符串并将其附加到StringBuilder的字符数组中。
System.Console的Write和WriteLine方法也能获取格式字符串和可替换参数。
14.4.3 提供定制格式化器
using System; using System.Text; using System.Threading; public static class Program { public static void Main() { StringBuilder sb = new StringBuilder(); sb.AppendFormat(new BoldInt32s(), "{0} {1} {2:M}", "Jeff", 123, DateTime.Now); Console.WriteLine(sb); } } internal sealed class BoldInt32s : IFormatProvider, ICustomFormatter { public Object GetFormat(Type formatType) { if (formatType == typeof(ICustomFormatter)) return this; return Thread.CurrentThread.CurrentCulture.GetFormat(formatType); } public String Format(String format, Object arg, IFormatProvider formatProvider) { String s; IFormattable formattable = arg as IFormattable; if (formattable == null) s = arg.ToString(); else s = formattable.ToString(format, formatProvider); if (arg.GetType() == typeof(Int32)) return "<B>" + s + "</B>"; return s; } }
任何时候只要StringBuilder的AppendFormat方法需要获取对象的字符串表示,就会调用这个接口的Format方法。
public StringBuilder AppendFormat(IFormatProvider formatProvider, String format, params Object[] args) { // If an IFormatProvider was passed, find out // whether it offers an ICustomFormatter object. ICustomFormatter cf = null; if (formatProvider != null) cf = (ICustomFormatter) formatProvider.GetFormat(typeof(ICustomFormatter)); // Keep appending literal characters (not shown in this pseudocode) // and replaceable parameters to the StringBuilder's character array. Boolean MoreReplaceableArgumentsToAppend = true; while (MoreReplaceableArgumentsToAppend) { // argFormat refers to the replaceable format string obtained // from the format parameter String argFormat = /* ... */; // argObj refers to the corresponding element // from the args array parameter Object argObj = /* ... */; // argStr will refer to the formatted string to be appended // to the final, resulting string String argStr = null; // If a custom formatter is available, let it format the argument. if (cf != null) argStr = cf.Format(argFormat, argObj, formatProvider); // If there is no custom formatter or if it didn't format // the argument, try something else. if (argStr == null) { // Does the argument's type support rich formatting? IFormattable formattable = argObj as IFormattable; if (formattable != null) { // Yes; pass the format string and provider to // the type's IFormattable ToString method. argStr = formattable.ToString(argFormat, formatProvider); } else { // No; get the default format by using // the thread's culture information. if (argObj != null) argStr = argObj.ToString(); else argStr = String.Empty; } } // Append argStr's characters to the character array field member. /* ... */ // Check if any remaining parameters to format MoreReplaceableArgumentsToAppend = /* ... */; } return this; }
14.5解析字符串来获取对象:Parse
能解析字符串的任何类型都提供了公共静态方法Parse。方法获取一个String并返回类型的实例。从某种意义上说,Parse扮演了一个工厂(factory)的角色。
public static Int32 Parse(String s, NumberStyles style, IFormatProvider provider);
从原型就能猜出方法具体如何工作。String参数s是希望解析成Int32对象的一个数字的字符串表示。System.Globalization.Numberstyles参数style是位标志(bit lag)集合,标识了Parse应在字符串查找的字符(也就是字符串s中允许的样式,有不允许的样式会抛出异常)。如本章前面所述,IFormatProvider参数provider标识了一个对象,Parse方法通过该对象获取语言文化特有的信息。
Int32 x = Int32.Parse(" 123", NumberStyles.None, null);
要允许Parse跳过前导的空白字符,要像下面这样修改style参数:
Int32 x = Int32.Parse(" 123", NumberStyles.AllowLeadingWhite, null);
如果应用程序频繁调用Parse,而且Parse频繁抛出异常时(由于无效的用户输入),应用程序的性能会显著下降。为此,Microsoft在所有数值数据类型,DateTime类型、TimeSpan类型、甚至IPAddress类型中加入了TryParse方法。
public static Boolean TryParse(String s, NumberStyles style, IFormatProvider provider, out Int32 result);
14.6 编码:字符和字节的相互转换
在CLR中,所有字符都表示成16位Uncode码值,而所有字符串都由16位Unicode码值构成,这简化了运行时的字符和字符串处理。
但偶尔也想要将字符串保存到文件中,或者通过网络传输。如果字符串中的大多数字符都是英语用户用的,那么保存或传输一系列16位值,效率就显得不那么理想,因为写入的半数字节都只由零构成。相反,更有效的做法是将16位值编码成压缩的字节数组,以后再将字节数组解码回16位值的数组。
用System.IO.BinaryWriter或者System.IO.Stream Writer类型将字符串发送给文件或网络流时,通常要进行编码。对应地,用System.I0.BinaryReader或者System.IO.StreamReader类型从文件或网络流中读取字符串时,通常要进行解码。不显式指定一种编码方案,所有这些类型都默认使用UTF-8。(UTF全称是Unicode Transformation Format,即"Unicode转换格式”)
- UTF-16将每个16位字符编码成2个字节。不对字符产生任何影响,也不发生压缩——性能非常出色。UTF-16编码也称为"Unicode编码”。
- UTF-8将部分字符编码成1个字节,部分编码成2个字节,部分编码成3个字节,再有部分编码成4个字节。UTF-8编码方案非常流行,但如果要编码的许多字符都具有0x0800或者之上的值,效率反而不如UTF-16
- UTF-32使用4个字节来编码所有字符。要写简单算法来遍历所有字符,同时不愿意花额外精力应付字节数可变的字符,就适合采用这种编码。
- UTF-7编码用于旧式系统。
- ASCII编码方案将16位字符编码成ASCI1字符;也就是说,值小于0x0080的16位字符被转换成单字节。值超过0x007F的任何字符都不能被转换,否则字符的值会丢失。
要编码或解码一组字符时,应获取从System.Text.Encoding派生的一个类的实例。
using System; using System.Text; public static class Program { public static void Main() { // This is the string we're going to encode. String s = "Hi there."; // Obtain an Encodingderived object that knows how // to encode/decode using UTF8 Encoding encodingUTF8 = Encoding.UTF8; // Encode a string into an array of bytes. Byte[] encodedBytes = encodingUTF8.GetBytes(s); // Show the encoded byte values. Console.WriteLine("Encoded bytes: " + BitConverter.ToString(encodedBytes)); // Decode the byte array back to a string. String decodedString = encodingUTF8.GetString(encodedBytes); // Show the decoded string. Console.WriteLine("Decoded string: " + decodedString); } }
除了UTF8静态属性,Encoding类还提供了以下静态属性:Unicode,BigEndianUnicode,UTF32,UTF7,ASCII和 Default,Default属性返回的对象使用用户当前的代码页来进行编码/解码。
除了这些属性,Encoding还提供了静态GetEncoding方法,允许指定代码页(整数或字符串形式),并返回可以使用指定代码页来编码/解码的对象。
除了调用Encoding的某个静态属性或者它的GetEncoding方法,还可构造以下某个类的实例:System.Text.UnicodeEncoding,System.Text.UTF8Encoding,System.Text.UTF32Encoding System.Text.UTF7Encoding或者System.Text.ASCIIEncoding。
其中4个类(UnicodeEncoding,UTF8Encoding,UTF32Encoding和UTF7Encoding)提供了多个构造器,允许对编码和前导码,进行更多的控制(前导码有时也称为“字节顺序标记”,即Byte Order Mark或者BOM)。在这4个类中,前3个类还提供了特殊的构造器,允许在对一个无效的字节序列进行解码的时候抛出异常。如果需要保证应用程序的安全性,防范
无效的输入数据,就应当使用这些能抛出异常的类。
从Encoding派生的每个对象都提供了一组公共只读属性,可查询这些属性来获取有关编码的详细信息。
using System; using System.Text; public static class Program { public static void Main() { foreach (EncodingInfo ei in Encoding.GetEncodings()) { Encoding e = ei.GetEncoding(); Console.WriteLine("{1}{0}" + "\tCodePage={2}, WindowsCodePage={3}{0}" + "\tWebName={4}, HeaderName={5}, BodyName={6}{0}" + "\tIsBrowserDisplay={7}, IsBrowserSave={8}{0}" + "\tIsMailNewsDisplay={9}, IsMailNewsSave={10}{0}", Environment.NewLine, e.EncodingName, e.CodePage, e.WindowsCodePage, e.WebName, e.HeaderName, e.BodyName, e.IsBrowserDisplay, e.IsBrowserSave, e.IsMailNewsDisplay, e.IsMailNewsSave); } } }
TABLE 14-3 Methods of the Encoding -Derived Classes
|
Method |
Description |
|
GetPreamble |
返回一个字节数组,指出在写入任何已编码字节之前,首先应该在一个流中写入什么字节。 |
|
Convert |
将字节数组从一种编码(来源编码)转换为另一种(目标编码)。在内部,这个静态方法调用来源编码对象的GetChars方法,并将结果传给目标编码对象的GetBytes方法。结果字节数组返回给调用者 |
|
Equals |
如果从Encoding派生的两个对象代表相同的代码页和前导码设置,就返回true |
|
GetHashCode |
返回当前Encoding实例的哈希码 |
14.6.1 字符和字节流的编码和解码
字节块解码首先要获取一个Encoding派生对象引用(参见上一节),再调用其GetDecoder方法。方法返回对一个新构造对象的引用,该对象的类型从System.Text.Decoder类派生。
Decoder的所有派生类都提供了两个重要的方法:GetChars和GetCharCount。显然,这些方法的作用是对字节数组进行解码,工作方式类似于前面讨论过的Encoding的GetChars和GetCharCount方法。调用其中一个方法时,它会尽可能多地解码字节数组。假如字节数组包含的字节不足以完成一个字符,剩余的字节会保存到Decoder对象内部。下次调用其中一个方法时,Decoder对象会利用之前剩余的字节再加上传给它的新字节数组。这样一来,就可以确保对数据块进行正确解码。从流中读取字节时Decoder对象的作用很大。
从Encoder派生的所有类都提供了两个重要方法:GetBytes和GetByteCount。每次调用,从Encoder派生的对象都会维护余下数据的状态信息,以便以成块的方式对数据进行编码。
14.6.2 Base-64字符串编码和解码
将Base-64字符串编码成字节数组需调用Convert的静态FromBase64String或FromBase64CharArray方法。类似地,将字节数组解码成Base-64字符串需调用Convert的静态ToBase64String或者ToBase64CharArray方法。
using System; public static class Program { public static void Main() { // Get a set of 10 randomly generated bytes Byte[] bytes = new Byte[10]; new Random().NextBytes(bytes); // Display the bytes Console.WriteLine(BitConverter.ToString(bytes)); // Decode the bytes into a base64 string and show the string String s = Convert.ToBase64String(bytes); Console.WriteLine(s); // Encode the base64 string back to bytes and show the bytes bytes = Convert.FromBase64String(s); Console.WriteLine(BitConverter.ToString(bytes)); } }
14.7安全字符串
有的政府部门有严格的安全要求,对各种安全措施进行了非常具体的规定。为了满足这些要求,Microsof在FCL中增添了一个更安全的字符串类,即System.Security.SecureString.构造SecureString对象时,会在内部分配一个非托管内存块,其中包含一个字符数组。
SecureString类实现了IDisposable接口,允许以简单的方式确定性地摧毁字符串中的安全内容。应用程序不再需要敏感的字符串内容时,只需调用SecureString的Dispose方法。
只有少数方法才能接受SecureString数。在NET Framework 4中,以下情况允许将SecureString作为密码传递
- 与加密服务提供程序(Cryptographic Service Provider,CSP)协作。参见System.Security.Cryptography.CspParameters类。
- 创建、导入或导出X.509证书。参见System.Security.Cryptography.x509Certificates.x509Certificate类和System.Security.Cryptography.X509Certificates.X509Certificate2类
- 在特定用户帐户下启动新进程。参见System.Diagnostics.Process和System.Diagnostics.ProcessStartInfo类
- 构造事件日志会话。参见System.Diagnostics.Eventing.Reader.EventLogSession类。
- 使用System.Windows.Controls.PasswordBox控件。参见该类的SecurePassword属性
使用一个SecureString(编译时要为C#编译器指定/unsafe开关选项
using System; using System.Security; using System.Runtime.InteropServices; public static class Program { public static void Main() { using (SecureString ss = new SecureString()) { Console.Write("Please enter password: "); while (true) { ConsoleKeyInfo cki = Console.ReadKey(true); if (cki.Key == ConsoleKey.Enter) break; // Append password characters into the SecureString ss.AppendChar(cki.KeyChar); Console.Write("*"); } Console.WriteLine(); // Password entered, display it for demonstration purposes DisplaySecureString(ss); } // After 'using', the SecureString is Disposed; no sensitive data in memory } // This method is unsafe because it accesses unmanaged memory private unsafe static void DisplaySecureString(SecureString ss) { Char* pc = null; try { // Decrypt the SecureString into an unmanaged memory buffer pc = (Char*) Marshal.SecureStringToCoTaskMemUnicode(ss); // Access the unmanaged memory buffer that // contains the decrypted SecureString for (Int32 index = 0; pc[index] != 0; index++) Console.Write(pc[index]); } finally { // Make sure we zero and free the unmanaged memory buffer that contains // the decrypted SecureString characters if (pc != null) Marshal.ZeroFreeCoTaskMemUnicode((IntPtr) pc); } } }
TABLE 14-4 Methods of the Marshal Class for Working with Secure Strings
|
将SecureString解密到缓冲的方法 |
清零并释放缓冲区的方法 |
|
SecureStringToBSTR |
ZeroFreeBSTR |
|
SecureStringToCoTaskMemAnsi |
ZeroFreeCoTaskMemAnsi |
|
SecureStringToCoTaskMemUnicode |
ZeroFreeCoTaskMemUnicode |
|
SecureStringToGlobalAllocAnsi |
ZeroFreeGlobalAllocAnsi |
|
SecureStringToGlobalAllocUnicode |
ZeroFreeGlobalAllocUnicode |
浙公网安备 33010602011771号