字符、字符串和文本处理
1.1字符
在.NET Framework中,字符都是用16位Unicode编码(UTF-16)的(编译时用UTF-16编码成2进制存到硬盘,程序运行时再用utf-16解码显示代码中的字符串,在内存中相应的字节流就是用UTF-16编码过的),也就是说所有字符都是占2个字节16位,这简化了国际化应用程序的开发。Unicode字符集有很多种编码方案,常用的有:
UTF-16:所有字符被编码成2个字节
UTF-8:十进制小于128的字符被编码成1个字节(可表示欧美地区使用的字符),128~2047的字符被编码成2个字节(可表示欧洲和中东语言),大于2047的字符被编码成3个字节(可表示东亚地区的语言)
UTF-32:所有字符都被编码成4个字节
Unicode字符集还有个ASCII编码方案,这种编码只能将小于128的16位字符转换成单字节,而其他超过127的字符都会丢失。
GB2312等其他字符集(这些字符集可能只有一种同名编码方案)
针对Char的一个实例,可以调用Char类型的静态方法GetUnicodeCategory,这个方法返回的是System.Globalization.UnicodeCategory枚举类型的一个值。这个值指出该字符是控制字符、货币符号、小写字母、大写字母、标点符号、数字符号 还是其他Unicode标准定义的符号。 其他一些静态方法如IsDigit、IsLetter、IsUpper、IsControl、IsSymol等都在内部调用了GetUnicodeCategory,并简单返回true或false。注意,所以这些方法要么获取单个字符作为参数,要么获取一个String以及目标字符在这个String中的索引作为参数。
另外,可以调用静态方法ToLowerInvariant或者ToUpperInvariant以一种忽略语言文化的方式,将一个字符转化为小写或大写。如果调用ToLower和ToUpper方法,在转换时要使用与线程相关的语言文化信息,语言文化信息是这两个方法在内部查询System.Threading.Thread类的静态CurrentCulture属性来获得的。还可以向这些方法传递CultureInfo类的一个实例来具体指定一种语言文化。ToLower和ToUpper之所以需要语言文化信息,是因为字母的大小写转换是依赖于语言文化操作的。
可以使用三种技术实现各种数值类型与Char实例的相互转换,下面按照优先顺序列出这些技术。
*转型(强制类型转换)要将一个Char转换成一个数值(如Int32),最简单的方法是强制类型转换。在三种技术中,这种技术效率最高,因为编译器会生成IL(Intermediate Language中间语言)指令来执行转换,不必调用任何方法。
*使用Convert类型 System.Convert类型提供了几个静态方法来实现Char和数值类型的相互转换。这些方法都以checked方式来执行转换,因此一旦发现转换造成数据丢失,就会抛出一个OverflowException异常。
*使用IConvertible接口 Char类型和FCL中的所有数值类型都实现了IConvertible接口。该接口定义了像ToUInt16和ToChar这样的方法。但是这种技术效率最差,因为在值类型上调用一个接口方法,要求对实例进行装箱(Char和所有数值类型都是值类型)。如果某个类型不能转换(比如Char转换成Boolean),或者转换造成数据的丢失,IConvertible的方法会抛出一个System.InvalidCastException异常。
以下代码简单演示如何使用这三种技术
1 Char c; 2 Int32 n; 3 4 //使用C#强制转换技术实现数字与字符的相互转型 5 c = (Char) 65; 6 Console.WriteLine(c); //显示"A" 7 8 n = (Int32) c; 9 Console.WriteLine(n); //显示"65" 10 11 c = unchecked((Char) (65536 + 65)); 12 Console.WriteLine(c); //显示"A" 13 14 //使用Convert实现数字与字符的相互转换 15 c = Convert.ToChar(65); 16 Console.WriteLine(c); //显示"A" 17 18 n = Convert.ToInt32(c); 19 Console.WriteLine(n); //显示"65" 20 21 //演示Convert的范围检查 22 try 23 { 24 c = Convert.ToChar(70000); //对16位来说太大 25 Console.WriteLine(c); // 不执行 26 } 27 catch (OverflowException) 28 { 29 30 Console.WriteLine("Can't 70000 to a char!"); 31 } 32 33 //使用IConvertible实现数字与字符的相互转换 34 c = ((IConvertible) 65).ToChar(null); 35 Console.WriteLine(c); //显示"A" 36 37 n = ((IConvertible) c).ToInt32(null); 38 Console.WriteLine(n); //显示"65"
1.2 System.String类型
1.2.1 构造字符串
一个String代表一个不可变的顺序字符集。String类型直接派生自Object,所以它是一个引用类型。因此String对象总是存在于堆上,永远不会跑到线程栈。许多编程语言都将String视为一个基元类型----可以再源代码中直接表示文本常量字符串(string s="hi"); 编译器将这些文本常量字符串放到模块的元数据中,并在运行时加载和引用它们。
在C#中,不能使用new操作符从一个文本常量字符串构造一个String对象。
对于换行符、回车符和退格符这样的特殊字符,C#采用的是C/C++开发人员熟悉的转义机制:
1 // 包含回车符和换行符的字符串 2 string s="hi\r\nthere"; 3 4 //NewLine是System.Environment类型定义的一个属性,NewLine属性是依赖于平台的,它在任何平台上都能正确工作,建议使用这种方式 5 string s="hi"+Environment.NewLine+"there";
可以使用C#的+操作符将几个字符串连接成一个,如下所示:
对于如下由好几个文本常量字符串组成的字符串:
string s="hi"+" "+"there"; //注意:编译器会在编译时连接它们,最终只会将一个字符串放到模块的元数据中
对于如下由好几个非文本常量字符串组成的字符串:
string s1="hi"; string s2="there"; string s=s1+s2; //注意:对非文本常量字符串使用+操作符,连接会在运行时进行。
若要在运行时将几个字符串连接到一起,应避免使用+操作符,因为它会在堆上创建多个字符串对象,而堆是需要回收的,从而影响性能。相反,应尽量使用System.Text.StringBuilder类型
C#还提供了逐字字符串,通常用于指定文件或目录的路径,或与正则表达式配合使用。采取这种方式,引号之间的所有字符都会被视为字符串的一部分:
1 //指定应用程序路径 2 string file="C:\\windows\\System32\\Notepad.exe"; 3 4 //使用逐字字符串来指定应用程序路径 5 string file=@"C:\windows\System32\Notepad.exe";
在字符串前添加@符号,使编译器知道字符串是一个逐字字符串,这告诉编译器将反斜杠视为文本常量,而不是转义符,使文件路径在源代码中更易读。
1.2.2 字符串是不可变的
string对象最重要的一个事实就是,它是不可变的,也就是说字符串一经创建便不能更改,不能变长,变短或修改其中任何字符。
所以允许对一个字符串进行各种操作而不实质的改变字符串:
1 string s="hiworld"; if(s.ToUperInvariant().Substring(0,2).EndsWith("EXE")) 2 { 3 ... 4 }
在此,ToUperInvariant()返回一个新的字符串,它没有修改s的字符,然后Substring(0,2)在ToUperInvariant()返回的新字符串的基础上又返回一个新字符串。 ToUperInvariant和Substring创建的两个临时字符串不会由应用程序代码长久的引用,垃圾回收器会在下次回收时回收它们的内存,如果执行大量的字符串操作,会在堆上创建大量的string对象,造成频繁的垃圾回收,从而损害应用程序的性能,要想高效率地执行大量字符串操作,请用StringBuilder类。
使字符串不可变,还意味着在操纵或访问一个字符串时不会发生线程同步问题。