[CLR via C#]14. 字符、字符串和文本处理

一、字符

  在.NET Framewole中,字符总是表示成16位Unicode代码值,这简化了国际化应用程序的开发。

  每个字符都表示成System.Char结构(一个值类型) 的一个实例。System.Char类型提供了两个公共只读常量字段:MinValue(定义成"\0")和MaxValue(定义成'\uffff')。
 
  针对Char的一个实例,可以调用GetUnicodeCategory方法,这个方法返回的是System.Globalization.UnicodeCategory枚举类型的一个值。这个值支出该字符是控制字符、货币符号、小写符号、大写符号、标点符号、数学符号,还是其他字符(Unicode定义的字符)。
 
  为了简化开发,Char类型还提供了几个静态方法,比如IsDigit,IsLotter,IsUpper,IsLower,isNumber等。其中大多数方法都在内部调用了GetUnicodeCategory,并简单的返回true和false。
 
    另外可调用ToLowerInvariant和ToUpperInvariant,以忽略语言文化(culture)的方式,将一个字符转换成小写和大写形式。作为另一种替代方案,ToLower和ToUpper方法将字符转换成小写和大写形式,但是转换时要使用与调用线程关联的静态CurrenCulture属性来获得。ToLower和ToUpper之所以需要语言文化信息,是因为字母大小写的转换是依赖于语言文化的。不同的语言文化,大小写的形式也不尽相同。
 
  可以使用三种技术实现各种数值类型与Char实例的相互转换:
  1) 转型(强制类型转换) 要将一个Char转换成为一个数值(比如Int32),最简单的方法就是转型。在三种技术中,效率也是最高的,因为编译器会生成中间语言(IL)指令来执行转型,不必调用任何方法。
 
  2) 使用Convert类型 System.Convert类型提供了几个静态方法来实现Char和数值类型的相互转型。所以这些方法都是以checked方式进行转换,所以一旦发现转型会造成数据丢失,就会抛出OverflowException异常。
 
  3) 使用IConvertible接口 Char类型和FCL的所有数值类型都实现的ICOnvertible接口。该接口定义了像ToUInt16和ToChar这些的方法。这种技术效率最差,因为在值类型上调用一个接口方法,要求对实例进行装箱—— Char和所有数值类型都是值类型。如果某个类型不能转换,或者转换造成数据的丢失,IConvertible的方法会抛出System.InvalidCastException异常。
 
  下面演示这三种方法的调用:
internal static class CharConvert {
   public static void Go() {
      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"
   }
}

 

二、字符串

  一个String代表一个不可变(immutable)的顺序字符集。String直接派生自Object,所以它是一个引用类型。因此,String(字符串数组)总是存在于堆上,不会跑到栈上去。
 
  String类型还实现了几个接口,IComparable、ICloneable等
 
  1.构造字符串
 
  许多编程语言(包括C#)都将String视为一个基元类型——也就是说,编译器允许在源代码中直接表示文本常量字符型。编译器将这些文本常量字符串放到模块的元数据中,并在运行时加载和引用它们。
 
  在C#中,不能使用new操作符从一个文本常量字符串构造一个String对象,相反,必须使用简化的语法表示: 
    
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属性是依赖于平台的,他会一句底层平台返回恰当的字符串。

  若要在运行时将几个字符串连接到一起,请避免使用+操作符,因为它会在堆上创建多个字符串对象,而对象是需要垃圾回收的,从而影响性能。相反,应尽量使用System.Text.StringBuilder类型。
 
  最后,C#还提供了一种特殊的字符串声明方式(@"xxx")。采用这种方式,引号之间的所有字符都会被视为字符串的一部分。这种特殊声明称为"逐字字符串",通常用于指定文件或目录的路径,或者配合正则表达式使用。
 
  2.字符串是不可变的
 
  String对象最重要的一个事实就是,它是不可变的(immytable)。也就是说,字符串一经创建就不能更改,不能变长、变短或修改其中任何字符。
字符串不可变也下面几点好处:
  1.它允许在字符串上执行任何操作,而不实际的更改字符串。
  2.在操作或访问字符串时不会发生线程同步问题。
  3.CLR可通过一个String对象共享多个完全一致的String内容。这样能减少系统中的字符串属性,从而节省内存,这就是"字符串留用"技术的目的。
 
  考虑到性能方面的原因,String类型和CLR是紧密集成的。具体的说,CLR知道String类型中定义的字段是如何布局的,而且CLR会直接访问这些字段。但是,为了获得这种性能和直接访问的好处,开发时只好将String定义为密封类。
 
  3.比较字符串
 
  判断字符串相等性或对字符串进行排序时,强烈建议调用下面列出的方法之一:
 
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方法,是因为它们对语言文化敏感。
 
4.字符串留用
 
  检查字符串的相等性是许多应用程序的常见操作——这个任务可能验证损害性能。执行序号(ordinal)相等性检查时,CLR快速测试两个字符串是否包含相同数量的字符。如果是否定,字符串肯定不相等;如果肯定,字符串可能相等。然后,CLR必须比较每个单独的字符才能确定。值得注意的是,在执行需要注意语言文化的比较是,CLR始终都要比较所有单独的字符,因为两个字符串即使长度不同,也可能是相等的。
 
  除此之外,如果在内存中复制同一个字符串的多个实例,会造成内存的浪费,因为字符串是"不可变"的。如果只在内存中保留字符串的一个实例,那么将显著提高内存的利用率。需要引用字符串的所有变量只需指向单独一个字符串对象。
 
  如果引用程序经常对字符串进行区分大小写、序号式比较,或者事先知道许多字符串对象都有相同的值,就可以利用CLR的"字符串留用"机制来显著提高性能。CLR初始化时会创建一个内部哈希表。在i这个表中,键(key)是字符串,而值(value)是对托管堆中String对象的引用。哈希表最开始是空的,String类提供了两个方法,便于你访问这个内部哈希表:
//检索系统对指定 System.String 的引用
public static string Intern(string str)
//检索对指定 System.String 的引用
public static string IsInterned(string str)

  第一个方法Intern获取一个String,获得它的哈希码,并在内部哈希表中检查是否有匹配的。如果存在一个完全相同的字符串,就返回对这个字符串已经存在的String对象的一个引用。如果不存在,就创建字符串的副本,将副本添加到内部哈希表中,并返回对这个副本的一个引用。

 
  和Intern方法一样,IsInterned方法也获取一个String,并在内部哈希表中查找它。如果哈希表中有一个匹配的字符串,IIsInterned就返回对这个留用的字符串对象的一个引用。然而,如果哈希表中没有一个相匹配的字符串,IsInterned会返回null;它不会将字符串添加到哈希表中。
 
  一个程序集加载时,CLR默认会留用程序集的元数据中描述的所有文本常量(literal)字符串。Microsoft知道可能因为额外的哈希表查找会造成性能显著下降,所以现在是可以禁用这个"特性"的。
 
  根据ECMA规范,CLR可能选择不留用那个程序集的元数据中定义的所有字符串。即使指定了CLR不留用那个程序集中的字符串,但是CLR也可能选择对字符串进行留用,但不应该依赖于CLR的这种"自主"行为。事实上,除非自己显式调用String的Intern方法,否则永远都不要以"字符串已留用"为前提来写自己的代码。以下代码演示了字符串留用:
 public static void Go() {
      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.0版本上运行,会发现显示True。因为这个版本的CLR选择了忽略C#编译器中字符串不留用的设置。
 
5. 字符串池
 
  编译源代码时,编译器必须处理每个文本常量字符串,并在托管模块的元数据上嵌入字符串。如果同一个文本常量字符串在源代码中多次出现,将所有这些字符串都嵌入元数据中,会陡然增大最终生成的文件。
 
  为了解决这个问题,许多编译器(包括C#编译器)都只在模块的元数据中将文本常量字符串写入一次。引用该字符串的所有代码都会被修改,以引用元数据中的同一个字符串。编译器这种将单个字符串的多个实例合并为一个实例的做法,可以显著减少模块大小。
 
三、高效率构造字符串
 
  由于String类型是一个不可变的字符串,所以FCL提供了另一个名为System.Text.StringBuilder的类型,可利用它高效率得对字符串和字符进行动态代理,最后基于处理结果创建一个String。
 
  从逻辑上说,StringBuilder对象包含一个字段,该字段引用了有Char结构构成的一个数组。可利用StringBuilder的各个成员来操作这个字符数组,高效率的缩短或更改字符串中的字符。如果字符串变大,超过已分配的字符数组的大小,StringBuilder会自动分配一个新的、更大的数组,复制字符,并开始使用新数组。前一个数组会被垃圾回收。
 
四、获取对象的字符串表示:ToString
 
  在.NET Framework中可以调用ToString方法来和获取任何对象的字符串表示。System.Objetc定义了一个public、virtual、无参的ToString方法,所以在任何类型的一个实例上都能调用该方法。在语义上,ToString返回代表对象当前值的一个字符串,而且这个字符串应该根据调用线程当前的语言文化进行格式化。
 
五、解析字符串来获取对象:Parse
 
  能解析一个字符串的任何类型都提供了一个名为Parse的public static方法。该方法获取一个String对象,并返回类型的一个实例。从某种意义上说,Parse扮演了一个工厂的角色。在FCL中,所有数值类型,DateTime、TimeSpan以及一些其他类型均提供了Parse方法。
posted @ 2014-02-24 12:40  烧点饭  阅读(1733)  评论(1编辑  收藏  举报