6.2 重温字符类型
在介绍了 Unicode 之后,让我们回到本章的真正主题,即 Object Pascal 语言如何管理字符和字符串。我在第 2 章中介绍了 Char 数据类型,并提到了 Character 单元中的一些辅助函数。现在,你已经对 Unicode 有了更好的理解,值得重温这部分内容并进一步了解其中的细节。
首先,Char类型并非总是代表Unicode码点。实际上,Char数据类型的每个元素使用2字节。虽然 Char 确实表示 Unicode 基本多语言平面(BMP)中元素的码点,但Char也可以是一对代理值的一部分,代表一个码点。
从技术上讲,有一种不同的类型可以直接用于表示任何Unicode码点,这就是UCS4Char类型,它使用4字节来表示一个值。这种类型很少使用,因为通常很难证明所需的额外内存是合理的,但是,正如你很快会看到的,字符单元(接下来会介绍)也包含了对这种数据类型的一些操作。
回到Char类型。请记住,这是一个序数类型,因此具有序列的概念,并提供了Ord、Inc、Dec、High和Low等代码操作。大多数扩展操作,包括特定的类型助手,不属于基本系统RTL单元,而是需要包含Character单元。
6.2.1 Character单元中的Unicode操作
大多数Unicode字符(当然还有Unicode字符串)的具体操作定义在一个名为System.Character的特殊单元中。该单元定义了Char类型的TCharHelper类助手,允许您直接对该类型的变量应用操作。
注解: Character单元还定义了yige TCharacter记录,它基本上是一组静态类函数的集合,再加上一些映射到这些方法的全局例程。这些都是较老的、已弃用的函数。在Unicode级别上处理Char类型的首选方式是使用类助手。
该单元还定义了两个有趣的枚举类型。第一个称为TUnicodeCategory
,映射了各种字符的大类,如控制符、空格、大写或小写字母、十进制数字、标点符号、数学符号等。第二个枚举称为TUnicodeBreak
,定义了各种空格(是的,空格不止一种)、连字符和分隔符的族。如果你习惯于 ASCII 操作,这将是一个很大的变化。首先,Unicode 中的数字不仅仅是 0 到 9 之间的字符;空格也不仅限于 #32 字符;256 元素字母表中的许多其他假设也是如此(简单得多)。
Char类型助手有40多个方法,包括许多不同的测试和操作,可用于诸如以下情况:
- 获取字符的数值表示(GetNumericValue)。
- 请求类别(GetUnicodeCategory)或检查字符是否属于各种类别(IsLetterOrDigit、IsLetter、IsDigit、IsNumber、IsControl、IsWhiteSpace、IsPunctuation、IsSymbol和IsSeparator)。我在前面的演示中使用了IsControl操作。
- 检查字符是否为小写或大写(IsLower和IsUpper),或转换其大小写(ToLower和ToUpper)。
- 验证它是否是UTF-16代理对的一部分(IsSurrogate、IsLowSurrogate和IsHighSurrogate),以及以各种方式转换代理对。
- 将其转换为UTF32(ConvertFromUtf32和ConvertToUtf32)和UCS4Char类型(ToUCS4Char)。
- 检查它是否是给定字符列表的一部分(IsInArray)。
请注意,其中一些操作可以应用于整个类型,而不是某个具体变量。在这种情况下,您必须使用Char类型作为前缀调用它们,如下面的第二个代码片段所示。
该演示的一个例子是调用大写和小写操作对 Unicode 元素的影响。事实上,RTL 的经典 UpCase 函数仅适用于 ANSI 表示法中的 26 个英语字符,而对某些具有特定大写表示法的 Unicode 字符则不起作用(并非所有字母都有大写表示法,因此这不是一个通用概念)。
为了测试这种情况,我在 CharTest 示例中添加了以下代码段,尝试将重音字母转换为大写字母:
var
Ch1: Char;
begin
Ch1 := 'ù';
Show('UpCase ù: ' + UpCase(Ch1));
Show('ToUpper ù: ' + Ch1.ToUpper);
传统的UpCase调用不会转换拉丁重音字符,而ToUpper函数可以正常工作:
UpCase ù: ù
ToUpper ù: Ù
Char 类型助手中有许多与 Unicode 相关的功能,例如下面代码中突出显示的功能,它将字符串定义为也包括 BMP(前 64K 个 Unicode 码点)以外的字符。该代码片段也是 CharTest 示例的一部分,对字符串的各种元素进行了一些测试,全部返回 True:
var
Str1: string;
begin
Str1 := '1.' + #9 + Char.ConvertFromUtf32(128) +
Char.ConvertFromUtf32($1D11E);
ShowBool(Str1.Chars[0].IsNumber);
ShowBool(Str1.Chars[1].IsPunctuation);
ShowBool(Str1.Chars[2].IsWhiteSpace);
ShowBool(Str1.Chars[3].IsControl);
ShowBool(Str1.Chars[4].IsSurrogate);
end;
在这种情况下使用的显示函数是一个经过调整的版本:
procedure TForm1.ShowBool(Value: Boolean);
begin
Show(BoolToStr(Value, True));
end;
注解:Unicode码点$1D11E是音乐G谱号符号。
6.2.2 Unicode字符字面量
我们已经在多个示例中看到,可以将单个字符字面量或字符串字面量赋值给字符串类型的变量。一般来说,使用带有 # 前缀的字符数字表示法非常简单。但也有一些例外情况。为了向后兼容,纯字符字面量会根据上下文进行转换。考虑以下对数值128进行简单赋值的代码,该数值表示使用欧元货币符号(€):
var
Str1: string;
begin
Str1 := #$80;
这段代码不符合 Unicode 标准,因为该符号的码点是 $8364 。事实上,这个数值并不是来自官方的 ISO 编码页,而是微软为 Windows 专门实现的。为了方便将现有代码移至 Unicode,Object Pascal 编译器可以将 2 位字符串字面量视为 ANSI 字符(这可能取决于你的实际编码页)。令人惊讶的是,如果将该值转换为字符,并再次显示,数字表示法将变为正确的表示法。因此,通过执行语句:
Show(Str1 + ' - ' + IntToStr(Ord(Str1[1])));
我将得到输出:
€ - 8364
如果你想完全移植旧代码,摆脱基于 ANSI 的字面值,可以使用特殊指令 `$HIGHCHARUNICODE` 来改变编译器的行为。该指令决定编译器如何处理 #$80 和 #$FF 之间的字面值。我前面讨论的是默认选项(OFF)的效果。如果将其打开,同样的程序将产生这样的输出结果:
◎ - 128
该数字被解释为一个实际的 Unicode 码点,输出将包含一个不可打印的控制字符。另一种表达该特定码点(或 #$FFFF 以下的任何 Unicode 码点)的方法是使用四位数符号:
Str1 := #$0080;
无论设置 $HIGHCHARUNICODE 指令与否,都不能将其解释为欧元货币符号。
注解:上述代码和配套演示仅适用于美国或西欧地区。对于其他地区,128 至 255 之间的字符会有不同的解释。
值得注意的是,您可以使用四位数符号来表示远东字符,例如以下两个日文字符:
Str1 := #$3042#$3044;
Show(Str1 + ' - ' + IntToStr(Ord(Str1.Chars[0])) +
' - ' + IntToStr(Ord(Str1.Chars[1])));
将以及它们的整数表示显示为:
あい - 12354 – 12356
您也可以使用超过 #$FFFF 的字面元素,它们将被转换为适当的代理对。
6.2.3 关于1字节Chars呢?
正如我之前提到的,Object Pascal
语言将Char类型映射到WideChar,但它仍定义了AnsiChar
类型,主要是为了与现有代码兼容。一般建议对于一个字节的数据结构使用Byte类型,但是AnsiChar
在进行一字节字符处理时的确很方便。
在多个版本的Delphi中,AnsiChar
在移动平台上并不可用,但从Delphi 10.4
开始,这种数据类型在所有 Delphi 编译器上都能正常使用了。在将数据映射到平台的API或保存到文件时,通常应该避免使用旧的一字节Char类型,即使该类型受支持。到目前为止,使用 Unicode
编码是首选方法。不过,1 字节字符的处理速度确实比 2 字节字符更快,使用的内存也更少。