第 14 章 字符、字符串和文本处理

第 14 章 字符、字符串和文本处理

本章内容:

14.1 字符

在.NET Framework 中,字符总是表示成 16 位 Unicode 代码值。每个字符都是System.Char结构(一个值类型)的实例。

14.2 System.String 类型

在任何应用程序中,System.String都是用得最多的类型之一。一个 String 代表一个不可变(immutable)的顺序字符集。String类型直接派生自Object,所以是引用类型。因此,String对象(它的字符数组)总是存在于堆上,永远不会跑到线程栈。

14.2.1 构造字符串

C# 不允许使用 new 操作符从字面值字符串构造String对象:

1
2
3
4
5
6
7
8
using System;

public static class Program {
public static void Main() {
String s = new String("Hi there."); // 错误
Console.WriteLine(s);
}
}

最后,C# 提供了一种特殊的字符串声明方式。采用这种方式,引号之间的所有字符会都被视为字符串的一部分。这种特殊声明称为”逐字字符串“(verbatim string),通常用于指定文件或目录的路径,或者与正则表达式配合使用。以下代码展示了如何使用和不使用逐字字符串字符(@)来声明同一个字符串:

1
2
3
4
5
// 指定应用程序路径
String file = "C:\\Windows\\System32\\Notepad.exe";

// 使用逐字字符串指定应用程序路径
String file = @"C:\Windows\System32\Notepad.exe";

14.2.2 字符串是不可变的

String 对象最重要的一点就是不可变(immutable)。使字符串不可变有几方面的好处。首先,它允许在一个字符串上执行各种操作,而不实际地更改字符串

字符串不可变还意味着在操纵或访问字符串时不会发生线程同步问题。

此外,CLR 可通过一个String对象共享多个完全一致的String内容。这样能减少系统中的字符串数量——从而节省内存——这就是所谓的”字符串留用“(string interning)

14.2.3 比较字符串

如何执行语言文化正确的比较

现在重点讲述执行语言文化正确的比较。.NET Framework使用System.Globalization.CultureInfo类型表示”语言/国家”对。每个线程关联了两个特殊属性,CurrentUICultureCurrentCulture属性。CurrentUICulture属性用于显示资源,例如GUI或Web窗体应用程序中的UI元素。CurrentCulture属性用于数字和日期格式化,字符串大小写转换以及字符串比较。

CultureInfo对象内部的一个字段引用了一个System.Globalization.CompareInfo对象,该对象封装了语言文化的字符排序表信息(根据 Unicode 标准的定义)。以下代码演示了序号比较和对语言文化敏感的比较的区别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void Main() {
String s1 = "Strasse";
String s2 = "Straße";
Boolean eq;

// Compare 返回非零值 ①
eq = String.Compare(s1, s2, StringComparison.Ordinal) == 0;
Console.WriteLine("Ordinal comparison:'{0}' {2} '{1}'", s1, s2, eq ? "==" : "!=");

// 面向在德国(DE)说德语(de)的人群,
// 正确地比较字符串
CultureInfo ci = new CultureInfo("de-DE");

// Compare 返回零值
eq = String.Compare(s1, s2, true, ci) == 0;
Console.WriteLine("Cultural comparison:'{0}' {2} '{1}'", s1, s2, eq ? "==" : "!=");
}

14.2.4 字符串留用

在程序中,如果将同一个字符串赋值给不同的字符串引用,会导致系统多次分配内存空间,浪费宝贵的内存且降低系统性能。为了改善性能,.net引入了字符串拘留池机制。

CLR 初始化时会创建一个内部哈希表。在这个表中,键(key)是字符串,而值(value)是对托管堆中的String对象的引用。

String类提供了两个方法,便于你访问这个内部哈希表:

1
2
public static String Intern(String str);
public static String IsInterned(String str);

第一个方法 Intern 获取一个 String, 获得它的哈希码,并在内部哈希表中检查是否有相匹配的。如果存在完全相同的字符串,就返回对现有 String 对象的引用。如果不存在完全相同的字符串,就创建字符串的副本,将副本添加到内部哈希表中,返回对该副本的引用。如果应用程序不再保持对原始String对象的引用,垃圾回收器就可释放那个字符串的内存。

IsInterned 方法也获取一个 String,并在内部哈希表中查找它。如果哈希表中有匹配的字符串,IsInterned就返回对这个留用(interned)字符串对象的引用。但如果没有,IsInterned会返回null,不会将字符串添加到哈希表中。

14.2.5 字符串池

编译器在处理源代码时会将字面值字符串嵌入托管模块的元数据。为了减小文件大小,编译器将单个字符串的多个实例合并成一个实例,能显著减少模块的大小。这个技术被称为字符串池,能提升字符串性能。

14.2.6 检查字符串中的字符和文本元素

14.2.7 其他字符串操作

还可利用String类型提供的一些方法来复制整个字符串或者它的一部分。表 14-1 总结了这些方法。

表 14-1 用于复制字符串的方法

成员名称 方法类型 说明
Clone 实例 返回对同一个对象(this)的引用。能这样做是因为String对象不可变(immutable)。该方法实现了 StringICloneable接口
Copy 静态 返回指定字符串的新副本。该方法很少用,它的存在只是为了帮助一些需要把字符串当作 token 来对待的应用程序。通常,包含相同字符内容的多个字符串会被“留用”(intern)为单个字符串。该方法创建新字符串对象,确保即时字符串包含相同字符内容,引用(指针)也有所不同
CopyTo 实例 将字符串中的部分字符复制到一个字符数组中
Substring 实例 返回代表原始字符串一部分的新字符串
ToString 实例 返回对同一个对象(this)的引用

14.3 高效率构造字符串

由于String类型代表不可变字符串,所以 FCL 提供了 System.Text.StringBuilder 类型对字符串和字符进行高效动态处理,并返回处理好的String对象。

从逻辑上说,StringBuilder对象包含一个字段,该字段引用了由Char结构构成的数组。可利用StringBuilder的各个成员来操纵该字符数组,高效率地缩短字符串或更改字符串中的字符。如果字符串变大,超过了事先分配的字符数组大小,StringBuilder会自动分配一个新的、更大的数组,复制字符,并开始使用新数组。前一个数组被垃圾回收。

StringBuilder 对象构造好字符串后,调用StringBuilderToString方法即可将StringBuilder的字符数组“转换”成String

下面解释了StringBuilder类的关键概念。

  • 最大容量
    一个Int32值,指定了能放到字符串中的最大字符数。默认值是Int32.MaxValue(约 20 亿)。一般不用更改这个值。构造好之后,这个StringBuilder的最大容量就固定下来了,不能再变。

  • 容量
    一个Int32值,指定了由StringBuilder维护的字符数组的长度。默认为16。如果事先知道要在这个StringBuilder中放入多少字符,那么构造StringBuilder对象时应该自己设置容量。
    向字符数组追加字符时,StringBuilder会检测数组会不会超过设定的容量。如果会,StringBuilder会自动倍增容量字段,用新容量来分配新数组,并将原始数组的字符复制到新数组中。随后,原始数组可以被垃圾回收。数组动态扩容会损害性能。要避免就要设置一个合适的初始容量。

  • 字符数组
    一个由Char 结构构成的数组,负责维护“字符串”的字符内容。字符数总是小于或等于“容量”和“最大容量”值。可用StringBuilderLength属性来获取数组中已经使用的字符数。。

14.3.2 StringBuilder的成员

String不同,StringBuilder代表可变(mutable)字符串。也就是说,StringBuilder的大多数成员都能更改字符数组的内容,同时不会造成在托管堆上分配新对象。StringBuilder只有以下两种情况才会分配新对象。

  • 动态构造字符串,其长度超过了设置的“容量”。
  • 调用StringBuilderToString方法。

14.4 获取对象的字符串表示:ToString

任何类型要想提供合理的方式获取对象当前值的字符串表示,就应重写ToString方法。

14.4.1 指定具体的格式和语言文化

为了使调用者能选择格式和语言文化,类型应该实现 System.IFormattable接口:

1
2
3
public interface IFormattable {
String ToString(String format, IFormatProvider formatProvider);
}

IFormattableToString方法获取两个参数。第一个是format,这个特殊字符串告诉方法应该如何格式化对象。第二个是formatProvider,是实现了System.IFormatProvider接口的一个类型的实例。

FCL 只有 3 个类型实现了IFormatProvider接口。第一个是前面解释过的CultureInfo。另外两个是NumberFormatInfoDateTimeFormatInfo

14.4.2 将多个对象格式化成一个字符串

但有时需要构造由多个已格式化对象构成的字符串。例如,以下字符串由一个日期、一个人名和一个年龄构成:

1
2
String s = String.Format("On {0}, {1} is {2} years old.", new DateTime(2012, 4, 22, 14, 35, 5), "Aidan", 9);
Console.WriteLine(s);

On 4/22/2012 2:35:05 PM, Aidan is 9 years old.

在内部,Format方法会调用每个对象的ToString方法来获取对象的字符串表示。返回的字符串依次连接到一起,并返回最终的完整字符串。

14.6 编码:字符和字节的相互转换

14.7 安全字符串

String 对象可能包含敏感数据,如用户密码或信用卡资料。String对象在内存中包含字符数组,可能被不安全或非托管的代码利用。 使用 SecureString 类可以加密字符串,防止恶意代码获取敏感信息。SecureString对象内存可以被确定性地销毁,确保敏感信息不被泄露。 SecureString对象在回收后,加密字符串的内容将不再存在于内存中。