字符串和文本

字符串是一个刻板的数据结构,在任何地方传递它都有很多重复的过程.它是隐藏信息的完美工具. —Alan Perlis, epigram #34

原文

The string is a stark data structure and everywhere it is passed there is much duplication of process. It is a perfect vehicle for hiding information. —Alan Perlis, epigram #34

在本书中,我们一直在使用Rust的主要文本类型,String,strchar.在第64页的”字符串类型(String Types)”中,我们描述了字符和字符串字面量的语法,并展示了字符串在内存中的表示方式.在本章中,我们将更详细地介绍文本处理.

在这一章当中:

  • 我们为你提供有关Unicode的背景知识,可帮助你了解标准库的设计.

  • 我们描述了char类型,表示单个Unicode代码点.

  • 我们描述Stringstr类型,表示拥有和借用的Unicode字符序列.它们有各种用于构建,搜索,修改和迭代其内容的方法.

  • 我们涵盖Rust的字符串格式化工具,如println!format!宏.你可以编写自己的宏来处理格式化字符串,并扩展它们以支持你自己的类型.

  • 我们概述了Rust的正则表达式支持.

  • 最后,我们讨论为什么Unicode规范化很重要,并展示如何在Rust中执行它.

一些Unicode背景知识(Some Unicode Background)

这本书是关于Rust的,而不是Unicode,它已经有很多关于它的书了.但Rust的字符和字符串类型是围绕Unicode设计的.以下是一些有助于解释Rust的Unicode.

ASCII,Latin-1,和Unicode(ASCII, Latin-1, and Unicode)

Unicode和ASCII匹配所有ASCII的代码点,从00x7f:例如,两者都为字符'*'分配代码点42.类似地,Unicode将00xff分配给与ISO/IEC 8859-1字符集相同的字符,这是用于西欧语言的ASCII的8位超集.Unicode将此范围的代码点称为 Latin-1代码块(Latin-1 code block) ,因此我们将通过更令人回味的名称 Latin-1 来引用ISO/IEC 8859-1.

由于Unicode是Latin-1的超集,因此将Latin-1转换为Unicode甚至不需要表:

  1. fn latin1_to_char(latin1: u8) -> char {
  2. latin1 as char
  3. }

假设代码点落在Latin-1范围内,反向转换也是微不足道的:

  1. fn char_to_latin1(c: char) -> Option<u8> {
  2. if c as u32 <= 0xff {
  3. Some(c as u8)
  4. } else {
  5. None
  6. }
  7. }

UTF-8

Rust的Stringstr类型使用UTF-8编码形式表示文本.UTF-8将字符编码为1到4个字节的序列(图17-1).

图17-1. UTF-8编码.

对格式良好的UTF-8序列有两个限制.首先,只考虑任何给定代码点的最短编码;你不能花四个字节来编码一个适合三个的代码点.此规则确保给定代码点只有一个UTF-8编码.其次,格式良好的UTF-8不能编码从0xd8000xdfff或超过0x10ffff的数字:这些数字要么保留用于非字符目的,要么完全在Unicode范围之外.

图17-2显示了一些示例.

图17-2. UTF-8示例.

请注意,即使蟹表情符号(carb emoji)的编码,其前导字节仅对代码点贡献零,它仍然需要四字节编码:三字节UTF-8编码只能传送16位代码点,而0x1f980是17位长.

这是一个字符串的快速示例,其中包含具有不同长度编码的字符:

  1. assert_eq!("うどん: udon".as_bytes(),
  2. &[0xe3, 0x81, 0x86, // う
  3. 0xe3, 0x81, 0xa9, // ど
  4. 0xe3, 0x82, 0x93, // ん
  5. 0x3a, 0x20, 0x75, 0x64, 0x6f, 0x6e // : udon
  6. ]);

该图显示了UTF-8的一些非常有用的属性:

  • 由于UTF-8将代码点00x7f编码为不超字节00x7f,因此保存ASCII文本的字节范围是有效的UTF-8.如果UTF-8字符串仅包含ASCII字符,则反之亦然:UTF-8编码是有效的ASCII.

对于Latin-1也不是这样:例如,Latin-1将'é'编码为字节0xe9,UTF-8将其解释为三字节编码的第一个字节.

  • 通过查看任何字节的高位.你可以立即判断它是某个字符的UTF-8编码的开头,还是来自其中一个字节.

  • 编码的第一个字节单独通过其前导位告诉你编码的全长.

  • 由于编码不超过四个字节,因此UTF-8处理永远不需要无界循环,这在处理不可信的数据时很好.

  • 在格式良好的UTF-8中,即使从字节中间的随机点开始,你也可以可以清楚地知道字符编码的开始和结束位置.UTF-8的第一个字节和后面的字节总是不同的,因此一个编码不能在另一个编码中间开始.第一个字节确定编码的总长度,因此没有编码可以是另一个的前缀.这有很多好的结果.例如,在UTF-8字符串中搜索ASCII分隔符字符只需要对分隔符的字节进行简单扫描.它永远不会出现在多字节编码的任何部分,因此根本不需要跟踪UTF-8结构.类似地,在另一个字节串中搜索一个字节字符串的算法将在不修改UTF-8字符串的情况下工作,即使有些算法甚至不检查正在搜索的文本的每个字节.

尽管可变宽度编码比固定宽度编码更复杂,但这些特性使得UTF-8的使用比你预期的更加舒适.标准库为你处理大多数方面.

文本方向性(Text Directionality)

而拉丁语,西里尔语和泰语等手稿是从左到右书写的,其他手稿如希伯来语和阿拉伯语则是从右到左书写的.Unicode按照通常写入或读取的顺序存储字符,因此保存希伯来语文本的字符串的初始字节编码将在右侧写入的字符:

  1. assert_eq!("ערב טוב".chars().next(), Some('ע'));

标准库中的一些方法名称使用术语leftright来表示文本的开头和结尾.当我们描述这些函数时,我们将详细说明他们实际做了什么.

字符(char)(Characters (char))

Rustchar是一个包含Unicode代码点的32位值.保证char00xd7ff的范围内,或者在0xe0000x10ffff的范围内;所有创建和操作char值的方法都确保这是真的.char类型实现了CopyClone,以及用于比较,哈希和格式化的所有常用trait.

在下面的描述中,变量ch始终是char类型.

字符分类(Classifying Characters)

char类型具有将字符分类为几个常见类别的方法.这些都是从Unicode中得出它们的定义,如表17-0所示.

方法 描述 示例
ch.is_numeric() 一个数字字符.这包括Unicode常规类别”Number;digit”和”Number;letter”,但不是”Number;other”. '4'.is_numeric()
'ᛮ'.is_numeric()
!'⑧'.is_numeri()
ch.is_alphabetic() 字母字符:Unicode的”字母(Alphabetic)”派生属性. 'q'.is_alphabetic()
'七'.is_alphabetic()
ch.is_alphanumeric() 数字或字母,如上所定义 '9'.is_alphanumeric()
'饂'.is_alphanumeric()
!'*'.is_alphanumeric()
ch.is_whitespace() 空白字符:Unicode字符属性”WSpace=Y”. ' '.is_whitespace()
'\n'.is_whitespace()
'\u{A0}'.is_whitespace()
ch.is_control() 控制字符:Unicode的”Other,control”常规类别. '\n'.is_control()
'\u{85}'.is_control()

处理数字(Handling Digits)

对于处理数字,你可以使用以下方法:

  • ch.to_digit(radix)决定ch是否为基数radix的数字.如果是,则返回Some(num),其中numu32.否则,它返回None.这仅识别ASCII数字,而不识别char::is_numeric所涵盖的更广泛的字符类.radix参数的范围为2到36.对于大于10的基数,任一情况的ASCII字母都被视为数字,值为10到35.

  • 如果可能,自由函数std::char::from_digit(num, radix)u32数字值num转换为char.如果num可以用radix表示为单个数字,from_digit返回Some(ch),其中ch是数字.当radix大于10时,ch可以是小写字母.否则,它返回None.

这与to_digit相反.如果std::char::from_digit(num, radix)Some(ch),则ch.to_digit(radix)Some(num).如果ch是ASCII数字或小写字母,则反之亦然.

  • ch.is_digit(radix),如果ch是基数radix的ASCII数字,则返回true.这相当于ch.to_digit(radix) != None.

所以,例如:

  1. assert_eq!('F'.to_digit(16), Some(15));
  2. assert_eq!(std::char::from_digit(15, 16), Some('f'));
  3. assert!(char::is_digit('f', 16));

字符的大小写转换(Case Conversion for Characters)

处理字符案例:

  • ch.is_lowercase()ch.is_uppercase()指示ch是小写字母还是大写字母.它们遵循Unicode的小写(Lowercase)和大写(Uppercase)派生属性,因此它们涵盖非拉丁字母表,如希腊语和西里尔语,并为ASCII提供预期的结果.

  • ch.to_lowercase()ch.to_uppercase()返回根据Unicode默认大小写转换算法生成ch等效的大写和小写字符的字符的迭代器:

  1. let mut upper = 's'.to_uppercase();
  2. assert_eq!(upper.next(), Some('S'));
  3. assert_eq!(upper.next(), None);

这些方法返回迭代器而不是单个字符,因为Unicode中的大小写转换并不总是一对一的过程:

  1. // The uppercase form of the German letter "sharp S" is "SS":
  2. let mut upper = 'ß'.to_uppercase();
  3. assert_eq!(upper.next(), Some('S'));
  4. assert_eq!(upper.next(), Some('S'));
  5. assert_eq!(upper.next(), None);
  6. // Unicode says to lowercase Turkish dotted capital 'İ' to 'i'
  7. // followed by `'\u{307}'`, COMBINING DOT ABOVE, so that a
  8. // subsequent conversion back to uppercase preserves the dot.
  9. let ch = 'İ'; // `'\u{130}'`
  10. let mut lower = ch.to_lowercase();
  11. assert_eq!(lower.next(), Some('i'));
  12. assert_eq!(lower.next(), Some('\u{307}'));
  13. assert_eq!(lower.next(), None);

为方便起见,这些迭代器实现了std::fmt::Displaytrait,因此你可以将它们直接传递给println!write!宏.

转换为整数和从整数转换(Conversions to and from Integers)

Rust的as运算符将char转换为任何整数类型,静默掩盖任何高位:

  1. assert_eq!('B' as u32, 66);
  2. assert_eq!('饂' as u8, 66); // upper bits truncated
  3. assert_eq!('二' as i8, -116); // same

as运算符将任何u8值转换为char,而char也实现From<u8>,但更宽的整数类型可以表示无效的代码点,因此对于那些必须使用std::char::from_u32,它返回Option<char>:

  1. assert_eq!(char::from(66), 'B');
  2. assert_eq!(std::char::from_u32(0x9942), Some('饂'));
  3. assert_eq!(std::char::from_u32(0xd800), None); // reserved for UTF-16

String和str(String and str)

Rust的Stringstr类型保证只保留格式良好的UTF-8.该库通过限制你创建Stringstr值的方式以及可以对它们执行的操作来确保这一点,这样在引入时值就是格式良好的,并且在你使用它们时保持这样.他们所有的方法都保护了这个保证:没有安全操作可以引入格式错误的UTF-8.这简化了与文本一起使用的代码.

Rust将文本处理方法放在strString上,具体取决于方法是否需要可调整大小的缓冲区,或者只是使用文本就可以了.由于String解引用&str,因此str上定义的每个方法也可以在String上直接使用.本节介绍两种类型的方法,按粗略函数分组.

这些方法按字节偏移量索引文本,并以字节为单位测量其长度,而不是字符.实际上,考虑到Unicode的性质,按字符索引并不像看起来那么有用,字节偏移更快更简单.如果你试图使用一个位于某个字符的UTF-8编码中间的字节偏移量,该方法会发生恐慌,因此你不能以这种方式引入格式错误的UTF-8.

String被实现为Vec<u8>的包装器,确保向量的内容始终是格式良好的UTF-8.Rust永远不会更改String以使用更复杂的表示,因此你可以假设String共享Vec的性能特征.

在这些解释中,以下变量具有给定的类型:

变量 推定类型
string String
slice &str或对其解引用的东西,如StringRc<String>
ch char
n usize,长度
i,j usize,字节偏移量
range usize字节偏移量范围,或者完全限制为i..j,或者部分地限制为i..,..j,或..
pattern 任何模式类型:char,String,&str,&[char],或FnMut(char) -> bool

我们在第402页的”搜索文本的模式(Patterns for Searching Text)”中描述模式类型.

创建字符串值(Creating String Values)

有几种常见的方法可以创建String值:

  • String::new()返回一个新的空字符串.它没有堆分配的缓冲区,但将根据需要分配一个缓冲区.

  • String::with_capacity(n)返回一个新的空字符串,其中预分配的缓冲区至少保存n个字节.如果你知道预先构建的字符串的长度,则此构造函数允许你从一开始就正确地获取缓冲区大小,而不是在构建字符串时调整缓冲区的大小.如果字符串的长度超过n个字节,字符串仍然会根据需要增长缓冲区.与向量一样,字符串具有capacity,reserveshrink_to_fit方法,但通常默认的分配逻辑很好.

  • slice.to_string()分配一个新的String,其内容是slice的副本.我们在本书中一直使用像"literal text".to_string()这样的表达式来从字符串字面量中创建String.

  • iter.collect()通过连接迭代器的项来构造字符串,这些项可以是char.&strString值.例如,要从字符串中删除所有空白,你可以编写:

  1. let spacey = "man hat tan";
  2. let spaceless: String =
  3. spacey.chars().filter(|c| !c.is_whitespace()).collect();
  4. assert_eq!(spaceless, "manhattan");

使用collect这种方式利用了Stringstd::iter::FromIteratortrait的实现.

  • &str类型无法实现Clone:trait需要在&Tclone以返回T值,但str是无大小的.但是,&str确实实现了ToOwned,它允许实现者指定其拥有的等价物,因此slice.to_owned()返回slice的副本作为新分配的String.

简单检查(Simple Inspection)

这些方法从字符串切片获取基本信息:

  • slice.len()slice的长度,以字节为单位.

  • slice.is_empty(),如果slice.len() == 0,则为true.

  • slice[range]返回借用给定slice部分的切片.部分有界和无界范围都可以:例如:

  1. let full = "bookkeeping";
  2. assert_eq!(&full[..4], "book");
  3. assert_eq!(&full[5..], "eeping");
  4. assert_eq!(&full[2..4], "ok");
  5. assert_eq!(full[..].len(), 11);
  6. assert_eq!(full[5..].contains("boo"), false);
  • 你不能使用单个位置索引字符串切片,如slice[i].在给定的字节偏移处获取单个字符有点笨拙:你必须在切片上生成char迭代器,并要求它解析一个字符的UTF-8:
  1. let parenthesized = "Rust (饂)";
  2. assert_eq!(parenthesized[6..].chars().next(), Some('饂'));

但是,你应该很少需要这样做.Rust有更好的方法来迭代切片,我们在第403页的”迭代文本(Iterating over Text)”中对此进行了描述.

  • slice.split_at(i)返回从slice借来的两个共享切片的元组:直到字节偏移量i的部分,以及它后面的部分.换句话说,这返回(slice[..i], slice[i..]).

  • slice.is_char_boundary(i),如果字节偏移量i落在字符边界之间,则为true,因此适合作为切片的偏移量.

当然,切片可以比较相等性,是有序的和可哈希的.有序比较只是将字符串视为Unicode代码点序列,并按字典顺序对它们进行比较.

追加和插入文本(Appending and Inserting Text)

以下方法向String添加文本:

  • string.push(ch)将字符ch追加到string末尾.

  • string.push_str(slice)追加slice的全部内容.

  • string.extend(iter)将迭代器iter生成的项追加到字符串中.迭代器可以生成char,strString值.这些是Stringstd::iter::Extend实现:

  1. let mut also_spaceless = "con".to_string();
  2. also_spaceless.extend("tri but ion".split_whitespace());
  3. assert_eq!(also_spaceless, "contribution");
  • string.insert(i, ch)string中的字节偏移量i处插入单个字符ch.这需要在i之后移动任何字符以便为ch腾出空间,因此以这种方式构建字符串可能需要时间与字符串长度二次方.

  • string.insert_str(i, slice)slice执行相同操作,具有相同的性能警告.

String实现了std::fmt::Write,这意味着write!writeln!宏可以将格式化文本追加到String:

  1. use std::fmt::Write;
  2. let mut letter = String::new();
  3. writeln!(letter, "Whose {} these are I think I know", "rutabagas")?;
  4. writeln!(letter, "His house is in the village though;")?;
  5. assert_eq!(letter, "Whose rutabagas these are I think I know\n\
  6. His house is in the village though;\n");

因为write!writeln!设计用于写到输出流,它们返回一个Result,如果你忽略,Rust就会抱怨.这段代码使用?运算符来处理它,但是写入String实际上是绝对可靠的,因此在这种情况下,调用.unwrap()也可以. 由于String实现了Add<&str>AddAssign<&str>,你可以编写如下代码:

  1. let left = "partners".to_string();
  2. let mut right = "crime".to_string();
  3. assert_eq!(left + " in " + &right, "partners in crime");
  4. right += " doesn't pay";
  5. assert_eq!(right, "crime doesn't pay");

当应用于字符串时,+运算符通过值获取其左操作数,因此它实际上可以重用该String作为添加的结果.因此,如果左操作数的缓冲区足够大以保存结果,则不需要分配.

在一个不幸的缺乏对称性的情况下,+的左操作数不能是&str,所以你不能写:

  1. let parenthetical = "(" + string + ")";

你必须这样写:

  1. let parenthetical = "(".to_string() + string + ")";

但是,这种限制不鼓励从末尾向后构建字符串.这种方法表现不佳,因为文本必须重复地移向缓冲区的末尾.

然而,通过追加小块来从头到尾构建字符串是有效的.String表现得像向量的行为方式,当需要更大容量时,总是至少使其缓冲区的大小加倍.如第62页上的”一个元素一个元素地构建向量(Building Vectors Element by Element)”中所述,使得重新复制(recopying)开销与最终的大小成正比.即便如此,使用String::with_capacity来创建具有正确缓冲区大小的字符串以避免调整大小,并且可以减少对堆分配器的调用次数.

删除文本(Removing Text)

String有一些删除文本的方法(这些方法不会影响字符串的容量;如果需要释放内存,请使用shrink_to_fit):

  • string.clear()将字符串重置为空字符串.

  • string.truncate(n)丢弃字节偏移量n之后的所有字符,保留string的长度最多为n.如果string短于n个字节,则无效.

  • string.pop()string中删除最后一个字符(如果有),并将其作为Option<char>返回.

  • string.remove(i)从字符串偏移i中删除字符并返回它,将任何后续字符向前移动.这需要时间与后续字符数线性.

  • string.drain(range)返回给定字节索引范围内的迭代器,并在删除迭代器后删除字符.范围后的字符向前移动:

  1. let mut choco = "chocolate".to_string();
  2. assert_eq!(choco.drain(3..6).collect::<String>(), "col");
  3. assert_eq!(choco, "choate");

如果你只想删除范围,可以立即删除迭代器,而不从中拉取任何项:

  1. let mut winston = "Churchill".to_string();
  2. winston.drain(2..6);
  3. assert_eq!(winston, "Chill");

搜索和迭代约定(Conventions for Searching and Iterating)

Rust用于搜索文本和迭代文本的标准库函数遵循一些命名约定,以便更容易记住:

  • 大多数操作从头到尾处理文本,但名称以r开头的操作从尾到头工作.例如,rsplitsplit的从尾到头(end-to-start)版本.在某些情况下,改变方向不仅会影响值的生成顺序,还会影响值本身.有关此示例,请参见图17-3中的图表.

  • 名称以n结尾的迭代器将自己限制为给定数量的匹配.

  • 名称以_indices结尾的迭代器与它们通常的迭代值一起产生它们出现的切片中的字节偏移量.

标准库不为每个操作提供所有组合.例如,许多操作不需要n变量,因为它很容易提前结束迭代.

搜索文本的模式(Patterns for Searching Text)

当标准库函数需要搜索,匹配,拆分或修剪(trim)文本时,它会接受几种不同的类型来表示要查找的内容:

  1. let haystack = "One fine day, in the middle of the night";
  2. assert_eq!(haystack.find(','), Some(12));
  3. assert_eq!(haystack.find("night"), Some(35));
  4. assert_eq!(haystack.find(char::is_whitespace), Some(3));

这些类型称为 模式(patterns) ,大多数操作都支持它们:

  1. assert_eq!("## Elephants"
  2. .trim_left_matches(|ch: char| ch == '#' || ch.is_whitespace()),
  3. "Elephants");

标准库支持四种主要模式:

  • char作为模式,匹配该字符.

  • String&str&&str作为模式,匹配等于模式的子字符串.

  • FnMut(char) -> bool闭包作为模式,匹配闭包返回true的单个字符.

  • &[char]作为模式(不是&str,而是char值的切片)匹配列表中出现的任何单个字符.请注意,如果将列表写为数组字面量,则可能需要使用as表达式来获取正确的类型:

  1. let code = "\t function noodle() { ";
  2. assert_eq!(code.trim_left_matches(&[' ', '\t'] as &[char]),
  3. "function noodle() { ");
  4. // Shorter equivalent: &[' ', '\t'][..]

否则,Rust会被固定大小的数组类型&[char; 2]所迷惑,遗憾的是这不是模式类型.

在库自己的代码中,模式是实现std::str::Patterntrait的任何类型.Pattern的细节还不稳定,所以你不能在稳定(stable)的Rust中为你自己的类型实现它,但是在未来允许正则表达式和其他复杂模式的门是敞开的.Rust确保现在支持的模式类型将来继续有效.

搜索和替换(Searching and Replacing)

Rust有一些方法可以在切片中搜索模式,并可能用新文本替换它们:

  • slice.contains(pattern),如果slice包含pattern匹配,则返回true.

  • slice.starts_with(pattern)slice.ends_with(pattern),如果slice的初始或最终文本与pattern匹配则返回true:

  1. assert!("2017".starts_with(char::is_numeric));
  • slice.find(pattern)slice.rfind(pattern),如果slice包含pattern的匹配,返回some(i),其中i是模式出现的字节偏移量.find方法返回第一个匹配,rfind最后一个匹配:
  1. let quip = "We also know there are known unknowns";
  2. assert_eq!(quip.find("know"), Some(8));
  3. assert_eq!(quip.rfind("know"), Some(31));
  4. assert_eq!(quip.find("ya know"), None);
  5. assert_eq!(quip.rfind(char::is_uppercase), Some(0));
  • slice.replace(pattern, replacement)返回一个新的String,它通过用replacement替换所有匹配pattern而形成:
  1. assert_eq!("The only thing we have to fear is fear itself"
  2. .replace("fear", "spin"),
  3. "The only thing we have to spin is spin itself");
  4. assert_eq!("`Borrow` and `BorrowMut`"
  5. .replace(|ch:char| !ch.is_alphanumeric(), ""),
  6. "BorrowandBorrowMut");
  • slice.replacen(pattern, replacement, n)也是这样,但最多替换前n个匹配.

迭代文本(Iterating over Text)

标准库提供了几种迭代切片文本的方法.图17-3显示了一些示例.

你可以将splitmatch系列视为彼此的补充:拆分是匹配之间的范围.

图17-3.一些迭代切片的方式.

对于某些类型的模式,从尾到头工作可以改变生成的值;例如,请参见图中"rr"模式的拆分.始终匹配单个字符的模式不能以这种方式运行.当迭代器在任一方向上生成相同的项的集时(也就是说,只有顺序受影响时),迭代器是DoubleEndedIterator,这意味着你可以应用其rev方法以其他顺序迭代,并从任意端拉取项:

  • slice.chars()返回一个在slice的字符上的迭代器.

  • slice.char_indices()返回slice字符及其字节偏移量的迭代器:

  1. assert_eq!("élan".char_indices().collect::<Vec<_>>(),
  2. vec![(0, 'é'), // has a two-byte UTF-8 encoding
  3. (2, 'l'),
  4. (3, 'a'),
  5. (4, 'n')]);

请注意,这不等同于.chars().enumerate(),因为它提供在切片内每个字符的字节偏移量,而不仅仅是对字符进行编号.

  • slice.bytes()返回一个在slice的各个字节上的迭代器,公开UTF-8编码:

  • slice.lines()返回slice行上的迭代器.行以"\n""\r\ n"结尾.生成的每个项都是从slice借来的&str.这些项目不包括行的终止字符.

  • slice.split(pattern)返回切片由pattern匹配的分隔的部分上的迭代器.这会在紧邻的匹配之间产生空字符串,也会在slice的开头和结尾处匹配的产生.

  • slice.rsplit(pattern)方法是相同的,但是从尾到头扫描slice,按顺序生成匹配.

  • slice.split_terminator(pattern)slice.rsplit_terminator(pattern)类似,不同之处在于模式被视为终结符,而不是分隔符:如果patternslice的右端匹配,则迭代器不会生成表示空字符串的切片,该匹配与切片末尾之间的空字符串,如splitrsplit一样.例如:

  1. // The ':' characters are separators here. Note the final "".
  2. assert_eq!("jimb:1000:Jim Blandy:".split(':').collect::<Vec<_>>(),
  3. vec!["jimb", "1000", "Jim Blandy", ""]);
  4. // The '\n' characters are terminators here.
  5. assert_eq!("127.0.0.1 localhost\n\
  6. 127.0.0.1 www.reddit.com\n"
  7. .split_terminator('\n').collect::<Vec<_>>(),
  8. vec!["127.0.0.1 localhost",
  9. "127.0.0.1 www.reddit.com"]);
  10. // Note, no final ""!
  • slice.splitn(n, pattern)slice.rsplitn(n, pattern)类似于splitrsplit,只是它们将字符串拆分为最多n个切片,在模式的第一个或最后n-1个匹配处.

  • slice.split_whitespace()返回一个在slice的空白分隔部分上的迭代器.多个空白字符的运行被视为单个分隔符.尾随空白被忽略.这使用与char::is_whitespace相同的 空白(whitespace) 定义:

  1. let poem = "This is just to say\n\
  2. I have eaten\n\
  3. the plums\n\
  4. again\n";
  5. assert_eq!(poem.split_whitespace().collect::<Vec<_>>(),
  6. vec!["This", "is", "just", "to", "say",
  7. "I", "have", "eaten", "the", "plums",
  8. "again"]);
  • slice.matches(pattern)返回切片中与pattern匹配的上迭代器.slice.rmatches(pattern)是相同的,但是从尾到头迭代.

  • slice.match_indices(pattern)slice.rmatch_indices(pattern)是相似的,只是生成的项是(offset, match)对,其中offset是匹配开始的字节偏移量,match是匹配的切片.

Trimming(Trimming)

trim 字符串是从字符串的开头或结尾删除文本,通常是空白.它通常用于清理文件中的输入读取,其中用户可能为了易读性而缩进文本,或者意外地在行上留下尾随空格.

  • slice.trim()返回slice的子切片,省略任何前导和尾随空格.slice.trim_left()仅省略前导空格,slice.trim_right()仅尾随空格:
  1. assert_eq!("\t*.rs ".trim(), "*.rs");
  2. assert_eq!("\t*.rs ".trim_left(), "*.rs ");
  3. assert_eq!("\t*.rs ".trim_right(), "\t*.rs");
  • slice.trim_matches(pattern)返回slice的子切片,它从开头和结尾省略了pattern的所有匹配. trim_left_matchestrim_right_matches方法仅对前导或尾随匹配执行相同操作:
  1. assert_eq!("001990".trim_left_matches('0'), "1990");

请注意,这些方法名称中的术语leftright分别始终指切片的开头和结尾,而不管它们保存的文本的方向性如何.

字符串的大小写转换(Case Conversion for Strings)

方法slice.to_uppercase()slice.to_lowercase()返回一个新分配的字符串,其中包含转换为大写或小写的slice文本.结果可能与切片的长度不同;有关详细信息,请参见第396页的”字符大小写转换(Case Conversion for Characters)”.

从字符串解析其他类型(Parsing Other Types from Strings)

Rust为从字符串中解析值和生成值的文本表示都提供了标准trait.

如果类型实现了std::str::FromStrtrait,那么它提供了一种从字符串切片中解析值的标准方式:

  1. pub trait FromStr: Sized {
  2. type Err;
  3. fn from_str(s: &str) -> Result<Self, Self::Err>;
  4. }

所有常见的机器类型都实现了FromStr:

  1. use std::str::FromStr;
  2. assert_eq!(usize::from_str("3628800"), Ok(3628800));
  3. assert_eq!(f64::from_str("128.5625"), Ok(128.5625));
  4. assert_eq!(bool::from_str("true"), Ok(true));
  5. assert!(f64::from_str("not a float at all").is_err());
  6. assert!(bool::from_str("TRUE").is_err());

std::net::IpAddr类型,一个包含IPv4或IPv6互联网地址的enum,也实现了FromStr:

  1. use std::net::IpAddr;
  2. let address = IpAddr::from_str("fe80::0000:3ea9:f4ff:fe34:7a50")?;
  3. assert_eq!(address,
  4. IpAddr::from([0xfe80, 0, 0, 0, 0x3ea9, 0xf4ff, 0xfe34, 0x7a50]));

字符串切片有一个parse方法,可以将切片解析为你喜欢的任何类型,假设它实现了FromStr.与Iterator::collect一样,你有时需要拼出你想要的类型,因此parse并不总是比直接调用from_str更易读:

  1. let address = "fe80::0000:3ea9:f4ff:fe34:7a50".parse::<IpAddr>()?;

将其他类型转换为字符串(Converting Other Types to Strings)

将非文本值转换为字符串有三种主要方法:

  • 具有自然人类可读打印形式的类型可以实现std::fmt::Displaytrait,它允许你在format!宏中使用{}格式说明符:
  1. assert_eq!(format!("{}, wow", "doge"), "doge, wow");
  2. assert_eq!(format!("{}", true), "true");
  3. assert_eq!(format!("({:.3}, {:.3})", 0.5, f64::sqrt(3.0)/2.0),
  4. "(0.500, 0.866)");
  5. // Using `address` from above.
  6. let formatted_addr: String = format!("{}", address);
  7. assert_eq!(formatted_addr, "fe80::3ea9:f4ff:fe34:7a50");

所有Rust的机器数字类型都实现了Display,字符,字符串和切片也是如此.智能指针类型Box<T>,Rc<T>Arc<T>实现Display,如果T本身实现:它们的显示形式只是它们的指示对象.像VecHashMap这样的容器没有实现Display,因为这些类型没有单一的自然人类可读形式.

  • 如果一个类型实现了Display,标准库会自动为它实现std::str::ToStringtrait,当你不需要format!的灵活性时,它的唯一方法to_string可以更方便:
  1. // Continued from above.
  2. assert_eq!(address.to_string(), "fe80::3ea9:f4ff:fe34:7a50")

ToStringtrait早于Display的引入,并且灵活性较低.对于你自己的类型,你通常应该实现Display而不是ToString.

  • 标准库中的每个公有类型都实现了std::fmt::Debug,它接受一个值并将其格式化为字符串,以对程序员有用的方式.使用Debug生成字符串的最简单方法是通过format!宏的{:?}格式说明符:
  1. // Continued from above.
  2. let addresses = vec![address,
  3. IpAddr::from_str("192.168.0.1")?];
  4. assert_eq!(format!("{:?}", addresses),
  5. "[V6(fe80::3ea9:f4ff:fe34:7a50), V4(192.168.0.1)]");

对于任何本身实现DebugT,这都利用了针对Vec<T>Debug的全面实现.Rust的所有集合类型都有这样的实现.

你也应该为自己的类型实现Debug.通常最好让Rust派生一个实现,正如我们在本书前面对Complex类型所做的那样:

  1. #[derive(Copy, Clone, Debug)]
  2. struct Complex { r: f64, i: f64 }

DisplayDebug格式trait只是format!宏及其相关的用于将值格式化为文本中的两个.我们将在第413页的”格式化值(Formatting Values)”中介绍其他内容,并解释如何实现它们.

借用作为其他类似文本的类型(Borrowing as Other Text-Like Types)

你可以通过几种不同的方式借用切片的内容:

  • 切片和String实现AsRef<str>,AsRef<[u8]>,AsRef<Path>AsRef<OsStr>.许多标准库函数使用这些trait作为其参数类型的限制,因此你可以直接将切片和字符串传递给它们,即使它们真正想要的是其他类型.有关更详细的说明,请参见第294页的”AsRef和AsMut(AsRef and AsMut)”.

  • 切片和字符串也实现了std::borrow::Borrow<str>trait.HashMapBTreeMap使用Borrow使String作为表中的键很好地工作,就像[T]::binary_search这样的函数一样.有关详细信息,请参见第296页中的”Borrow和BorrowMut(Borrow and BorrowMut)”.

以UTF-8访问文本(Accessing Text as UTF-8)

获取表示文本的字节有两种主要方式,具体取决于你是要获取字节的所有权还是只是借用它们:

  • slice.as_bytes()将切片的字节借用为&[u8].由于这不是可变引用,因此slice可以假设其字节将保持格式良好的UTF-8.

  • string.into_bytes()获取字符串的所有权,并通过值返回字符串字节的Vec<u8>.这是一个廉价的转换,因为它只是将字符串用作缓冲区的Vec<u8>移交. 由于字符串不再存在,因此不需要字节继续是格式良好的UTF-8,并且调用者可以随意修改Vec<u8>.

从UTF-8数据生成文本(Producing Text from UTF-8 Data)

如果你有一个你认为包含UTF-8数据的字节块,则可以使用几个选项将它们转换为String或切片,具体取决于你希望如何处理错误:

  • str::from_utf8(byte_slice)接受&[u8]字节切片并返回Result:如果byte_slice包含格式正确的UTF-8,则为Ok(&str),否则为错误.

  • String::from_utf8(vec)尝试从通过值传递的Vec<u8>构造字符串.如果vec包含格式良好的UTF-8,from_utf8返回Ok(string),其中string取得vec的所有权用作其缓冲区.不会发生堆分配或文本复制.

如果字节是无效的UTF-8,则返回Err(e)其中eFromUtf8Error错误值.调用e.into_bytes()会返回原始向量vec,因此转换失败时不会丢失:

  1. let good_utf8: Vec<u8> = vec![0xe9, 0x8c, 0x86];
  2. assert_eq!(String::from_utf8(good_utf8).ok(), Some("錆".to_string()));
  3. let bad_utf8: Vec<u8> = vec![0x9f, 0xf0, 0xa6, 0x80];
  4. let result = String::from_utf8(bad_utf8);
  5. assert!(result.is_err());
  6. // Since String::from_utf8 failed, it didn't consume the original
  7. // vector, and the error value hands it back to us unharmed.
  8. assert_eq!(result.unwrap_err().into_bytes(),
  9. vec![0x9f, 0xf0, 0xa6, 0x80]);
  • String::from_utf8_lossy(byte_slice)尝试从&[u8]共享的字节切片构造一个String&str.此转换始终成功,用Unicode替换字符替换任何格式错误的UTF-8.返回值是一个Cow<str>,如果它包含格式良好的UTF-8,则直接从byte_slice借用&str,或者拥有一个新分配的字符串,用替换字符替换了格式错误的字节.因此,当byte_slice格式良好时,不会发生堆分配或复制.我们将在第410页的”延迟分配(Putting Off Allocation)”中更详细地讨论Cow<str>.

  • 如果你知道Vec<u8>包含格式良好的UTF-8,那么你可以调用不安全函数String::from_utf8_unchecked.这简单地将Vec<u8>包装为String并返回它,而不检查字节.你有责任确保没有将错误的UTF-8引入系统,这就是为什么这个功能被标记为unsafe.

  • 类似地,str::from_utf8_unchecked接受&[u8]并将其作为&str返回,而不检查它是否包含格式良好的UTF-8.与String::from _utf8_unchecked一样,你有责任确保这是安全的.

延迟分配(Putting Off Allocation)

假设你希望程序向用户致意.在Unix上,你可以写:

  1. fn get_name() -> String {
  2. std::env::var("USER") // Windows uses "USERNAME"
  3. .unwrap_or("whoever you are".to_string())
  4. }
  5. println!("Greetings, {}!", get_name());

对于Unix用户,这会通过用户名来问候它们.对于Windows用户和的悲惨的未命名用户,它提供了替代的库存文本.

std::env::var函数返回一个String—并且有充分的理由这样做我们不会进入这里.但这意味着替代的库存文本也必须作为String返回.这是令人失望的:当get_name返回一个静态字符串时,根本不需要分配.

问题的结论是有时返回name的值应该是一个拥有的String,有时它应该是一个&'static str,在我们运行程序之前我们无法知道它将是哪一个.这个动态字符提示你考虑使用std::borrow::Cow,这是一种可以保存拥有数据或借用数据的写时克隆(clone-on-write)类型.

正如第300页”在工作中借用和拥有:谦卑的Cow(Borrow and ToOwned at Work: The Humble Cow)”中所解释的那样,Cow<'a, T>是一个包含两种变体的枚举:OwnedBorrowed.Borrowed持有一个引用&'a T,Owned持有&T的拥有版本:对&str来说是String,对&[i32]来说是Vec<i32>,等等.无论是Owned还是Borrowed,Cow<'a, T>总能生成一个&T供你使用.事实上,Cow<'a, T>解引用为&T,表现为一种智能指针.

更改get_name以返回Cow结果,如下所示:

  1. use std::borrow::Cow;
  2. fn get_name() -> Cow<'static, str> {
  3. std::env::var("USER")
  4. .map(|v| Cow::Owned(v))
  5. .unwrap_or(Cow::Borrowed("whoever you are"))
  6. }

如果成功读取"USER"环境变量,则map会将生成的String作为Cow::Owned返回.如果失败,则unwrap_or将其静态&str作为Cow::Borrowed返回.调用可以保持不变:

  1. println!("Greetings, {}!", get_name());

只要T实现了std::fmt::Displaytrait,显示Cow<'a, T>就会产生与显示T相同的结果.

当你可能需要或不需要修改你借用的某些文本时,Cow也很有用.如果不需要更改,你可以继续借用它.但是Cow的同名写时克隆(clone-on-write)行为可以根据需要为你提供所拥有的,可变的值副本.Cowto_mut方法确保CowCow::Owned,必要时应用值的ToOwned实现,然后返回对该值的可变引用.

因此,如果你发现某些用户(但不是所有用户)拥有通过标题称呼他们,你可以说:

  1. fn get_title() -> Option<&'static str> { ...}
  2. let mut name = get_name();
  3. if let Some(title) = get_title() {
  4. name.to_mut().push_str(", ");
  5. name.to_mut().push_str(title);
  6. }
  7. println!("Greetings, {}!", name);

这可能会产生如下输出:

  1. $ cargo run
  2. Greetings, jimb, Esq.!
  3. $

这里有什么好处,如果get_name()返回一个静态字符串而get_title返回None,那么Cow只会将静态字符串一直带到println!.除非真的有必要,否则你已经设法推迟分配,同时仍然编写简单的代码.

由于Cow经常用于字符串,因此标准库对Cow<'a,str>有一些特殊的支持.它提供了来自String&strFromInto转换,因此你可以更简洁地编写get_name:

  1. fn get_name() -> Cow<'static, str> {
  2. td::env::var("USER")
  3. .map(|v| v.into())
  4. .unwrap_or("whoever you are".into())
  5. }

Cow<'a, str>也实现了std::ops::Addstd::ops::AddAssign,所以要将标题添加到名称中,你可以写:

  1. if let Some(title) = get_title() {
  2. name += ", ";
  3. name += title;
  4. }

或者,因为String可以是write!宏的结果:

  1. use std::fmt::Write;
  2. if let Some(title) = get_title() {
  3. write!(name.to_mut(), ", {}", title).unwrap();
  4. }

和以前一样,在尝试修改Cow之前不会发生任何分配.

请记住,并非每个Cow<..., str>都必须是'static:你可以使用Cow借用先前计算的文本,直到需要复制为止.

字符串作为泛型集合(Strings as Generic Collections)

String实现std::default::Defaultstd::iter::Extend:default返回一个空字符串,extend可以将字符,字符串切片或字符串附加到字符串的末尾.这是Rust的其他集合类型(如VecHashMap)为泛型构造模式(如collectpartition)实现的相同trait组合.

&str类型也实现Default,返回一个空切片.这在某些角落的情况下很方便;例如,它允许你为包含字符串切片的结构派生Default.

格式化值(Formatting Values)

在整本书中,我们一直在使用像println!这样的文本格式化宏:

  1. println!("{:.3}μs: relocated {} at {:#x} to {:#x}, {} bytes",
  2. 0.84391, "object",
  3. 140737488346304_usize, 6299664_usize, 64);

这个调用产生下面的输出:

  1. 0.844μs: relocated object at 0x7fffffffdcc0 to 0x602010, 64 bytes

字符串字面量用作输出的模板:模板中的每个{...}都被以下参数之一的格式化形式替换.模板字符串必须是常量,以便Rust可以在编译时根据参数的类型对其进行检查.每个参数都必须使用;否则Rust会报告编译时错误.

几个标准库功能共享这个小语言来格式化字符串:

  • format!宏用它来构建String.

  • println!print!宏将格式化文本写入标准输出流.

  • writeln!write宏将其写入指定的输出流.

  • panic!宏使用它来构建一个(希望提供信息的)终端惊慌的表达式.

Rust的格式化功能是开放式的.通过实现std::fmt模块的格式化trait,你可以来扩展这些宏来支持你自己的类型.你可以使用format_args!宏和std::fmt::Arguments类型使你自己的函数和宏支持格式化语言.

格式化宏总是借用对其参数的共享引用;他们永远不会取得它们的所有权或改变它们.

模板的{...}形式称为 格式化参数(format parameters) ,其格式为 {which:how} .两个部分都是可选的;经常使用{}.

which值选择模板后面的参数应该取的参数的位置.你可以按索引或按名称选择参数.没有which值的参数只是从左到右与参数配对.

how值表示参数应该如何格式化:填充多少,精度,数字基数,等等.如果how存在,则需要前面的冒号.

这里有些例子:

模板字符串 参数列表 结果
"number of {}: {}" "elephants", 19 "number of elephants: 19"
"from {1} to {0}" "the grave", "the cradle" "from the cradle to the grave"
"v = {:?}" vec![0,1,2,5,12,29] "v = [0, 1, 2, 5, 12, 29]"
"name = {:?}" "Nemo" "name = \"Nemo\""
"{:8.2} km/s" 11.186 " 11.19 km/s"
"{:20} {:02x} {:02x}" "adc #42", 105, 42 "adc #42 69 2a"
"{1:02x} {2:02x} {0}" "adc #42", 105, 42 "69 2a adc #42"
"{lsb:02x} {msb:02x} insn="adc #42", lsb=105, "69 2a adc #42"
{insn}" msb=42

如果要在输出中包含'{''}'字符,请将模板中的字符加倍:

  1. assert_eq!(format!("{{a, c}} ⊂ {{a, b, c}}"),
  2. "{a, c} ⊂ {a, b, c}");

格式化文本值(Formatting Text Values)

格式化像&strString这样的文本类型时(char被视为单字符字符串),参数的值how具有多个部分,都是可选的.

  • 文本长度限制(text length limit) .如果它超过这个,Rust会截断你的参数.如果未指定限制,Rust将使用全文.

  • 最小字段宽度(minimum field width) .在任何截断之后,如果你的参数比这个短,Rust会在右侧(默认情况下)用空格(默认情况下)填充它以创建此宽度的字段.如果省略,Rust不会填充你的参数.

  • 对齐(alignment) .如果你的参数需要填充以满足最小字段宽度,它表示文本应放在字段中的位置.<,^>分别将文本放在开头,中间和结尾.

  • 填充(padding) 字符,用在此填充过程中.如果省略,Rust使用空格.如果指定填充字符,则还必须指定对齐方式.

以下是一些示例,说明如何编写内容及其效果.所有都使用相同的八个字符的参数,bookends:

使用的功能 模板字符串 结果
默认 "{}" "bookends"
最小字段宽度 "{:4}"
"{:12}"
"bookends"
"bookends "
文本长度限制 "{:.4}"
"{:.12}"
"book"
"bookends"
字段宽度,长度限制 "{:12.20}"
"{:4.20}"
"{:4.6}"
"{:6.4}"
"bookends "
"bookends"
"booken"
"book "
左对齐,宽度 "{:<12}" "bookends "
居中,宽度 "{:^12}" " bookends "
右对齐,宽度 "{:>12}" " bookends"
填充'=',居中,宽度 "{:=^12}" "==bookends=="
填充'*',右对齐,宽度,限制 "{:*>12.4}" "********book"

Rust的格式化程序对宽度有一种天真的(naïve)理解:它假定每个字符占据一列,不考虑组合字符,半角片假名(half-width katakana),零宽度空间(zero-width spaces)或Unicode的其他混乱现实.例如:

  1. assert_eq!(format!("{:4}", "th\u{e9}"), "th\u{e9} ");
  2. assert_eq!(format!("{:4}", "the\u{301}"), "the\u{301}");

尽管Unicode表示这些字符串都相当于"thé",但Rust的格式化程序并不知道像'\u{301}'这样的字符(结合重音符(COMBINING ACUTE ACCENT))),需要特殊处理.它正确填充第一个字符串,但假设第二个字符串是四列宽并且不添加填充.虽然很容易看出Rust在这种特定情况下如何改进,但所有Unicode脚本的真正多语言文本格式是一项艰巨的任务,最好通过依赖平台的用户界面工具包来处理,或者通过生成HTML和CSS以及让Web浏览器来搞定一切.

除了&strString之外,你还可以将带有文本引用对象的智能指针类型传递格式化宏,如Rc<String>Cow<'a, str>,随便地.

由于文件名路径不一定是格式良好的UTF-8,因此std::path::Path不是一个文本类型;你不能将std::path::Path直接传递给格式化宏.但是,Pathdisplay方法返回一个值,你可以格式化该值,以适合平台的方式:

  1. println!("processing file: {}", path.display());

格式化数字(Formatting Numbers)

当格式化参数具有类似usizef64的数字类型时,参数的how值具有以下部分,所有都是可选的:

  • 填充(padding)对齐(alignment) ,与文本类型一样.

  • +字符,要求始终显示数字的符号,即使参数为正数.

  • #字符,要求显式的基数前缀,如0x0b.请参阅此列表总结的”表示法(notation)”要点.

  • 0字符,通过在数字中包含前导零来要求满足最小字段宽度,而不是通常的填充方法.

  • 最小字段宽度(minimum field width) .如果格式化的数字不超过这个宽度,则Rust会在左侧(默认情况下)使用空格(默认情况下)将其填充为一个给定宽度的字段.

  • 精度(precision) 用于浮点参数,指示Rust在小数点后应包含的位数.Rust必须根据需要进行舍入(rounds)或零扩展(zero-extends)以生成这么多小数位数.如果省略精度,Rust会尝试使用尽可能少的数字准确表示值.对于整数类型的参数,将忽略精度.

  • 表示法(notation) .对于整数类型,对于二进制可以是b,对于八进制可以是o,对于具有小写或大写字母的十六进制,可以是xX.如果包含#字符,则包括显式的Rust风格基数前缀,0b,0o,0x0X.对于浮点类型,eE的基数要求科学记数法,使用归一化系数,使用eE作为指数.如果未指定任何表示法,Rust会以十进制格式数字.

格式化i321234的一些示例:

使用的功能 模板字符串 结果
默认 "{}" "1234"
强制符号 "{:+}" "1234"
最小字段宽度 "{:12}"
"{:2}"
" 1234"
"1234"
符号,宽度 "{:+12}" " +1234"
前导零,宽度 "{:012}" "000000001234"
符号,零,宽度 "{:+012}" "+00000001234"
左对齐,宽度 "{:<12}" "1234 "
居中,宽度 "{:^12}" " 1234 "
右对齐,宽度 "{:>12}" " 1234"
左对齐,符号,宽度 "{:<+12}" "+1234 "
居中,符号,宽度 "{:^+12}" " +1234 "
右对齐,符号,宽度 "{:>+12}" " +1234"
填充'=',居中,宽度 "{:=^12}" "====1234===="
二进制表示法 "{:b}" "10011010010"
宽度,八进制表示法 "{:12o}" " 2322"
符号,宽度,十六进制表示法 "{:+12x}" " +4d2"
符号,宽度,用大写数字的十六进制 "{:+12X}" " +4D2"
符号,显式基数前缀,宽度,十六进制 "{:+#12x}" " +0x4d2"
符号,基数,零,宽度,十六进制 "{:+#012x}"
"{:+#06x}"
"+0x0000004d2"
+0x4d2"

如最后两个示例所示,最小字段宽度适用于整数,符号,基数前缀和所有.

负数总是包括他们的符号.结果类似于”强制符号(forced sign)”示例中显示的结果.

当你要求前导零时,简单地忽略对齐和填充字符,因为零会扩展数字以填充整个字段. 使用参数1234.5678,我们可以显示特定于浮点类型的效果:

使用的功能 模板字符串 结果
默认 "{}" "1234.5678"
精度 "{:.2}"
"{:.6}"
"1234.57"
"1234.567800"
最小字段宽度 "{:12}" " 1234.5678"
最小,精度 "{:12.2}"
" 1234.57"
" 1234.567800"
前导零,最小,精度 "{:012.6}" "01234.567800"
科学计数法 "{:e}" "1.2345678e3"
科学计数法,精度 "{:.3e}" "1.235e3"
科学计数法,最小,精度 "{:12.3e}"
"{:12.3E}"
" 1.235e3"
" 1.235E3"

格式化其它类型(Formatting Other Types)

除了字符串和数字之外,你还可以格式化其他几种标准库类型:

  • 错误类型都可以直接格式化,从而可以轻松地将它们包含在错误消息中.每个错误类型都应该实现std::error::Errortrait,它扩展了默认的格式traitstd::fmt::Display.因此,任何实现Error的类型都可以格式化.

  • 你可以格式化互联网协议地址类型,如std::net::IpAddrstd::net::SocketAddr.

  • 可以格式化布尔值truefalse值,尽管这些值通常不是直接呈现给最终用户的最佳字符串.

你应该使用与字符串相同的格式参数.长度限制,字段宽度和对齐控制按预期工作.

格式化值以调试(Formatting Values for Debugging)

为了帮助调试和记录,{:?}参数以一种对程序员有帮助的方式格式化Rust标准库中的任何公有类型.你可以使用它来检查向量,切片,元组,哈希表,线程和数百种其他类型.

例如,你可以编写以下内容:

  1. use std::collections::HashMap;
  2. let mut map = HashMap::new();
  3. map.insert("Portland", (45.5237606,-122.6819273));
  4. map.insert("Taipei", (25.0375167, 121.5637));
  5. println!("{:?}", map);

这打印:

  1. {"Taipei": (25.0375167, 121.5637), "Portland": (45.5237606, -122.6819273)}

HashMap(f64, f64)类型已经知道如何格式化自己,你无需付出任何努力.

如果在format参数中包含#字符,Rust将优质打印该值.将此代码更改为`println!(“{:#?}”, map)会导致此输出:

  1. {
  2. "Taipei": (
  3. 25.0375167,
  4. 121.5637
  5. ),
  6. "Portland": (
  7. 45.5237606,
  8. -122.6819273
  9. )
  10. }

这些确切的形式无法保证,从一个Rust版本到下一个版本有时会有更改.

正如我们已经提到的,你可以使用#[derive(Debug)]语法使你自己的类型与{:?}一起使用:

  1. #[derive(Copy, Clone, Debug)]
  2. struct Complex { r: f64, i: f64 }

有了这个定义,我们可以使用{:?}格式来打印Complex值:

  1. let third = Complex { r: -0.5, i: f64::sqrt(0.75) };
  2. println!("{:?}", third);

这打印:

  1. Complex { r: -0.5, i: 0.8660254037844386 }

这对于调试来说很好,但是如果{}能够以更传统的形式打印它们可能会很好,例如-0.5 + 0.8660254037844386i.在第421页的”格式化自己的类型(Formatting Your Own Types)”中,我们将展示如何做到这一点.

格式化指针以调试(Formatting Pointers for Debugging)

通常,如果你将任何类型的指针传递给格式化宏—一个引用,一个Box,一个Rc—宏只是跟随指针并格式化它的引用的对象;指针本身不感兴趣.但是当你进行调试时,看到指针有时会很有帮助:地址可以作为单个值的粗略”名称”,在检查具有周期或共享的结构时可以发挥作用.

{:p}表示法将引用,boxes和其他类似指针的类型格式化为地址:

  1. use std::rc::Rc;
  2. let original = Rc::new("mazurka".to_string());
  3. let cloned = original.clone();
  4. let impostor= Rc::new("mazurka".to_string());
  5. println!("text: {}, {}, {}", original, cloned, impostor);
  6. println!("pointers: {:p}, {:p}, {:p}", original, cloned, impostor);

这段代码打印:

  1. text: mazurka, mazurka, mazurka
  2. pointers: 0x7f99af80e000, 0x7f99af80e000, 0x7f99af80e030

当然,特定的指针值会随着运行的不同而不同.但即便如此,比较地址会清楚地表明前两个是对同一个String的引用,而第三个指向一个不同的值.

地址往往看起来像十六进制汤,因此更精确的可视化可能是值得的,但{:p}样式仍然是一个有效的应急的(quick-and-dirty)解决方案.

通过索引或名称引用参数(Referring to Arguments by Index or Name)

格式化参数可以显式选择它使用的参数.例如:

  1. assert_eq!(format!("{1},{0},{2}", "zeroth", "first", "second"),
  2. "first,zeroth,second");

你可以在冒号后包含格式参数:

  1. assert_eq!(format!("{2:#06x},{1:b},{0:=>10}", "first", 10, 100),
  2. "0x0064,1010,=====first");

你还可以按名称选择参数.这使得具有许多参数的复杂模板更加清晰.例如:

  1. assert_eq!(format!("{description:.<25}{quantity:2} @ {price:5.2}",
  2. price=3.25,
  3. quantity=3,
  4. description="Maple Turmeric Latte"),
  5. "Maple Turmeric Latte..... 3 @ 3.25");

(这里的命名参数类似于Python中的关键字参数,但这只是格式化宏的一个特殊功能,而不是Rust的函数调用语法的一部分.)

你可以在单个格式化宏使用中将索引,命名和位置(即没有索引或名称)参数混合在一起.位置参数与从左到右的参数配对,就像索引和命名参数不存在一样:

  1. assert_eq!(format!("{mode} {2} {} {}",
  2. "people", "eater", "purple", mode="flying"),
  3. "flying purple people eater");

命名参数必须出现在列表的末尾.

动态宽度和精度(Dynamic Widths and Precisions)

参数的最小字段宽度,文本长度限制和数字精度不必总是固定值;你可以在运行时选择它们.

我们一直在研究像这个表达式这样的例子,它给你在20个字符宽的字段中右对齐的字符串content:

  1. format!("{:>20}", content)

但是如果你想在运行时选择字段宽度,你可以写:

  1. format!("{:>1$}", content, get_width())

为最小字段宽度写1$告诉format!使用第二个参数的值作为宽度.引用的参数必须是一个usize.你还可以按名称引用参数:

  1. format!("{:>width$}", content, width=get_width())

同样的方法也适用于文本长度限制:

  1. format!("{:>width$.limit$}", content,
  2. width=get_width(), limit=get_limit())

除了文本长度限制或浮点精度之外,你还可以编写*,它表示将下一个位置参数作为精度.以下剪切content最多为get_limit()字符:

  1. format!("{:.*}", get_limit(), content)

作为精度的参数必须是一个usize.字段宽度没有相应的语法.

格式化你自己的类型(Formatting Your Own Types)

格式化宏使用std::fmt模块中定义的一组trait将值转换为文本.你可以通过自己实现一个或多个这些trait,使Rust的格式化宏格式你自己的类型.

格式化参数的表示法指示其参数的类型必须实现的trait:

表示法 示例 Trait 目的
none {} std::fmt::Display 文本,数字,,错误:全能trait
b {bits:#b} std::fmt::Binary 二进制数字
o {:#5o} std::fmt::Octal 八进制数
x {:4x} std::fmt::LowerHex 十六进制,小写数字
X {:016X} std::fmt::UpperHex 十六进制,大写数字
e {:.3e} std::fmt::LowerExp 科学记数法中的浮点数
E {:.3E} std::fmt::UpperExp 相同,大写E
? {:#?} std::fmt::Debug 调试视图,适用于开发人员
p {:p} std::fmt::Pointer 指针作为地址,供开发人员使用

当你在类型定义上放置#[derive(Debug)]属性以便可以使用{:?}格式参数时,你只需要求Rust为你实现std::fmt::Debugtrait.

格式化trait都具有相同的结构,仅在名称上有所不同.我们将使用std::fmt::Display作为代表:

  1. trait Display {
  2. fn fmt(&self, dest: &mut std::fmt::Formatter)
  3. -> std::fmt::Result;
  4. }

fmt方法的工作是生成一个格式正确的self表示,并将其字符写入dest.除了用作输出流之外,dest参数还包含从格式参数解析的详细信息,如对齐和最小字段宽度.

例如,在本章的前面我们建议如果Complex值以通常的a + bi形式打印出来会很好.这是一个Display实现,它执行此操作:

  1. use std::fmt;
  2. impl fmt::Display for Complex {
  3. fn fmt(&self, dest: &mut fmt::Formatter) -> fmt::Result {
  4. let i_sign = if self.i < 0.0 { '-' } else { '+' };
  5. write!(dest, "{} {} {}i", self.r, i_sign, f64::abs(self.i))
  6. }
  7. }

这利用了Formatter本身就是输出流的事实,所以write宏可以为我们做大部分工作.有了这个实现,我们可以编写以下内容:

  1. let one_twenty = Complex { r: -0.5, i: 0.866 };
  2. assert_eq!(format!("{}", one_twenty),
  3. "-0.5 + 0.866i");
  4. let two_forty = Complex { r: -0.5, i: -0.866 };
  5. assert_eq!(format!("{}", two_forty),
  6. "-0.5 - 0.866i");

以极坐标形式显示复数有时是有帮助的:如果你想象在从原点到数字的复平面上绘制的线,极坐标形式给出线的长度,其顺时针角度给出正x轴.格式参数中的#字符通常选择一些备用显示形式;Display实现可以将其视为使用极坐标形式的请求:

  1. impl fmt::Display for Complex {
  2. fn fmt(&self, dest: &mut fmt::Formatter) -> fmt::Result {
  3. let (r, i) = (self.r, self.i);
  4. if dest.alternate() {
  5. let abs = f64::sqrt(r * r + i * i);
  6. let angle = f64::atan2(i, r) / std::f64::consts::PI * 180.0;
  7. write!(dest, "{} ∠ {}°", abs, angle)
  8. } else {
  9. let i_sign = if i < 0.0 { '-' } else { '+' };
  10. write!(dest, "{} {} {}i", r, i_sign, f64::abs(i))
  11. }
  12. }
  13. }

使用此实现:

  1. let ninety = Complex { r: 0.0, i: 2.0 };
  2. assert_eq!(format!("{}", ninety),
  3. "0 + 2i");
  4. assert_eq!(format!("{:#}", ninety),
  5. "2 ∠ 90°");

虽然格式化trait的fmt方法返回fmt::Result值(典型的特定于模块的Result类型),但你应该仅从Formatter上的操作传播故障,因为fmt::Display实现会调用write!;你的格式化函数必须永远不会自己生成错误.这允许像format!一样的宏简单地返回一个String而不是Result<String, ...>,因为将格式化文本附加到String永远不会失败.它还可以确保你从write!或者writeln!中获得的任何错误,反映底层I/O流的实际问题,而不是格式化问题.

Formatter还有很多其他有用的方法,包括一些用于处理结构化数据的方法,如地图,列表等,这里我们不会介绍;有关完整详细信息,请参阅在线文档.

在自己的代码中使用格式化语言(Using the Formatting Language in Your Own Code)

您可以使用Rust的format_args!宏和std::fmt::Arguments类型编写自己的函数和接受格式模板和参数的宏.例如,假设你的程序需要在运行时记录状态消息,并且你希望使用Rust的文本格式化语言来生成它们.以下是一个开始:

  1. fn logging_enabled() -> bool {
  2. ...
  3. }
  4. use std::fs::OpenOptions;
  5. use std::io::Write;
  6. fn write_log_entry(entry: std::fmt::Arguments) {
  7. if logging_enabled() {
  8. // Keep things simple for now, and just
  9. // open the file every time.
  10. let mut log_file = OpenOptions::new()
  11. .append(true)
  12. .create(true)
  13. .open("log-file-name")
  14. .expect("failed to open log file");
  15. log_file.write_fmt(entry)
  16. .expect("failed to write to log");
  17. }
  18. }

你可以像这样调用write_log_entry:

  1. write_log_entry(format_args!("Hark! {:?}\n", mysterious_value));

在编译时,format_args!宏解析模板字符串并根据参数的类型进行检查,如果有任何问题则报告错误.在运行时,它会计算参数并构建一个Arguments值,其中包含格式化文本所需的所有信息:模板的预解析形式,以及对参数值的共享引用.

构造一个Arguments值很便宜:它只是收集了一些指针.尚未进行格式化工作,只有稍后需要的信息集合.这可能很重要:如果未启用日志记录,则会浪费任何将数字转换为十进制,填充值等所花费的时间.

File类型实现了std::io::Writetrait.其write_fmt方法接受Argument并进行格式化.它将结果写入底层流.

write_log_entry的调用并不漂亮.这是宏可以帮助的地方:

  1. macro_rules! log { // no ! needed after name in macro definitions
  2. ($format:tt, $($arg:expr),*) => (
  3. write_log_entry(format_args!($format, $($arg),*))
  4. )
  5. }

我们将在第20章详细介绍宏.现在,请相信它定义了一个新的log!宏,传递给它得参数到了format_args!,然后在生成的Arguments值上调用write_log_entry函数.格式化宏,如println!,writeln!,format!都是大致相同的想法.

你可以像这样使用log!:

  1. log!("O day and night, but this is wondrous strange! {:?}\n",
  2. mysterious_value);

希望这看起来好一点.

正则表达式(Regular Expressions)

外部regexcrate是Rust的官方正则表达式库.它提供了通常的搜索和匹配函数.它对Unicode有很好的支持,但它也可以搜索字节字符串.虽然它不支持你经常在其他正则表达式包中找到的某些功能,例如反向引用和环视模式,但这些简化允许regex确保搜索花费时间与表达式的大小和正在搜索的文本长度方面线性关系.除其他外,这些保证使得regex可以安全使用,即使不受信任的表达式搜索不受信任的文本也是如此.

在本书中,我们仅提供regex的概述;有关详细信息,请参阅其在线文档.

虽然regexcrate不在std中,但它由Rust库团队维护,该团队负责std.要使用regex,请将以下行放在你creat的 Cargo.toml 文件的[dependencies]部分中:

  1. regex = "0.2.2"

然后在crate的根中放置一个extern crate项:

  1. extern crate regex;

在以下部分中,我们假设你已经进行了这些更改.

基本的正则表达式使用(Basic Regex Use)

Regex值表示已解析的正则表达式,可供使用.Regex::new构造函数尝试将&str解析为正则表达式,并返回Result:

  1. use regex::Regex;
  2. // A semver version number, like 0.2.1.
  3. // May contain a pre-release version suffix, like 0.2.1-alpha.
  4. // (No build metadata suffix, for brevity.)
  5. //
  6. // Note use of r"..." raw string syntax, to avoid backslash blizzard.
  7. let semver = Regex::new(r"(\d+)\.(\d+)\.(\d+)(-[-.[:alnum:]]*)?")?;
  8. // Simple search, with a Boolean result.
  9. let haystack = r#"regex = "0.2.5""#;
  10. assert!(semver.is_match(haystack));

Regex::captures方法在字符串中搜索第一个匹配项,并返回一个regex::Captures值,该值保存表达式中每个组的匹配信息:

  1. // You can retrieve capture groups:
  2. let captures = semver.captures(haystack)
  3. .ok_or("semver regex should have matched")?;
  4. assert_eq!(&captures[0], "0.2.5");
  5. assert_eq!(&captures[1], "0");
  6. assert_eq!(&captures[2], "2");
  7. assert_eq!(&captures[3], "5");

如果请求的组不匹配,则索引Captures值会发生恐慌.要测试特定组是否匹配,可以调用Captures::get,它返回Option<regex::Match>.Match值记录单个组的匹配:

  1. assert_eq!(captures.get(4), None);
  2. assert_eq!(captures.get(3).unwrap().start(), 13);
  3. assert_eq!(captures.get(3).unwrap().end(), 14);
  4. assert_eq!(captures.get(3).unwrap().as_str(), "5");

你可以迭代字符串中的所有匹配项:

  1. let haystack = "In the beginning, there was 1.0.0. \
  2. For a while, we used 1.0.1-beta, \
  3. but in the end, we settled on 1.2.4.";
  4. let matches: Vec<&str> = semver.find_iter(haystack)
  5. .map(|match_| match_.as_str())
  6. .collect();
  7. assert_eq!(matches, vec!["1.0.0", "1.0.1-beta", "1.2.4"]);

find_iter迭代器为表达式的每个非重叠匹配生成一个Match值,从字符串的开头到结尾.captures_iter方法类似,但会生成记录所有捕获组的Captures值.必须报告捕获组时搜索速度较慢,因此如果你不需要它们,最好使用其中一种不返回它们的方法.

惰性地构建Regex值(Building Regex Values Lazily)

Regex::new构造函数可能很昂贵:在快速的开发人员计算机上构建一个1200字符正则表达式的Regex可能需要几乎毫秒,甚至一个简单的表达式也需要几微秒.最好保持Regex构造不受重度计算循环的影响;相反,你应该构建一次你的Regex,然后重用相同的.

lazy_staticcrate提供了一种在第一次使用时惰性地构造静态值的好方法.首先,请注意 Cargo.toml 文件中的依赖项:

  1. [dependencies]
  2. lazy_static = "0.2.8"

这个crate提供了一个宏来声明这些变量:

  1. #[macro_use]
  2. extern crate lazy_static;
  3. lazy_static! {
  4. static ref SEMVER: Regex
  5. = Regex::new(r"(\d+)\.(\d+)\.(\d+)(-[-.[:alnum:]]*)?")
  6. .expect("error parsing regex");
  7. }

宏扩展为名为SEMVER的静态变量的声明,但其类型不完全是Regex.相反,它是一个宏生成的类型,它实现了Deref<Target=Regex>,因此暴露了与Regex相同的所有方法.第一次解引用SEMVER时,将初始化程序进行计算,并保存该值以供以后使用.由于SEMVER是一个静态变量,而不仅仅是一个局部变量,初始化程序每次执行程序时最多运行一次.

有了这个声明,使用SEMVER很简单:

  1. use std::io::BufRead;
  2. let stdin = std::io::stdin();
  3. for line in stdin.lock().lines() {
  4. let line = line?;
  5. if let Some(match_) = SEMVER.find(&line) {
  6. println!("{}", match_.as_str());
  7. }
  8. }

你可以把lazy_static!声明放进模块中,甚至在使用Regex的函数内部,如果这是最合适的范围.正则表达式仍然始终只在每个程序执行时编译一次.

规范化(Normalization)

大多数用户会认为茶的法语单词 thé ,长度为三个字符.但是,Unicode实际上有两种表示此文本的方式:

  • 组合(composed) 形式中, thé 包含三个字符't','h''é',其中'é'是单个Unicode字符,代码点为0xe9.

  • 分解(decomposed) 形式中, thé 包含四个字符't','h','e''\u{301}',其中'e'是纯ASCII字符,没有重音,代码点0x301是”COMBINING ACUTE ACCENT”字符,它为其后面的任何字符添加了重音符号.

Unicode不认为 é 的组合形式或分解形式是”正确的(correct)”;相反,它认为它们都是同一抽象字符的等效表示.Unicode表示两种形式都应以相同的方式显示,并允许文本输入方法生成,因此用户通常不知道他们正在查看或键入哪种形式.(Rust允许你直接在字符串字面量中使用Unicode字符,所以如果你不关心你得到哪种编码,你可以简单地写"thé".为了清楚起见,我们将使用\u转义.

但是,如果将"th\u{e9}""\u{301}"视为Rust&strString值,那么他们是完全不同的.它们具有不同的长度,比较为不相等,具有不同的哈希值,并且相对于其他字符串的顺序不同:

  1. assert!("th\u{e9}" != "the\u{301}");
  2. assert!("th\u{e9}" > "the\u{301}");
  3. // A Hasher is designed to accumulate the hash of a series of values,
  4. // so hashing just one is a bit clunky.
  5. use std::hash::{Hash, Hasher};
  6. use std::collections::hash_map::DefaultHasher;
  7. fn hash<T: ?Sized + Hash>(t: &T) -> u64 {
  8. let mut s = DefaultHasher::new();
  9. t.hash(&mut s);
  10. s.finish()
  11. }
  12. // These values may change in future Rust releases.
  13. assert_eq!(hash("th\u{e9}"), 0x53e2d0734eb1dff3);
  14. assert_eq!(hash("the\u{301}"), 0x90d837f0a0928144);

显然,如果你打算比较用户提供的文本,或者将其用作哈希表或B树中的键,则需要首先将每个字符串放在某个规范形式中.

幸运的是,Unicode为字符串指定了 规范化(normalized) 形式,每当根据Unicode的规则将两个字符串视为等效时,它们的规范化形式就是逐字符(character-for-character)相同的.使用UTF-8编码时,它们是逐字节(byte-for-byte)相同的.这意味着你可以用==比较规范化的字符串,将它们用作HashMapHashSet中的键,等等,你将获得Unicode的相等概念.

未能规范化甚至可能产生安全后果.例如,如果你的网站在某些情况下规范化用户名但是另一些情况下没有,则最终可能会有两个名为bananasflambé的不同用户,其中某些部分的代码视其为同一用户,但其他部分则区别对待,从而导致某人的权限被错误地扩展到另一个.当然,有很多方法可以避免这类问题,但历史表明还有很多方法不能.

规范化形式(Normalization Forms)

Unicode定义了四种规范化形式,每种形式都适用于不同的用途.有两个问题需要回答:

  • 首先,你是希望字符尽可能 组合(composed) 还是尽可能 分解(decomposed) ?

例如,越南词 Phở 的最复合表示是三字符串"Ph\ u{1edf}",其中音调标记 ̉和元音标记 ̛ 都应用于基本字符”o”,单个Unicode字符'\u{1edf}',Unicode尽职尽责地命名 LATIN SMALL LETTER O WITH HORN AND HOOK ABOVE(带有角和上钩的拉丁小写字母o).

分解最多的表示将基本字母及其两个标记分成三个单独的Unicode字符:'o','\u{31b}'(COMBINING HORN(组合角))和'\u{309}'(COMBINING HOOK ABOVE(组合上钩)),结果在"Pho\u{31b}\u{309}"中.(每当组合标记显示为单独的字符,而不是作为组合字符的一部分时,所有规范化形式都指定它们必须出现的固定顺序,因此即使字符具有多个重音,也可以很好地指定规范化).

组合形式通常具有较少的兼容性问题,因为它更接近地匹配在Unicode建立之前大多数语言用于其文本的表示.它还可以更好地使用简单的字符串格式功能,比如Rust的format!宏.另一方面,分解形式可能更适合显示文本或搜索,因为它使文本的详细结构更加明确.

  • 第二个问题是:如果两个字符序列代表相同的基本文本,但文本格式的方式不同,你是否希望将它们视为等效,还是保持它们不同?

Unicode有普通数字'5',上标数字'⁵'(或'\u{2075}')和带圈数字'⑤'(或'\u{2464}')的单独字符,但声明所有三着是 兼容性等效 .类似地,Unicode对于连字 ffi ('\u {fb03}')有当个字符,但声明这是与三字符序列"ffi"兼容性等效的.

兼容性等效对搜索有意义:搜索"difficult",仅使用ASCII字符,应匹配使用 ffi 连字的字符串"di\u{fb03}cult".将兼容性分解应用于后一个字符串将用三个普通字母"ffi"替换连字,使搜索更容易.但是将文本规范化为兼容性等效形式可能会丢失必要的信息,因此应该小心应用.例如,在大多数情况下将"2⁵"存储为"25"是不正确的.

Unicode规范化形式C和规范化形式D(NFC和NFD)使用每个字符的最大组合和最大分解形式,但不尝试统一兼容性等效序列.NFKC和NFKD规范化形式类似于NFC和NFD,但将所有兼容性等效序列规范化为其类别的某个简单代表.

万维网联盟的”Character Model For the World Wide Web(万维网字符模型)”建议对所有内容使用NFC.Unicode标识符和模式语法附件建议在编程语言中使用NFKC作为标识符,并提供在必要时调整形式的原则.

The unicode-normalization Crate(The unicode-normalization Crate)

Rust的unicode-normalizationcrate提供了一个trait,它将方法添加到&str以将文本放入四种规范化形式中的任何一种.要使用它,请将以下行添加到 Cargo.toml 文件的[dependencies]部分:

  1. unicode-normalization = "0.1.5"

你的crate的顶级文件需要一个extern crate声明:

  1. extern crate unicode_normalization;

有了这些声明,&str有四个新方法,它们返回字符串的特定规范化形式上的迭代器:

  1. use unicode_normalization::UnicodeNormalization;
  2. // No matter what representation the lefthand string uses
  3. // (you shouldn't be able to tell just by looking),
  4. // these assertions will hold.
  5. assert_eq!("Phở".nfd().collect::<String>(), "Pho\u{31b}\u{309}");
  6. assert_eq!("Phở".nfc().collect::<String>(), "Ph\u{1edf}");
  7. // The lefthand side here uses the "ffi" ligature character.
  8. assert_eq!("① Di\u{fb03}culty".nfkc().collect::<String>(), "1 Difficulty");

接受规范化化字符串并以相同的形式再次对其进行规范 化可保证返回相同的文本.

虽然规范化字符串的任何子字符串本身都是规范化的,但两个规范化字符串的连接不一定是规范化的:例如,第二个字符串可能以组合字符开始,应该在第一个字符串末尾的组合字符之前先放置这些字符.

只要文本在规范化时不使用未分配的代码点,Unicode就会承诺其标准化形式在未来版本的标准中不会改变.这意味着规范化形式通常可以安全地用于持久存储,即使Unicode标准发展也是如此.