12.1 字符
一个字符由一个System.Char结构实例表示,该类型提供了两个常数字段:MinValue(0x0000)和MaxValue(0xFFFF)
Char的静态方法GetUnicodeCategory以一个Char实例作为参数,返回一个System.Globalization.UnicodeCategory枚举值,我们可以根据该枚举值判断传入的字符的类型
Char类型还提供有其他几个静态方法如IsDigit、IsLetter、IsWhiteSpace、IsUpper、IsLower、IsPunctuation、IsLetterOrDigit、IsControl、IsSeparator、IsSurrogate等,这些方法在内部调用了GetUnicodeCategory方法并简单的返回true或false。这些方法接受的参数为一个字符或一个String和String中的一个字符索引
静态方法ToUpper或ToLower在做字符转换时会用到与线程相关联的语言文化信息,方法内部会查询System.Threading.Thread的静态属性CurrentCulture,我们也可以传递一个System.Globalization.CultureInfo实例作为参数来指定一个特定的语言文化信息
Char类型还定义了一些实例方法,如Equals(比较16为Unicode码值)、CompareTo(定义于IComparable接口,比较字符码值,与语言文化无关)、ToString等。静态方法Parse与ToString方法作用相反
静态方法GetNumericValue返回一个字符对应的数值形式,如果该字符表示数字则返回对应的数值,否则返回-1.0
在数值和Char实例之间?进行转换有以下三种方法(注意这里的转换是把数值和Char的Unicode码值相对应,不同于GetNumericValue):
1、转型
也就是直接用Char到数值的隐式转换或数值到Char的显式转换。这是将一个Char转换成一个数值最容易的方法,编译器会直接产生IL指令来执行转换而不会有任何的方法调用,因此效率最高。某些语言如C#允许我们使用Checked或unchecked来决定数据丢失时是否抛出System.OverflowException异常。这种技巧唯一的缺点是它要求编译器要将期望转换的数值类型看作是基元类型,例如VB不把UInt16看作一个基元类型,所以就不能在VB里用这种技巧在Char和UInt16间进行转换
2、使用Convert类型
System.Convert类型提供了几种静态方法允许我们在Char和数值类型间执行转换,所有这些方法执行的转换都为checked操作
3、使用IConvertible接口
Char类型和所有FCL中的数值类型都实现了IConvertible接口,该接口定义了如ToInt32、ToChar这样的方法。因为Char和所有的数值类型都是值类型,而在一个值类型上调用接口方法会导致装箱操作,所以这些方法执行的效率不如上面的好。如果类型间不能进行转换或转换会导致数据丢失,IConvertible中的方法都会抛出异常。FCL中的Char类型和数值类型都是以显式接口成员实现的方式来实现IConvertible接口中的方法,意味着我们在调用任何该接口方法前都必须先将实例转换为IConvertible接口。这些方法需要一个区域格式信息(一个System.IFormatProvider接口的实现)参数,可以直接使用null表示不选择特定的区域格式信息
12.2 System.String类型
String类型直接继承自Object,是一个引用类型,一个String表示一个恒定不变的字符序列集合。String类型实现了如下几个接口:IComparable、ICloneable、IConvertible和IEnumerable
12.2.1 创建字符串
许多编程语言包括C#都将String认为是一个基元类型,允许我们在源代码使用文本常量(literal)来直接表达字符串。编译器会将这些文本常量字符串存放在托管模块的元数据中,然后在运行时使用字符串驻留(string interning)的机制来访问
C#中我们不能使用new来创建String对象(事实上不是不能,只是参数不能为文本常量或字符串),而必须使用简化的语法,生成的IL代码中没有用构造对象实例的newobj而用了一个特殊的IL指令ldstr(即load string,加载字符串),该指令通过从元数据中获得的文本常量来构造String对象
C#中用+操作符来将几个字符串连接为一个字符串。如果所有的字符串都是文本常量字符串,编译器会在编译时就将它们连接起来,如果不是则回在运行时连接。使用+操作符运行时连接会在要执行垃圾收集的托管堆上创建多个字符串对象,建议用system.Text.StringBuilder类型进行字符串连接
字面字符串(verbatim string)以@开头,允许把双引号间的字符都认为是字符串而不是转义字符。但如果要在字面字符串中用到双引号,应该使用两个连续的没有空格的双引号
12.2.2 字符串的恒定性
String对象的恒定性允许我们在一个字符串上进行各种操作而不改变原来的字符串,任何改变字符串内容的操作都会返回一个新的字符串
字符串的恒定性还意味着操作字符串的时候不会出现线程同步的问题,另外如果两个字符串的值相等我们可以使其指向同一个字符串对象从而节省内存
出于性能考虑,String类型和CLR被紧密的集成在一起。String为密封类型,防止继承后破坏CLR对String所做的假设
12.2.3 字符串比较
String类型提供了以下一些方法进行字符串比较:
静态方法:Compare、CompareOrdinal、Equals
实例方法:CompareTo、StartsWith/EndsWith、Equals、GetHashCode
操作符:==、!=
CompareOrdinal方法只比较字符串中字符的码值,速度比较快。但有些字符串即使其中的字符码值不同,在逻辑上仍被认为是相等的,这时应使用Compare方法从逻辑上判断两个字符串是否相等。Compare方法在内部使用了特定语言文化的排序表,并可以在比较时选择是否考虑大小写敏感问题
在比较字符串或对字符串进行大小写转换时我们应使用System.Globalization命名空间下CultureInfo类型的InvariantCulture属性,只有在比较和转换的字符串要显示给一个特定语言下的用户时才应该使用一个非固定(noninvariant)的语言文化
System.Globalization命名空间下的CompareInfo类提供了Compare、IndexOf、IsLastIndexOf、IsPrefix、IsSuffix等方法,这些方法给予了我们比String类中相应的方法更多的控制
12.2.4 字符串驻留
字符串比较操作要求检查字符串中每一个字符直到有两个字符不同为止,这可能会极大的损伤系统性能。此外如果内存中有多个相同字符串的实例也是对内存的一种浪费。我们可以利用CLR中的字符串驻留(string interning)机制来提高性能
CLR会对嵌入源代码中的文本常量字符串进行字符串驻留操作。当CLR初始化时它会创建一个内部的散列表,键为字符串值为指向托管堆中的字符串对象的引用,JIT编译器编译方法时就把所有文本常量字符串保存在这个散列表中。因为相同的字符串保存在托管堆中的同一个位置,所以节约了内存空间,在比较字符串时也只需比较引用是否相同,从而提高了效率
对于动态创建的字符串,CLR没有将其添加到内部的散列表中,我们可以用String类型的两个静态方法Intern和IsInterned来做到这点。Intern方法查找散列表,如果字符串存在则返回String对象的引用,不存在则在散列表中添加该字符串后返回引用,IsInterned方法则是不存在的时候返回null。C#编译器就使用了IsInterned方法对switch/case语句进行了优化,它对switch语句中指定的字符串使用IsInterned操作,如果返回null则不可能匹配任何case语句的字符串(因为case语句中的字符串肯定存在于散列表中),否则就依次比较IsInterned返回的字符串引用和各case语句指定的字符串引用
垃圾收集器不会释放CLR内部散列表中引用的字符串对象,只有当进程中所有的应用程序域都不再引用这些字符串对象时它们才会被释放
12.2.5 字符串池技术
编译源代码时如果编译器把每个重复的文本常量都放入生成模块的元数据中,可能会造成元数据大小的急剧膨胀,为此许多编译器包括C#编译器都在生成模块的元数据中只写入一次这样的字符串,然后将所有引用该字符串的代码改变为引用元数据中的一个字符串
12.2.6 查看字符串中的字符
String类型提供了以下实例方法来帮我们实现查看字符串中字符的操作:Length(实例只读属性)、Chars(实例只读索引器属性)、GetEnumerator、ToCharArray、IndexOf、LastIndexOf、IndexOfAny、LastIndexOfAny
System.Char表示的16位Unicode并不必然等于一个抽象的Unicode字符,如某些抽象Unicode字符是两个码值的组合,某些抽象字符必须要使用两个16位值来表示(其中第一个码值称为高位代理项high surrogate,位于U+D800和U+DBFF之间,第二个码值称为低位代理项low surrogate,位于U+DC00和U+DFFF之间)等
要正确的处理抽象Unicode字符,应该使用System.Globalization.StringInfo类型,其中的静态方法GetTextElementEnumerator可以获取一个System.Globalization.TextElementEnumerator对象来枚举字符串中包括的所有抽象Unicode字符。而静态方法ParseCombiningCharacters可以获取一个Int32数组,数组长度表示字符串中抽象Unicode字符的个数,每个元素表示字符串中每一个抽象Unicode字符的第一个码值出现的索引
12.2.7 其他字符串操作
String类型提供了其他几种允许我们拷贝一个字符串或其一部分的方法:Clone、Copy、CopyTo、Substring、ToString,其中只有Copy方法为静态方法
此外String还提供了许多其他的静态方法和实例方法,如Insert、Remove、PadLeft、Replace、Split、Join、ToLower、ToUpper、Trim、Concat、Format等
12.3 高效地动态创建字符串
String对象是不可改变的。每次使用System.String类中的方法之一时,都要在内存中创建一个新的字符串对象,这就需要为该新对象分配新的空间。在需要对字符串执行重复修改的情况下,与创建新的String对象相关的系统开销可能会非常昂贵。如果要修改字符串而不创建新的对象,则可以使用System.Text.StringBuilder类。例如,当在一个循环中将许多字符串连接在一起时,使用StringBuilder类可以提升性能
StringBuilder对象在内部有一个指向Char结构数组的字段,它的成员允许我们操作该字符数组。如果字符串的增长超过了原来分配的字符数组,StringBuilder会自动将容量加倍并分配一个新的数组,先前的数组将成为可被垃圾收集器回收的对象
我们可以调用StringBuilder的ToString方法来将StringBuilder中的字符数组转换为一个String,该方法返回一个StringBuilder内部维护的字符串字段,而不需要拷贝字符数组,这使得StringBuilder的ToString方法执行很快
因为ToString方法返回的String必须是恒定的,当我们调用的StringBuilder的方法试图改变维护的字符串字段时,StringBuilder可以判断出ToString方法先前是否被调用过,如果是则会在内部创建一个新的字符数组来使用,使得操作不会影响到先前调用ToString返回的字符串
12.3.1 构造StringBuilder对象
StringBuilder对象不被大多数语言看成基元类型,构造时需要用new关键字。StringBuilder类型的构造器主要是分配和初始化StringBuilder对象的最大容量、容量和字符数组。最大容量的默认值为Int32.MaxValue(约20亿),通常情况下我们不应改变该值。容量的默认值为16,超过会自动加倍并分配新的数组,我们应该设置一个合理的初始化容量来避免数组动态增长。StringBuilder的Length属性可以取得字符数组的长度
12.3.2 StringBuilder的成员
多数StringBuilder的成员都会改变字符数组的内容,并且不会导致分配新的对象。只有当动态构造一个超过容量的字符串,或在调用StringBuilder的ToString方法后试图改变字符数组,这两种情况发生时StringBuilder才会分配新的对象
为获得更高的性能,StringBuilder的方法不保证线程安全
StringBuilder的多数方法都返回指向同一个StringBuilder对象的引用,这使得我们可以很方便的将几个操作放在一起执行
StringBuilder类型的使用可以参见VS2003 MSDN:
ms-help://MS.MSDNQTR.2003FEB.2052/cpguide/html/cpconusingstringbuilderclass.htm