【2025 Rust学习 --- 17 文本和格式化 】

打印 上一主题 下一主题

主题 1740|帖子 1740|积分 5230

字符串与文本

Rust 的主要文本类型 String、str 和 char
内容概括:


  • Unicode 背景知识?
  • 单个 Unicode 码点的 char?
  • String 类型和 str 类型都是表示拥有和借用的 Unicode 字符序列。
  • Rust 的字符串格式化工具,比如 println! 宏和 format! 宏。
  • Rust 对正则表达式的支持?
  • 为什么 Unicode 的规范化很紧张,怎样在 Rust 中对其进行规范化?
Unicode 背景知识

ASCII、Latin-1 和 Unicode

Unicode 和 ASCII 对于从 0 到 0x7f 的全部 ASCII 码点是一一对应的
Unicode 也将 0 到 0xff 分配给了与 ISO/IEC 8859-1 字符集相同的字符,这是 ASCII 字符集用于西欧语言的 8 位超集。Unicode 将此码点范围称为 Latin-1 码块,因此我们也将使用耳熟能详的名 称 Latin-1 来指代 ISO/IEC 8859-1。
Unicode 是 Latin-1 的超集,因此将 Latin-1 转换为 Unicode 甚至不需要查表
UTF-8 编码

Rust 的 String 类型和 str 类型表示使用了 UTF-8 编码形式的文本。UTF-8 会将字符编码为 1~4 字节的序列
格式良好的 UTF-8 序列有两个限定:


  • 只有任何给定码点的最短编码才被 以为是格式良好的,你不能耗费 4 字节来编码原来只需要 3 字节的码点。此规 则确保了每个码点只会有唯一一个 UTF-8 编码。
  • 格式良好的 UTF-8 不得 对从 0xd800 到 0xdfff 或凌驾 0x10ffff 的数值进行编码:这些数值要么保 留用作非字符目的,要么完全超出了 Unicode 的范围。

UTF-8 一些非常有用的属性:


  • 由于 UTF-8 会把码点 0 ~ 0x7f 编码为字节 0 ~ 0x7f,因此一段 ASCII 文本必然是有效的 UTF-8 字符串。反过来,假如 UTF-8 字符串中只包含 ASCII 字符,则它也必然是有效的 ASCII 字符串。 对于 Latin-1 则不是这样的,比如,Latin-1 会将 é 编码为字节 0xe9,而 UTF-8 会将其解释为三字节编码中的第一字节。
  • 通过检察任何字节的高位,就能立刻判断出它是某个字符的 UTF-8 编码的 起始字节照旧中心字节。 编码的第一字节会单独通过其前导位告诉你编码的全长。 由于不会有任何编码凌驾 4 字节,因此 UTF-8 在处置惩罚时从不需要无穷循环,这在处置惩罚不受信任的数据时非常有用。
  • 在格式良好的 UTF-8 中,即使从字节中心的任意点开始,你也始终可以明确地找出该字符编码的起始位置和竣事位置。UTF-8 的第一字节和背面的字 节一定不同,以是一段编码不可能从另一段编码的中心开始。第一字节会确 定编码的总长度,因此任何一段编码都不可能是另一段编码的前缀。这很有用。比方,要在 UTF-8 字符串中搜索 ASCII 分隔符只需对分隔符的字节进行简朴扫描即可。这个分隔符永远不会作为多字节编码的任何部分出现,因此根本不需要跟踪 UTF-8 的布局。类似地,在一个字节串中搜索另一个字 节串的算法无须针对 UTF-8 字符串做修改即可正常工作,甚至连那些根本不会查抄待搜文本中每字节的算法也没题目。
只管可变宽度编码比固定宽度编码更复杂,但以上特征让 UTF-8 比预想的更容易使用。标准库会帮你处置惩罚绝大部分题目。
文本方向性

拉丁文、西里尔文、泰文等文字是从左向右誊写的,而希伯来文、阿拉伯文等文 字则是从右向左誊写的。Unicode 以写入或读取字符的常规顺序存储字符,因此 在这种环境下字符串(如希伯来语文本)中保存的首字节是对写在最右端的字符的编码。
  1. assert_eq!("טוב ערב." chars().next(), Some('ע'));
复制代码
字符char —32bit

Rust 的 char 类型是一个包含 Unicode 码点的 32 位值。char 保证会落在 0~ 0xd7ff 或 0xe000~ 0x10ffff 范围内,全部用于创建和操作 char 值的方法 都会确保此规则永远创建。char 类型实现了 Copy 和 Clone,以及用于比较、 哈希和格式化的全部常用特型。
字符串切片可以使用slice.chars()生成针对其字符的迭代器:
assert_eq!("カニ".chars().next(), Some('カ'));
接下来出现的变量 ch 全都是 char 类型的。
字符分类

全部 is_ascii_… 方法也可用于 u8 字节类型:【u8和char范围一样】
留意 is_whitespace 和 is_ascii_whitespace 对某些字符的处置惩罚不同
  1. let line_tab = '\u{000b}'; //“行间制表符”,也叫“垂直制表符”
  2. assert_eq!(line_tab.is_whitespace(), true);
  3. assert_eq!(line_tab.is_ascii_whitespace(), false);
复制代码
char::is_ascii_whitespace 函数实现了许多 Web 标准中通用 的空白字符定义,而 char::is_whitespace 遵循的是 Unicode 标准。
处置惩罚数字



  • ch.to_digit(radix)(转数字)  判断 ch 是不是以 radix 为基数的数字。假如是,就返回 Some(num),其 中 num 是 u32;否则,返回 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。
字符大小写转换



  • ch.is_lowercase()(是小写?)和 ch.is_uppercase()(是大 写?)  指出 ch 是小写字母字符照旧大写字母字符。这两个方法遵循 Unicode 的派生属性 Lowercase(小写字母)和 Uppercase(大写字母),因此它们涵盖了非 拉丁字母表(如希腊字母和西里尔字母),并给出了和 ASCII 一样的预期结果。
  • ch.to_lowercase()(转小写)和 ch.to_uppercase()(转大写)  根据 Unicode 的默认大小写转换算法,返回生成 ch 的小写和大写对应字符 的迭代器,这两个方法会返回迭代器而不是单个字符,由于 Unicode 中的大小写转换并 不总是一对一的过程(德文等)为便于使用,这些迭代器都实现了 std::fmt:isplay 特型,因此可以将它 们直接传给 println! 或 write! 宏。
与整数之间的转换

as 运算符会将 char 转换为任何整数类型,并抹掉高位
  1. assert_eq!('B' as u32, 66);
  2. assert_eq!('饂' as u8, 66); // 截断高位
  3. assert_eq!('二' as i8, -116); // 同上
复制代码
as 运算符会将任何 u8 值转换为 char,并且 char 也实现了 From<u8>。但是,更宽的整数类型可以表示无效码点,因此对于那部分整数,必须使用 std::char::from_u32 进行转换,它会返回 Option<char>
String 与 str

Rust 的 String 类型和 str 类型会保证自己只包含格式良好的 UTF-8
标准库 通过限定你创建 String 值和 str 值的方式以及可以对它们执行的操作来确保 这一点。这样,当引入这些值时一定是格式良好的,而且在使用中也是如此。它们全部的方法都会服从这个保证:对它们的任何安全操作都不会引入格式错误的 UTF-8。这就简化了处置惩罚文本的代码。
Rust 可以将文本处置惩罚方法关联到 str 或 String 上,具体关联到哪个取决于该 方法是需要可调解大小的缓冲区照旧仅满足于就地使用文本。由于 String 可以解引用成 &str,因此在 str 上定义的每个方法都可以直接在 String 上使 用。
文本处置惩罚方法会按字节偏移量索引文本并以字节而不是字符为单位丈量其长度。 实际上,考虑到 Unicode 的性质,按字符索引并不像看起来那么有用,按字节偏移量索引反而更快且更简朴。假如试图使用位于某个字符的 UTF-8 编码中心的 字节偏移量,则该方法会发生 panic,因此不能通过这种方式引入格式错误的 UTF-8。
String 通过封装 Vec 实现,并可以确保向量中的内容永远是格式良好的 UTF-8。Rust 永远不会把 String 改成更复杂的表示形式,因此可假设 String 的性能表现始终会和 Vec 保持一致。
创建字符串值

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


  • String::new()(新建)  返回一个新的空字符串。这时还没有在堆上分配缓冲区,但将来会按需分配
  • String::with_capacity(n)(自带容量)  返回一个新的空字符串,此中预先在堆上分配了一个足以容纳至少 n 字节的缓冲区。假如事先知道要构建的字符串的长度,则此构造函数可以让你从一开始就精确设置缓冲区大小,而不是等构建字符串时再进行调解。假如字符串的长度凌驾 n 字节,则该字符串仍会根据需要增加其缓冲区。与向量一样,字符串也有capacity 方法、reserve 方法和 shrink_to_fit 方法,但一般来说默认的分配逻辑就很好。
  • str_slice.to_string()(转字符串)  分配一个新的 String,其内容是 str_slice 的副本
  • iter.collect()(网络)  通过串联迭代器的各个条目构造出字符串,迭代器的条目可以是 char 值、 &str 值或 String 值。比方,要从字符串中移除全部空格,可以这样写:
    1. let spacey = "ni gan ma";
    2. let spaceless: String = spacey.chars().filter(|c| !c.is_whitespace()).collect();
    3. assert_eq!(spaceless, "niganma");
    复制代码
    这种方式使用 collect 可以充分使用 String 对 std::iter::FromIterator 特型的实现。
  • slice.to_owned()(转自有)  将 slice 的副本作为新分配的 String 返回。str 类型无法实现 Clone:该特型需要在 &str 上进行 clone 以返回 str 值,但 str 是无固定大小类型。不过,&str 实现了 ToOwned,这能让实现者指定其自有 (Owned)版本的等效类型。
String的信息

从字符串切片中获取根本信息。


  • slice.len()(长度)  slice 的长度,以字节为单位。
  • slice.is_empty()(为空?)  假如 slice.len() == 0,就返回 True。
  • slice[range](范围内切片)  返回借用了 slice 给定部分的切片。有界的范围、部分有界的范围和无界 的范围都可以。要想在给定的字节偏移处获取单个字符:必须在切片上生成一个 chars 迭代器,并要求它解析成单个字符的 UTF-8:
    1. let parenthesized = "Rust (饂)";
    2. assert_eq!(parenthesized[6..].chars().next(), Some('饂'));
    复制代码
  • slice.split_at(i)(拆分于)  返回从 slice 借来的两个共享切片的元组:一个是字节偏移量 i 之前的部 分,另一个是字节偏移量 i 之后的部分。换句话说,这会返回 (slice[..i], slice[i..])。
  • slice.is_char_boundary(i)(是字符边界?)  假如字节偏移量 i 恰好落在字符边界之间并且适互助为 slice 的偏移量, 就返回 True。
也可以对切片做相等性比较、排序和哈希。有序比较只是将字符串视为一 系列 Unicode 码点,并按字典顺序进行比较。
追加文本与插入文本



  • string.push(ch)(压入)  将字符 ch 追加到 string 的末端。
  • string.push_str(slice)(压入字符串)  追加 slice 的全部内容。
  • string.extend(iter)(以 iter 扩展)  将迭代器 iter 生成的条目追加到字符串中。迭代器可以生成 char 值、 str 值或 String 值。这是 String 对 std::iter::Extend 特型的实现。
  • string.insert(i, ch)(插入于)  在 string 内的字节偏移量 i 处插入单个字符 ch。这需要平移 i 之后的 全部字符以便为 ch 腾出空间,因此用这种方式构建字符串的时间复杂度是 O(n) 。 不考虑重新分配内存的环境。
  • string.insert_str(i, slice)(插入字符串于)  这会在 string 内插入 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,假如你忽略 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");
    复制代码
    假如左操作数的缓冲区充足容纳结 果,那么就不需要分配内存。
    + 的左操作数不能是 &str,以是不能写成:let parenthetical = "(" + string + ")";
    只能改成: let parenthetical = "(".to_string() + &string + ")";
    使用 String::with_capacity 创建具有精确缓冲区大小的字符串可以完全制止调 整大小,并且可以镌汰对堆分配器的调用次数。
移除文本与替换文本

String 有以下几个移除文本的方法。(这些方法不会影响字符串的容量,假如 需要释放内存,请使用 shrink_to_fit)


  • string.clear()(清空)  将 string 重置为空字符串。
  • string.truncate(n)(截断为 n 个)  丢弃字节偏移量 n 之后的全部字符,留下长度最多为 n 的 string。假如 string 短于 n 字节,则毫无结果。
  • string.pop()(弹出)  从 string 中移除末了一个字符(假如有的话),并将其作为 Option 返回。
  • string.remove(i)(移除)  从 string 中移除字节偏移量 i 处的字符并返回该字符,将背面的全部字符平移到前面。这个操作所耗费的时间与后续字符的数目呈线性关系。
  • string.drain(range)(抽取)  返回给定字节索引范围内的迭代器,并在迭代器被丢弃后移除字符。范围之后的全部字符都会向前平移:
    1. let mut choco = "chocolate".to_string();
    2. assert_eq!(choco.drain(3..6).collect::<String>(), "col"); //[3,6]
    3. assert_eq!(choco, "choate");
    复制代码
  • string.replace_range(range, replacement)(替换范围)  用给定的替换字符串切片替换 string 中的给定范围。切片不必与要替换 的范围长度相同,但除非要替换的范围已到达 string 的末端,否则将需要移 动范围末端之后的全部字节。
    1. let mut beverage = "a piña colada".to_string();
    2. beverage.replace_range(2..7, "kahlua"); // 'ñ' 是两字节的!
    3. assert_eq!(beverage, "a kahlua colada");
    复制代码
搜索与迭代的规定



  • r  大多数操作会从头至尾处置惩罚文本,但名称以 r 开头的操作会从尾到头处置惩罚。比方,rsplit 是 split 的从尾到头版本。在某些环境下,改变处置惩罚方向 不仅会影响值生成的顺序,还会影响值自己。
  • n  名称以 n 结尾的迭代器会将自己限定为只取给定数目的匹配项。
  • _indices [index 的复数形式]——译者注  名称以 _indices 结尾的迭代器会生成通常的迭代值和在此 slice 中的字节偏移量构成的值对。
标准库并不会提供每个操作的全部组合。比方,许多操作并不需要 n 变体,由于很容易简朴地提前竣事迭代。
搜索文本的模式

  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));
复制代码
这些类型称为模式,大多数操作支持它们。
  1. assert_eq!("## Elephants".trim_start_matches(|ch: char| ch == '#' || ch.is_whitespace()),
  2. "Elephants");
复制代码
标准库支持 4 种主要的模式


  • 以 char 作为模式意味着要匹配该字符。
  • 以 String、&str 或 &&str 作为模式,意味着要匹配等于该模式的子 串。
  • 以 FnMut(char) -> bool 闭包作为模式,意味着要匹配该闭包返回 true 的单个字符。
  • 以 &[char](留意并不是 &str,而是 char 的切片)作为模式,意味着要匹配该列表中出现的任何单个字符。请留意,假如将此列表写成数组字面量,那么可能要调用 as_ref() 来得到精确的类型。不这么做,则 Rust 会误以为这是固定大小数组类型 &[char; 2]。遗憾的是,&[char; 2] 不是有效的模式类型。
    1. let code = "\t function noodle() { ";
    2. assert_eq!(code.trim_start_matches([' ', '\t'].as_ref()),"function noodle() { ");
    3. // 更短的等效形式:&[' ', '\t'][..]4
    4. // 传递了一个字符数组 [' ', '\t'],表示要移除的字符是空格 ' ' 和制表符 \t。.as_ref() 是用来将数组转换为切片(slice),因为 trim_start_matches 接受的是一个切片作为参数
    复制代码
模式就是实现了 std::str:attern 特型的任意类型。Pattern 的细节还不稳定,以是你不能在稳定版的 Rust 中为自己的类型实现它。但是,将来要支持正则表达式和其他复杂模式也很容易。Rust 可以保证现在支持的模式类型将来仍会继承有效。
搜索与替换



  • slice.contains(pattern)(包含)  假如 slice 包含 pattern 的匹配项,就返回 true。
  • slice.starts_with(pattern)(以 pattern 开头)和 slice.ends_with(pattern)(以 pattern 结尾)  假如 slice 的起始文本或结尾文本与 pattern 相匹配,就返回 true。
  • slice.find(pattern)(查找)和 slice.rfind(pattern)(右起查找)  假如 slice 包含 pattern 的匹配项,就返回 Some(i),此中的 i 是模式出现的字节偏移量。find 方法会返回第一个匹配项,rfind 方法则返回末了 一个。
  • 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");
    复制代码
    1. assert_eq!("cabababababbage".replace("aba", "***"), "c***b***babbage")
    复制代码
  • slice.replacen(pattern, replacement, n)(替换 n 次)  与上一个方法类似,但最多替换前 n 个匹配项。
遍历文本


split(拆分)和 match(匹配)系列方法是互补的:拆分取的是匹配项之间 的范围。
这些方法中大多数会返回可逆的迭代器(也就是说,它们实现了 DoubleEndedIterator):调用它们的 .rev() 适配器方法会为你提供一个 迭代器,该迭代器会生成相同的条目,只是顺序相反。


  • slice.chars()(字符迭代器)  返回访问 slice 中各个字符的迭代器。
  • slice.char_indices()(字符及其偏移量迭代器)  返回访问 slice 中各个字符及其字节偏移量的迭代器:
    1. assert_eq!("élan".char_indices().collect::<Vec<_>>(),
    2. vec![(0, 'é'), // 有一个双字节UTF-8编码
    3.      (2, 'l'),(3, 'a'),(4, 'n')]);
    复制代码
    不等同于 .chars().enumerate(),由于本方法提供的是 每个字符在切片中的字节偏移量,而不仅仅是字符的序号。
  • slice.bytes()(字节迭代器)  返回访问 slice 中各字节的迭代器,对外袒露 UTF-8 编码细节。
    1. assert_eq!("élan".bytes().collect::<Vec<_>>(),
    2. vec![195, 169, b'l', b'a', b'n']);
    复制代码
  • slice.lines()(行迭代器)  返回访问 slice 中各行的迭代器。各行以 “\n” 或 “\r\n” 结尾。生成的 每个条目都是从 slice 中借入的 &str。这些条目不包括行的终止字符。
  • slice.split(pattern)(拆分)  返回一个迭代器,该迭代器会迭代 slice 中由 pattern 匹配项分隔开的 各个部分。这会在紧邻的两个匹配项之间、位于 slice 开头的匹配项与头部之 间,以及结尾的匹配项与尾部之间生成空字符串。  假如 pattern 是 &str,则返回的迭代器不可逆,由于这类模式会根据不同的扫描方向生成不同的匹配序列,但可逆迭代器不答应这种行为。可以改用 rsplit 方法。
  • slice.rsplit(pattern)(右起拆分)  与上一个方法类似,但此方法会从尾到头扫描 slice,并按该顺序生成匹配项。
  • slice.split_terminator(pattern)(终结符拆分)和 slice.rsplit_terminator(pattern)(右起终结符拆分)  与刚刚讲过的拆分方法类似,但这两个方法会把模式视为终结符,而不是分隔符:假如 pattern 在 slice 的末端匹配上了,则迭代器不会像 split 和 rsplit 那样生成表示匹配项和切片末端之间空字符串的空切片。比方:
    1. // 这里把':'字符视为分隔符。注意结尾的""(空串)
    2. assert_eq!("jimb:1000:Jim Blandy:".split(':').collect::<Vec<_>>(),
    3. vec!["jimb", "1000", "Jim Blandy", ""]);
    4. // 这里把'\n'字符视为终结符
    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. // 注意,没有结尾的""!
    复制代码
  • slice.splitn(n, pattern)(拆分为 n 片)和 slice.rsplitn(n, pattern)(右起拆分为 n 片)  与 split 和 rsplit 类似,但这两个方法会把字符串分成最多 n 个切片, 拆分位置位于 pattern 的第 n-1 个(split)或倒数第 n-1 个(rsplit) 匹配项处。
  • slice.split_whitespace()(按空白字符拆分)和 slice.split_ascii_whitespace()(按 ASCII 空白字符拆分)  返回访问 slice 中以空白字符分隔的各部分的迭代器。这两个方法会把连 续多个空白字符视为单个分隔符。忽略尾部空白字符。
  • split_whitespace 方法会使用 Unicode 的空白字符定义,由 char 上的 is_whitespace 方法实现。
  • split_ascii_whitespace 方法则会使用只辨认 ASCII 空白字符的 char::is_ascii_whitespace。
  • slice.matches(pattern)(匹配项)  返回访问 slice 中 pattern 匹配项的迭代器。
  • slice.rmatches(pattern) 也一样,但会从尾到头迭代。
  • slice.match_indices(pattern)(匹配项及其偏移量)和 slice.rmatch_indices(pattern)(右起匹配项及其偏移量)  和上一个方法很像,但这两个方法生成的条目是 (offset, match)  值对,此中 offset 是匹配的起始字节的偏移量,而 match 是匹配到的切片。
修剪

剪字符串就是从字符串的开头或结尾移除文本(通常是空白字符)。
修剪常用于清理从文件中读取的输入,在此文件中,用户可能为了易读性而添加了文本缩进,或者不小心在一行中留下了尾随空白字符。


  • slice.trim()(修剪)  返回略去了任何前导空白字符和尾随空白字符的 slice 的子切片。
  • slice.trim_start() 只会略去前导空白字符
  • slice.trim_end() 只会略 去尾随空白字符。
  • slice.trim_matches(pattern)(按匹配修剪)  返回 slice 的子切片,该子切片从开头和结尾略去了 pattern 的全部匹配项。
  • trim_start_matches 方法和 trim_end_matches 方法只会对匹配 的前导内容或尾随内容执行修剪操作。
  • slice.strip_prefix(pattern)(剥离前缀)和 slice.strip_suffix(pattern)(剥离后缀)  假如 slice 以 pattern 开头,则 strip_prefix 会返回一个 Some,此中携带了移除匹配文本之后的切片。否则,它会返回 None。strip_suffix 方法与此类似,但会查抄字符串末端的匹配项。与 trim_start_matches 和 trim_end_matches 类似,但这里的两个 方法会返回 Option,并且只会移除一个匹配 pattern 的副本。
字符串的大小写转换

slice.to_uppercase() 方法和 slice.to_lowercase() 方法会返回一个 新分配的字符串,此中包含已转为大写或小写的 slice 文本。结果的长度可能 与 slice 不同
从字符串中解析出其他类型

  1. pub trait FromStr: Sized {
  2. type Err;
  3. fn from_str(s: &str) -> Result<Self, Self::Err>;
  4. }
复制代码
1、全部常见的机器类型都实现了 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());
复制代码
2、char 类型也实现了 FromStr,用于解析只有一个字符的字符串:
assert_eq!(char::from_str("é"), Ok('é'));
3、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]));
复制代码
4、字符串切片有一个 parse [zh:语法分析] 方法,该方法可以将切片解析为你想要的任何类型 ——只要它实现了 FromStr。与 Iterator::collect 一样,偶然需要明确写出想要的类型,因此用 parse 不一定比直接调用 from_str 可读性强。
  1. let address = "fe80::0000:3ea9:f4ff:fe34:7a50".parse::<IpAddr>()?;
复制代码
将其他类型转换为字符串



  • 那些具有人类可读的自然打印形式的类型可以实现 std::fmt:isplay 特型,该特型答应在 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), "(0.500, 0.866)");
    4. // 使用上一个例子中的`address`
    5. let formatted_addr: String = format!("{}", address);
    6. assert_eq!(formatted_addr, "fe80::3ea9:f4ff:fe34:7a50");
    复制代码
    Rust 的全部机器数值类型都实现了 Display,字符、字符串和切片也是如此。智能指针类型 Box、Rc 和 Arc 也实现了 Display(只要 T 自己实现了 Display):它们的显示形式就只是其引用目标的显示形式而已。而像 Vec 和 HashMap 这样的容器则没有实现 Display,由于这些类型没有人类可读的单一自然形式。
  • 假如一个类型实现了 Display,那么标准库就会自动为它实现 std::str::ToString 特型,当你不需要 format! 的灵活性时,使用此特型的唯一方法 to_string 更方便:
    1. // 接续前面的例子
    2. assert_eq!(address.to_string(), "fe80::3ea9:f4ff:fe34:7a50");
    复制代码
    对于自己的类型,你通常应该实现 Display 而非 ToString
  • 标准库中的每个公共类型都实现了 std::fmt:ebug,这个特型会接受 一个值并将其格式化为对步调员有用的字符串。用 Debug 生成字符串的最 简朴方法是使用 format! 宏的 {:?} 格式说明符:
    1. // 接续前面的例子
    2. let addresses = vec![address,IpAddr::from_str("192.168.0.1")?];
    3. assert_eq!(format!("{:?}", addresses),"[fe80::3ea9:f4ff:fe34:7a50, 192.168.0.1]");
    复制代码
    这里使用了 Vec 对 Debug 的 通用实现。Rust 的全部聚集类型都有这样的实现。
    为自己的类型实现 Debug:
    1. #[derive(Copy, Clone, Debug)]
    2. struct Complex { re: f64, im: f64 }
    复制代码
format! 及其相关宏在把值格式化为文本时用到了很多格式化特型,Display 和 Debug 只是此中的两个
借用其他类似文本的类型



  • 切片和 String 都实现了 AsRef、AsRef<[u8]>、AsRef<ath> 和 AsRef<OsStr>。许多标准库函数会使用这些特型作为参数类型的限界,因此可以直接将切片和字符串传给它们。
  • 切片和字符串还实现了 std::borrow::Borrow<str> 特型。HashMap 和 BTreeMap 会借助 Borrow 令 String 很好地用作表中的键。
以 UTF-8 格式访问文本

获取表示文本的那些字节:


  • slice.as_bytes()(用作字节切片)  把 slice 的字节借入为 &[u8]。由于这不是可变引用,因此 slice 可以假定其字节将保持为格式良好的 UTF-8。
  • string.into_bytes()(转为字节切片)  获取 string 的全部权并按值返回字符串字节的 Vec<u8>。这是一个开销极低的转换,由于它只是移动了字符串不停用作缓冲区的 Vec<u8>。由于 string 已经不复存在,因此这些字节无须继承保持为格式良好的 UTF-8,而调用者可以随意修改 这个Vec<u8>。
从 UTF-8 数据生成文本

一个包含 UTF-8 数据的字节块,那么有几个方法可以将其转换为 String 或切片:


  • str::from_utf8(byte_slice)(来自 utf8 切片)  接受 &[u8] 字节切片并返回 Result:假如 byte_slice 包含格式良好的 UTF-8,就返回 Ok(&str),否则,返回错误。
  • String::from_utf8(vec)(来自 utf8 向量)  实验从按值通报的 Vec 中构造字符串。假如 vec 持有格式良好的 UTF-8,那么 from_utf8 就会返回 Ok(string),此中 string 会取得 vec 的全部权并将其用作缓冲区。此过程不会发生堆分配或文本复制。  假如这些字节不是有效的 UTF-8,则返回 Err(e),此中 e 是 FromUtf8Error 型的错误值。调用 e.into_bytes() 会返回原始向量 vec,因此当转换失败时Vec<u8>并不会丢失:
    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. // 由于String::from_utf8失败了,因此它不会消耗原始向量,
    7. // 而是通过错误值把原始向量原原本本地还给了我们
    8. assert_eq!(result.unwrap_err().into_bytes(),vec![0x9f, 0xf0, 0xa6, 0x80]);
    复制代码
  • String::from_utf8_lossy(byte_slice)(来自 utf8,宽松版)  实验从 &[u8] 共享字节切片构造一个 String 或 &str。此转换总会乐成,任何格式错误的 UTF-8 都会被 Unicode 代用字符替换。返回值是一个 Cow,假如它包含格式良好的 UTF-8,就会直接从 byte_slice 借用 &str,否则会拥有一个新分配的 String,此中格式错误的字节会被代用字符替换。因此,当 byte_slice 是格式良好的 UTF-8 时,不会发生堆分配或复 制。
  • String::from_utf8_unchecked(vec)(来自 utf8,不查抄版)  假如你确信此 Vec 包含格式良好的 UTF-8,那就可以调用这个不安全 的函数。此方法只是将 vec 包装为一个 String 并返回它,根本不查抄字节。 你有责任确保没有将格式错误的 UTF-8 引入系统,这就是此函数被标记为 unsafe 的原因。
  • str::from_utf8_unchecked(byte_slice)(来自 utf8,不查抄 版) 与上一个方法类似,但此方法会接受 &[u8] 并将其作为 &str 返回,而不查抄它是否包含格式良好的 UTF-8。与 String::from_utf8_unchecked 一 样,你有责任确保 byte_slice 是安全的。
推迟分配

让步调向用户打招呼。在 Unix 上,可以这样写:
  1. fn get_name() -> String {
  2. std::env::var("USER") // 在Windows上要改成"USERNAME"
  3. .unwrap_or("whoever you are".to_string())
  4. }
  5. println!("Greetings, {}!", get_name());
复制代码
对于 Unix 用户,这个步调会根据用户名向他们问好。对于 Windows 用户和无名 用户,它提供了备用文本。 std::env::var 函数会返回一个 String。但这意味着备用文本也必须作为 String 返回。这不太理想:当 get_name 返回静态字符串时,根本没须要分配内存。
题目的关键在于,get_name 的返回值偶然应该是拥有型 String,偶然则应该 是 &'static str,并且在运行步调之前我们无法知道会是哪一个。这种动态的特点预示着应该考虑使用 std::borrow::Cow,这个写入时克隆类型既可以 持有拥有型数据也可以持有借入的数据。
   假如 get_name 返回的是一个静态字符串(如默认值 "whoever you are"),我们不需要分配新的内存,由于我们可以直接借用静态字符串。只有当需要返回实际来自环境变量的值时,才会进行克隆
  Cow<'a, T> 是一个具有 Owned 和 Borrowed 两个变体的枚举。Borrowed 持有一个引用 &'a T,而 Owned 持有 &T 的拥有型版本: 对于 &str 是 String,对于 &[i32] 是 Vec,等等。无论是 Owned 还 是 Borrowed,Cow<'a, T> 总能生成一个 &T 供你使用。事实上,Cow<'a, T> 可以解引用为 &T,其行为类似于一种智能指针。
  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 返回。调用者可以保持不变
只要 T 实现了 std::fmt:isplay 特型,显示 Cow<'a, T> 的结果就和显 示 T 的结果是一样的。
当你可能需要也可能不需要修改借用的某些文本时,Cow 也很有用。不需要修改 时,可以继承借用。但是 Cow 的写入时克隆行为可以根据需要为你提供一个拥有型的、可变的值副本。Cow 的 to_mut 方法会确保 Cow 是 Cow::Owned,须要时会应用该值的 ToOwned 实现,然后返回对该值的可变引用。 因此,假如你发现某些用户(但不是全部)拥有他们更想使用的头衔,就可以这样写:
  1. fn get_title() -> Option<&'static str> { ... }
  2. let mut name = get_name();if let Some(title) = get_title() {
  3. name.to_mut().push_str(", ");
  4. name.to_mut().push_str(title);
  5. }
  6. println!("Greetings, {}!", name);
复制代码
out: Greetings, jimb, Esq.!
这样做的优点是,假如 get_name() 返回一个静态字符串并且 get_title 返回 None,那么 Cow 只是将静态字符串透传到 println!。你已经设法把内存分配推迟到了确有须要的时间。
由于 Cow 经常用于字符串,因此标准库对 Cow<'a, str> 有一些特殊支持。 它提供了来自 String 和 &str 的 From 和 Into 这两个转换特型,可以更简洁地编写 get_name :
  1. fn get_name() -> Cow<'static, str> {
  2. std::env::var("USER")
  3. .map(|v| v.into()) //to Cow<'static, str>
  4. .unwrap_or("whoever you are".into())
  5. }
复制代码
Cow<'a, str> 还实现了 std:ps::Add 和 std:ps::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 借用以前计算好的文本,直到需要复制为止。
把字符串当作泛型聚集

String 同时实现了 std::default:efault 和 std::iter::Extend:
default 返回空字符串,而 extend 可以把字符、字符串切片、Cow<…, str> 或字符串追加到一个字符串尾部。
&str 类型也实现了 Default,返回一个空切片。这在某些极端环境下很方便,比如,这样可以让包含字符串切片的布局派生于 Default(# [derive(Default)])。
格式化各种值

每个 {…} 都会被其后跟随的某个 参数的格式化形式替换


  • format! 宏会用它来构建 String。
  • println! 宏和 print! 宏会将格式化后的文本写入标准输出流。
  • writeln! 宏和 write! 宏会将格式化后的文本写入指定的输出流。
  • panic! 宏会使用它构建一个信息丰富的异常终止描述。
通过实现 std::fmt 模块的格式化特型来扩展这些宏以支持自己的类型。也可以使用 format_args! 宏和 std::fmt::Arguments 类型来让自己的函数和宏支持格式化语言。
格式化宏总会借入对其参数的共享引用,但永远不会拥有或修改它们。
模板的 {...} 形式称为格式参数,具体形式为 {which:how}。Which 和 how 都是可选的,很多时间用 {} 就行。


  • which(哪个)值用于选择模板背面的哪个实参应该取代该形参的位置。可以按 索引或名称选择实参。没有 which 值的形参只会简朴地从左到右与实参配对。
  • how(怎样)值表示应怎样格式化参数:怎样弥补、精度怎样、数值基数等。
输出中包含 { 或 } 字符,将模板中的这些字符连写两个
格式化文本值

当格式化像 &str 或 String(将 char 视为单字符字符串)这样的文本类型 时,参数的 how:


  • 文本长度限定。假如参数比这个值长,Rust 就会截断它。假如未指定限 制,Rust 就使用全文。
  • 最小字段宽度。在完成全部截断之后,假如参数比这个值短,Rust 就会在 右边(默认)用空格(默认)弥补它以让字段达到这个宽度。假如省略, Rust 则不会弥补参数。
  • 对齐方式。假如参数需要弥补空白以满足最小字段宽度,那么这个值表示应 将文本放置在字段中的什么位置。<、^ 和 > 分别会将文本放在开头、中心 和结尾。
  • 弥补过程中使用的弥补字符。假如省略,Rust 就会使用空格。假如指 定了弥补字符,则必须同时指定对齐方式。
Rust 的格式化步调对宽度的处置惩罚方式比较“简陋”:它假设每个字符占据一列,而 不会考虑组合字符、半角片假名、零宽度空格或 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}’ 这样的字符(组合重音符)需要做特殊处置惩罚。它精确地弥补了 第一个字符串,但假设第二个字符串是 4 列宽并且不需要弥补。Rust 在这种特定环境下进行改进,但要支持全部 Unicode 脚本的真正多语言文本格式化是一项艰巨的使命。最好依靠所在平台的用户界面工具包来处置惩罚,或许也可以通过生成 HTML 和 CSS,让 Web 浏览器来处置惩罚。有一个流行的 crate(unicode-width)可以部分处置惩罚这个题目。
除了 &str 和 String,也可以直接向格式化宏传入带有文本型引用目标的智能指针类型,比如 Rc 或 Cow<'a, str>。
由于文件名路径不一定是格式良好的 UTF-8,因此 std::path:ath 不完全 是文本类型,不能将 std::path:ath 直接传给格式化宏。不过,Path 有 个 display 方法会返回一个供格式化的 UTF-8 值,以适合所在平台的方式解决题目。
println!("processing file: {}", path.display());
格式化数值

当格式化参数具有 usize 或 f64 之类的数值类型时,参数的 how 值:


  • 弥补与对齐,它们和对文本类型的寄义一样。
  • + 字符,要求始终显示数值的符号,即使相应参数是正数。
  • # 字符,要求加显式基数前缀,比如 0x 或 0b。
  • 0 字符,要求通过在数值中包含前导零(而不是通常的弥补方式)来满足最小字段宽度。
  • 最小字段宽度。假如格式化后的数值没有这么宽,那么 Rust 会在左侧(默 认)用空格(默认)弥补它以构成给定宽度的字段。
  • 浮点参数的精度,指示 Rust 应在小数点后包含多少位数字。Rust 会根据需 要进行舍入或零扩展以生成要求的小数位。假如省略精度,那么 Rust 会尝 试使用尽可能少的数字来正确表示该值。对于整数类型的参数,精度会被忽 略。
  • 进制符号。对于整数类型,二进制是 b,八进制是 o,十六进制是小写字母 x 或大写字母 X。假如包含 # 字符,则它们会包含显式的 Rust 风格的基数 前缀 0b、0o、0x 或 0X。对于浮点类型,e 或 E 的基数需要科学记数法, 具有归一化系数,使用 e 或 E 作为指数。假如不指定任何进制符号,则 Rust 会将数值格式化为十进制。
格式化其他类型



  • 错误类型全都可以直接格式化,从而很容易地将它们包含在错误消息中。每 种错误类型都应该实现 std::error::Error 特型,该特型扩展了默认格 式化特型 std::fmt:isplay。因此,任何实现了 Error 的类型都可以 格式化。
  • 可以格式化 std::net::IpAddr、std::net::SocketAddr 等互联网协议所在类型。
  • 布尔值 true 和 false 也可以被格式化,虽然它们通常不是直接出现给终极用户的最佳格式。
格式化值以进行调试

为了资助调试和记录日志,{:?} 参数能以对步调员有资助的方式格式化 Rust 标准库中的任何公共类型。你可以使用它来查抄向量、切片、元组、哈希表、线程和其他数百种类型
  1. use std::collections::HashMap;
  2. let mut map = HashMap::new();
  3. map.insert("Portland", (45,-122));
  4. map.insert("Shanghai", (31, 121));
  5. println!("{:?}", map);
复制代码
out:
  1. {"Shanghai": (31, 121), "Portland": (45, -122)}
复制代码
代码改成 println!(“{:#?}”, map) 会输出:
  1. {
  2.      "Shanghai": (
  3.          31,
  4.          121
  5.      ),
  6.      "Portland": (
  7.          45,
  8.          -122
  9.      )
  10. }
复制代码
  1. println!("ordinary: {:02?}", [9, 15, 240]);
  2. println!("hex: {:02x?}", [9, 15, 240]);
复制代码
  1. ordinary: [09, 15, 240]
  2. hex: [09, 0f, f0]
复制代码
用 #[derive(Debug)] 语法让自己的类型支持 {:?}
  1. #[derive(Copy, Clone, Debug)]
  2. struct Complex { re: f64, im: f64 }
复制代码
格式化指针以进行调试

将任何种类的指针传给格式化宏(引用、Box 或 Rc),宏都会简朴地追踪指针并格式化它的引用目标,指针自己并不紧张。
{:p} 表示法会将引用、Box 和其他类似指针的类型格式化为所在
按索引或名称引用参数

格式形参可以明确选择它要使用的参数
  1. assert_eq!(format!("{1},{0},{2}", "zeroth", "first", "second"),"first,zeroth,second");
复制代码
可以在冒号后包含格式参数:
  1. assert_eq!(format!("{2:#06x},{1:b},{0:=>10}", "first", 10,100),"0x0064,1010,=====first");
复制代码
按名称选择参数。这能让有许多参数的复杂模板更加清晰易读:
  1. assert_eq!(format!("{description:.<25}{quantity:2} @ {price:5.2}",price=3.25,quantity=3,description="Maple Turmeric Latte"),
  2. "Maple Turmeric Latte..... 3 @ 3.25");
复制代码
在单个格式化宏中将索引型参数、命名型参数和位置型(没有索引或名称 的)参数混用。位置型参数会从左到右与参数配对,就仿佛索引型参数和命名型 参数不存在一样(不参与位置编号):
  1. assert_eq!(format!("{mode} {2} {} {}","people", "eater", "purple", mode="flying"),
  2. "flying purple people eater");
复制代码
动态宽度与动态精度

参数的最小字段宽度、文本长度限定和数值精度不必总是固定值,也可以在运行期进行选择。 下面这个表达式会生成在 20 个字符宽的字段中右对齐的字符串 content:format!("{:>20}", content)
想在运行期选择字段宽度,则可以这样写:
format!("{:>1$}", content, get_width()) 1$ 就是在告诉 format! 使用第二个参数的值作为宽度。 它引用的参数必须是 usize
用于文本长度限定:
  1. format!("{:>width$.limit$}", content,width=get_width(), limit=get_limit())
复制代码
要代替文本长度限定或浮点精度,还可以写成 *,表示将下一个位置参数作为精 度。下面的代码会把 content 裁剪成最多 get_limit() 个字符:
format!("{:.*}", get_limit(), content)
用作精度的参数必须是 usize。
格式化自己的类型

自行 实现std::fmt中的特型中的一个或多个,就可以让 Rust 的格式化宏来格式化你的类型
将 #[derive(Debug)] 属性放在类型定义上,以期支持 {:?} 格式参数 时,其实只是在要求 Rust 替你实现 std::fmt:ebug 特型。
  1. trait Display {
  2. fn fmt(&self, dest: &mut std::fmt::Formatter)
  3. -> std::fmt::Result;
  4. }
复制代码
fmt 方法的使命是为 self 生成格式良好的表达形式并将其字符写入 dest。除 了用作输出流,dest 参数还携带着从格式参数解析出的详细信息,比如对齐方 式和最小字段宽度。
Complex 值实现通常的 a + bi 形式打印:
  1. use std::fmt;
  2. impl fmt::Display for Complex {
  3. fn fmt(&self, dest: &mut fmt::Formatter) -> fmt::Result {
  4.      let im_sign = if self.im < 0.0 { '-' } else { '+' };
  5.      write!(dest, "{} {} {}i", self.re, im_sign, f64::abs(self.im))
  6.     }
  7. }
复制代码
Formatter 自己就是一个输出流的事实,以是 write! 宏可以帮我 们完成大部分工作
  1. let one_twenty = Complex { re: -0.5, im: 0.866 };
  2. assert_eq!(format!("{}", one_twenty), "-0.5 + 0.866i");
复制代码
在自己的代码中使用格式化语言

使用 Rust 的 format_args! 宏和 std::fmt::Arguments 类型,你可以编写能接受格式模板和参数的自定义函数和宏。
步调需要在运行期记录状态消息,使用 Rust 的文本格式化语言来生成这些消息:
  1. fn logging_enabled() -> bool { ... }
  2. use std::fs::OpenOptions;
  3. use std::io::Write;
  4. fn write_log_entry(entry: std::fmt::Arguments) {
  5. if logging_enabled() {
  6.         // 尽量保持简单,所以每次只是打开文件
  7.      let mut log_file = OpenOptions::new().append(true).create(true).open("log-file-name").expect("failed to open log file");
  8.      log_file.write_fmt(entry).expect("failed to write to log");
  9. }
  10. }
  11. write_log_entry(format_args!("Hark! {:?}\n", mysterious_value));
复制代码
在编译期,format_args! 宏会解析模板字符串并据此查抄参数的类型,假如有任何题目则陈诉错误。在运行期,它会对参数求值并构建一个 Arguments 值,此中包含格式化文本时需要的全部信息:模板的预解析形式,以及对参数值 的共享引用。
构造一个 Arguments 值的代价很低:只是网络一些指针而已。这时尚未进行任 何格式化工作,仅网络稍后要用到的信息。这很紧张,否则假如未启用日志,那么像把数值转换为十进制、弥补值之类的任何开销都会白白浪费。
File 类型实现了 std::io::Write 特型,该特型的 write_fmt 方法会接受 一个 Argument 并进行格式化,然后会将结果写入底层流。 对 write_log_entry 的调用并不美丽。这时宏就可以大显技艺了:
  1. macro_rules! my_log { // 在宏定义中的宏名后不需要叹号(!)
  2. ($format:tt, $($arg:expr),*) => (
  3.         write_log_entry(format_args!($format, $($arg),*))
  4. )
  5. }
复制代码
定义了一个新 log! 宏并将其参数 传给 format_args!,然后在生成的 Arguments 值上调用 write_log_entry 函数即可。诸如 println!,writeln! 和 format! 之 类的格式化宏都采用了大抵相同的思绪。 可以像这样使用 log!:
  1. log!("O day and night, but this is wondrous strange! {:?}\n",mysterious_value);
复制代码
正则表达式

外部的 regex crate 是 Rust 的官方正则表达式库,它提供了通常的搜索函数和 匹配函数。
该库对 Unicode 有很好的支持,但它也可以搜索字节串。只管不支持 其他正则表达式包中的某些特性(比如反向引用和环视模式),但这些简化答应 regex 确保搜索时间始终与表达式的大小、表达式的长度和待搜文本的长度呈 线性关系。别的,这些保证还让 regex 即使在搜索不可信文本的不可信表达式 时也能安全地使用。 本书将只提供 regex 的概述。有关详细信息,可以查阅其在线文档。
只管 regex crate 不在 std 中,由 Rust 库团队维护的,该团队也负责维 护标准库 std。要使用 regex,添加依靠在 crate 的 Cargo.toml 文 件的 [dependencies] 部分:
regex = "1"
Regex的根本用法

Regex 值表示已经解析好的正则表达式。Regex::new 构造函数会实验将 &str 解析为正则表达式,并返回一个 Result:
  1. use regex::Regex;
  2. // 注意,使用原始字符串语法r"..."是为了避免一大堆反斜杠
  3. let semver = Regex::new(r"(\d+)\.(\d+)\.(\d+)(-[-.[:alnum:]]*)?")?;
  4. // 简单搜索,返回布尔型结果
  5. let haystack = r#"regex = "0.2.5""#;
  6. assert!(semver.is_match(haystack));
复制代码
Regex::captures 方法会在字符串中搜索第一个匹配项并返回一个 regex::Captures 值,此中包含表达式中每个组的匹配信息:
  1. let captures = semver.captures(haystack).ok_or("semver regex should have matched")?;
  2. assert_eq!(&captures[0], "0.2.5");
  3. assert_eq!(&captures[1], "0");
  4. assert_eq!(&captures[2], "2");
  5. assert_eq!(&captures[3], "5");
复制代码
假如所请求的组不匹配,则对 Captures 值进行索引就会出现 panic。要测试特 定组是否匹配,可以调用 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::new 构造函数的开销可能很高:在速率较快的开辟机器上为 1200 个字 符的正则表达式构造一个 Regex 会耗费差不多 1 毫秒时间,即使是一个微不足 道的表达式也要耗费几微秒时间。最好让 Regex 构造远离繁重的计算循环,这就意味着应该只构建一次 Regex,然后重复使用它。
lazy_static crate 提供了一种在首次使用时惰性构造静态值的好办法。首 先,请留意 Cargo.toml 文件中的依靠项:
  1. [dependencies]
  2. lazy_static = "1"
复制代码
这个 crate 提供了一个宏来声明这样的变量
  1. use lazy_static::lazy_static;
  2. lazy_static! {
  3. static ref SEMVER: Regex
  4.         = Regex::new(r"(\d+)\.(\d+)\.(\d+)(-[-.[:alnum:]]*)?")
  5.                 .expect("error parsing regex");
  6. }
复制代码
该宏会扩展成名为 SEMVER 的静态变量的声明,但其类型不完全是 Regex,而 是一个实现了 Deref 的由宏生成的类型,并公开了与 Regex 相同的全部方法。第一次解引用 SEMVER 时,会执行初始化步调,并保存该值供以后使用。由于 SEMVER 是一个静态变量,而不仅仅是局部变量,因此每次执行步调时初始化器都最多运行一次。
  1. use std::io::BufRead;
  2. let stdin = std::io::stdin();
  3. for line_result in stdin.lock().lines() {
  4. let line = line_result?;
  5. if let Some(match_) = SEMVER.find(&line) {
  6.         println!("{}", match_.as_str());
  7. }
  8. }
复制代码
可以把 lazy_static! 声明放在模块中,甚至可以放在使用 Regex 的函数内部。无论采用哪种方式,每当步调执行时, 正则表达式都只会编译一次。
规范化

法语单词 thé(意为“茶”)的长度是 3 个字符。然而, Unicode 实际上有两种方式来表示这个单词。


  • 在组合形式中,“thé”包含 3 个字符,即 ‘t’、‘h’ 和 ‘é’,此中 ‘é’ 是码 点为 0xe9 的单个 Unicode 字符。
  • 在分解形式中,“thé”包含 4 个字符,即 ‘t’、‘h’、‘e’ 和 ‘\u{301}’, 此中的 ‘e’ 是纯 ASCII 字符,没有重音符号,而码点 0x301 是“联合性锐音符号”字符,它会为它前面的任意字符添加一个锐音符号
Unicode 并不以为 é 的组合形式或分解形式是“精确的”形式,相反,它以为它们是同一抽象字符的等价表示。Unicode 规定这两种形式应该以相同的方式显示, 并且答应文本输入法生成任何一种形式,因此用户通常不知道他们正在检察或输 入的是哪种形式。(Rust 答应直接在字符串字面量中使用 Unicode 字符,因此 假如不关心自己得到的是哪种编码,则可以简朴地写成 “thé”。但为了清晰起见,这里我们会使用 \u 转义符。)
然而,作为 Rust 的 &str 值或 String 值,“th\u{e9}” 和 “the\u{301}” 是完全不同的。它们具有不同的长度,比较起来不相等,具有不同的哈希值,并且相对于其他字符串会以不同的方式排序:
  1. assert!("th\u{e9}" != "the\u{301}");
  2. assert!("th\u{e9}" > "the\u{301}");
  3. // 哈希器旨在累积求出一系列值的哈希值,因此仅哈希一个值有点儿大材小用
  4. use std::hash::{Hash, Hasher};
  5. use std::collections::hash_map::DefaultHasher;
  6. fn hash<T: ?Sized + Hash>(t: &T) -> u64 {
  7.      let mut s = DefaultHasher::new();
  8.      t.hash(&mut s); s.finish()
  9. }
  10. // 这些值可能会在将来的Rust版本中发生变化
  11. assert_eq!(hash("th\u{e9}"), 0x53e2d0734eb1dff3);
  12. assert_eq!(hash("the\u{301}"), 0x90d837f0a0928144);
复制代码
显然,假如计划比较用户提供的文本或者将其用作哈希表或 B 树中的键,则需 要先将每个字符串转换成某种规范形式。 荣幸的是,Unicode 指定了字符串的规范化形式。每当根据 Unicode 规则应将两个字符串视为等同时,它们的规范化形式是逐字符全同的。当使用 UTF-8 编码 时,它们是逐字节全同的。这意味着可以使用 == 来比较规范化后的字符串,可以将它们用作 HashMap 或 HashSet 中的键,等等,这样就能得到 Unicode 规定的相等性概念了。
假如未做规范化,则甚至会产生安全隐患。假如你的网站对用户名在某些环境下 做了规范化,但在其他环境下未做规范化,那么终极可能会出现两个名为 bananasflambé 的不同用户,你的一部分代码会将其视为同一用户,但另一部 分代码会以为这是两个用户,导致一个人的权限被错误地扩展到另一个人身上。 当然,有很多方法可以避开这种题目,但汗青表明也有很多方法不能避开。
规范化形式

Unicode 定义了 4 种规范化形式,每一种都实用于不同的用途。


  • 第一个题目是:你更喜欢让字符尽可能组合表示照旧尽可能分解表示? 比方,越南 语单词 Phở 最常用的组合表示是三字符字符串 "h\u{1edf}",此中声调 标记 和元音标记 都应用于根本字符“o”上,而其单个 Unicode 字符是 ‘\u{1edf}’,Unicode 很质朴地将其命名为“带角和钩形的拉丁文小写字 母 o”。 最常用的分解表示是将根本字母及其两个标记拆分为 3 个单独的 Unicode 字符:‘o’、‘\u{31b}’(组合角符)和 ‘\u{309}’(组合中计符),其结果就是 "ho\u{31b}\u{309}"。(每当组合标记作为单独的字符出 现,而不是作为组合字符的一部分时,全部规范化形式都指定了它们必须以 固定顺序出现,因此即使字符有多个重音符号,也能很好地进行规范化。) 组合形式通常具有较少的兼容性题目,由于它更接近于在 Unicode 创建之前用于其文本的大多数语言的表示。它也可以更好地与简朴的字符串格式化特性(如 Rust 的 format! 宏)协作。而分解形式可能更适合显示文本或搜索,由于它使文本的详细布局更加明确。
  • 第二个题目是:假如两个字符序列表示相同的根本文本,但文本的格式化方式不同,那么你是要将它们视为等同的照旧坚持以为有差异? Unicode 对平凡数字 5、上标数字 ⁵(或 ‘\u{2075}’)和带圆圈的数字 ⑤(或 ‘\u{2464}’)都有单独的字符,但声明这 3 个字符是兼容性等效 的。类似地,Unicode 对连字 ffi(‘\u{fb03}’)也有一个单字符,但声明 这与三字符序列 ffi 兼容性等效。 兼容性等效对搜索很有意义:搜索仅使用了 ASCII 字符的 “difficult”, 应该匹配使用了 ffi 连字符的字符串 “di\u{fb03}cult”。对后一个字符 串应用兼容性分解会将连字替换为 3 个纯字母 “ffi”,从而让搜索更容易。但是将文本规范化为其兼容的等效形式可能会丢失紧张信息,因此不应马虎应用。比方,在大多数环境下将 “2⁵” 存储为 “25” 是不精确的。
Unicode 规范化形式 C(NFC)和规范化形式 D(NFD)会使用每个字符的最大组合形式和最大分解形式,但不会试图同一兼容性等价序列。NFKC 规范化形式 和 NFKD 规范化形式类似于 NFC 和 NFD,但它们会将全部兼容性等效序列规范 化为各自的一些简朴表示法。 万维网同盟的“WWW 字符模子”建议对全部内容都使用 NFC。Unicode 标识符和模式语法附件则建议使用 NFKC 作为编程语言中的标识符,并提供了在须要时适 配此形式的原则。
unicode-normalization crate

ust 的 unicode-normalization crate 提供了一个特型,可以将方法添加到 &str 中,以便将文本转成四种规范化形式中的任何一种。要使用这个 crate, 添加依靠到 Cargo.toml 文件的 [dependencies] 部分:
  1. unicode-normalization = "0.1.17"
复制代码
有了这个声明,&str 就有了 4 个新方法,它们会返回字符串的特定规范化形式 的迭代器:
  1. use unicode_normalization::UnicodeNormalization;
  2. // 不管左边的字符串使用哪种表示形式(无法仅仅通过观察得知),这些断言都成立
  3. assert_eq!("Phở".nfd().collect::<String>(), "Pho\u{31b}\u{309}");
  4. assert_eq!("Phở".nfc().collect::<String>(), "Ph\u{1edf}");
  5. // 左侧使用了"ffi"连字符
  6. assert_eq!("① Di\u{fb03}culty".nfkc().collect::<String>(), "1 Difficulty");
复制代码
接受规范化的字符串并以相同的形式再次对其进行规范化可以保证返回相同的文本。 只管规范化字符串的任何子字符串自己也是规范化的,但两个规范化字符串拼接 起来不一定是规范化的。比方,第二个字符串可能以组合字符开头,并且这个字 符按规范应该排在第一个字符串末端的组合字符之前。 只要文本在规范化时没有使用未分配的码点,Unicode 就承诺其规范化形式在标准的将来版本中不会改变。这意味着规范化形式通常可以安全地用于持久存储, 即使 Unicode 标准在不断发展也不会受影响。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

泉缘泉

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表