【Rust 笔记】15-字符串与文本(下)

15.4 - 格式化值

  • 格式化宏:总是借用对其参数的共享引用,不会取得所有权,也不会修改它们。

  • 只要实现了

    std::fmt
    

    模块的格式化特型,就可以扩展下述宏,以支持自定义类型:

    • format!:使用模板来构建 String
    • println!print!:将格式化后的文本写入标准输出流。
    • writeln!write!:将格式化后的文本写入指定输出流。
    • panic!:使用模板构建一个终止诧异的表达式,可以包含自定义的信息。
    • format_args! 宏和 std::fmt::Arguments 类型:可以写出自定义的支持格式化语言的函数和宏。
  • 模板中的每个 {},都会被后面某个参数的格式化形式取代。模板字符串必须是常量。

  • 模板的

    {}
    

    部分称为格式化形参,形式为

    {which:how}
    

    • whichhow 是可选的。
    • 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}" insn="adc #42", lsb = 105, msb = 42 "69 2a adc #42"

15.4.1 - 格式化文本值

格式化 &strStringchar 等同于只有一个字符的字符串)文本类型时,格式化形参 how 部分可以包含如下可选内容:

使用的特性 模板字符串 结果
默认 "{}" "bookends"
最小字段宽度 "{: 4}" "bookends"
"{: 12}" "bookends "
文本长度限制 "{:. 4}" "book"
"{:. 12}" "bookends"
字段宽度及长度限制 "{: 12.20}" "bookends "
"{: 4.20}" "bookends"
"{: 4.6}" "booken"
"{: 6.4}" "book "
左对齐,字段宽度 "{: <12}" "bookends "
居中对齐,字段宽度 "{: ^12}" " bookends "
右对齐,字段宽度 "{: >12}" " bookends"
填充 =,居中对齐,字段宽度 "{: =^12}" "==bookends=="
填充 *,右对齐,字段宽度,长度限制 "{: *>12.4}" "********book"
  • &strString 之外,也可以给格式化宏传入引用目标为文本的智能指针类型,比如 Rc<String>Cow<'a, str>

  • 不能直接把文件名路径 std::path::Path 传给格式化宏,但可以使用 Pathdispaly 方法返回的值进行格式化。

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

15.4.2 - 格式化数值

当格式化参数具有 usizef64 数值类型时,形参 how 的值必须包含,有以下可选组成部分。

  • 格式化数值
使用的特性 模板字符串 结果
默认 "{}" "1234"
强制符号 "{: +}" "+1234"
最小字段宽度 "{: 12}" " 1234"
"{: 2}" "1234"
符号,宽度 "{: +12}" " +1234"
前置零,宽度 "{: 012}" "000000001234"
符号,前置零,宽度 "{: +012}" "+00000001234"
左对齐,宽度 "{: <12}" "1234 "
居中对齐,宽度 "{: ^12}" " 1234 "
右对齐,宽度 "{: >12}" " 1234"
左对齐,符号,宽度 "{: <+12}" "+1234 "
居中对齐,符号,宽度 "{: ^+12}" " +1234 "
右对齐,符号,宽度 "{: >+12}" " +1234"
填充 =,居中对齐,宽度 "{: =^12}" "====1234===="
二进制计数法 "{: b}" "100110100010"
宽度,八进制计数法 "{: 12o}" " 2322"
符号,宽度,十六进制计数法 "{: +12x}" " +4d2"
符号,宽度,大写十六进制计数法 "{: +12X}" " +4D2"
符号,基数前缀,宽度,十六进制 "{: +#12x}" " +0x4d2"
符号,基数,补零,宽度,十六进制 "{: +#012x}" "+0x0000004d2"
"{: +#06x}" "+0x4d2"
  • 格式化浮点数
使用的特性 模板字符串 结果
默认 "{}" "1234.5678"
精度 "{:. 2}" "1234.57"
"{:. 6}" "1234.567800"
最小字段宽度 "{: 12}" " 1234.5678"
宽度,精度 "{: 12.2}" " 1234.57"
"{: 12.6}" " 1234.567800"
前置零,宽度,精度 "{: 012.6}" "01234.567800"
科学计数法 "{: e}" "1.234578e3"
科学计数法,精度 "{: 3e}" "1.235e3"
科学计数法,宽度,精度 "{: 12.3e}" " 1.235e3"
"{: 12.3E}" " 1.235E3"

15.4.3 - 格式化其他类型

  • 每种错误类型都应该实现 std::error::Error 特型,该特型扩展支持了默认的格式化特型 std::fmt::Display。任何实现 Error 的类型都是可以格式化的。
  • std::net::IpAddrstd::net::SocketAddr 类型:可以格式化互联网协议地址。
  • truefalse 布尔值:可以直接格式化。

15.4.4 - 为调试格式化值

  • {:?} 格式:可以调试和输出日志,也支持格式化任意 Rust 标准库中的公共类型。

    • 可以用来检查向量、切片、元组、散列表、线程等数百种类型。

    • 使用#[derive(Debug)] 语法,可以让自定义类型支持 {:?}

      #[derive(Copy, Clone, Debug)]
      struct Complex {
              
              
          r: f64,
          i: f64
      }
      let third = Complex {
              
               r: -0.5, i: f64::sqrt(0.75) };
      println!("{:?}", third);
      // 输出结果
      Complex {
              
               r: -0.5, i: 0.8660254037544386 }
      
  • #字符:在格式化形参中添加,用来美化打印输出结果。

    use std::collections::HashMap;
    
    let mut map = HashMap::new();
    map.insert("Portland", (45.5237606, -122.6819273));
    map.insert("Shanghai", (25.0375167, 121.5637));
    println!("{:?}", map);
    // 输出结果
    {
          
          "Shanghai": (25.0375167, 121.5637), "Portland": (45.5237606, -122.6819273)}
    
    println!("{: #?}", map);
    // 输出结果
    {
          
          
        "Shanghai": (
            25.0375167,
            121.5637
        ),
        "Portland": (
            45.5237606,
            -122.6819273
        )
    }
    

15.4.5 - 为调试格式化指针

{: p} 符号:用于将引用、Box 和其他指针类型格式化为地址:

use std::rc::Rc;

let original = Rc::new("mazurka".to_string());
let cloned = original.clone();
let impostor = Rc::new("mazurka".to_string());
println!("text:    {}, {}, {}", original, cloned, impostor);
println!("pointers: {: p}, {: p}, {: p}", original, cloned, impostor);

// 输出结果
text:    mazurka, mazurka, mazurka
pointers: 0x7f99af80e000, 0x7f99af80e000, 0x7f99af80e030

15.4.6 - 通过索引或名字引用参数

  • 格式化形参可以使用索引显示选择它使用的参数。

    assert_eq!(format!("{1}, {0}, {2}", "zeroth", "first", "second"), "first, zeroth, second");
    
  • 冒号后面可以再跟其他格式化形参。

    assert_eq!(format!("{2:#06x}, {1:b}, {0:=>10}", "first", 10, 100), "0x0064, 1010, =====first");
    
  • 处理通过索引选择参数,还可以使用变量名称。

    assert_eq!(format!("{descirption:. <25} {quantity: 2} @ {price: 5.2}",
        price = 3.25,
        quantity = 3,
        description = "Maple Turnmeric Latte"
    ),
        "Maple Turnmeric Latte..... 3 @ 3.25"
    );
    
  • 还可以在一个格式化宏中混用索引、名字和位置参数。位置参数从左往右匹配,不考虑已有的索引和命名参数。

    assert_eq!(format!("{mode} {2} {} {}", "people", "eater", "purple", mode = "flying"),
        "flying purple people eater"
    );
    
  • 混合使用时,命名参数必须放在列表末尾。

15.4.7 - 动态宽度与精度

  • 1$ 作为最小字段宽度:告诉 format! 使用第二个参数的值作为宽度

    format!("{: >1$}", content, get_width());
    
  • 引用的参数必须是 usize。还可以通过名字来引用参数:

    format!("{: >width$}", content, width=get_width());
    
  • 同样也支持文本长度限制。

    format!("{: >width$.limit$}", content, width=get_width(), limit=get_limit());
    
  • * 符号:表示取得下一个位置上的参数作为精度。

    format!("{:. *}", get_limit(), content);
    
  • 作为精度的参数必须是 usize。字段宽度没有对应的语法。

15.4.8 - 格式化自定义类型

  • 格式化宏使用

    std::fmt
    

    模块中定义的一组特型,将值转换为文本。

    • 如果自定义其中的一个或多个特型,就可以让自定义类型实现格式化宏的格式。
    • 格式形参的记号表示其参数类型必须实现哪个特型。
记号 示例 特型 用途
{} std::fmt::Display 文本、数值、错误: 兜底的特型
b {: #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::Debug 特型。

    • 格式化特型的结构都一样,只是名字不同,如下的 std::fmt::Display 特型:

      trait Display {
              
              
          fn fmt(&self, dest: &mut std::fmt::Formatter) -> std::fmt::Result;
      }
      
    • Display 实现 Complex

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

15.4.9 - 在代码中使用格式化语言

使用 format_args! 宏和 std::fmt::Arguments 类型,在示例代码中,编写接收格式化模板和参数的函数和宏。

fn logging_enabled() -> bool {
    
    
    ...
}

use std::fs::OpenOptions;
use std::io::Write;

fn write_log_entry(entry: std::fmt::Arguments) {
    
    
    if logging_enabled() {
    
    
        let mut log_file = OpenOptions::new()
            .append(true)
            .create(true)
            .open("log-file-name")
            .expect("failed to open log fie");
        
        log_file.write_fmt(entry)
            .expect("failed to write to log");
    }
}

// 实现一个调用write_log_entry的宏
macro_rules! log {
    
    
    ($format: tt, $($arg: expr), *) => (
        write_log_entry(format_args!($format, ${
    
    $arg}, *))
    )
}

fn main() {
    
    
    write_log_entry(format_args!("Hard! {:?}\n", mysterious_value)); // 直接调用
    log!("O day and night, but this is wondrous strange! {:?}\n", mysterious_value);
}
  • 编译时,format_args! 宏会解析模板字符串,并按照参数类型对其进行检查,并在发现问题时报错。
  • 运行时,format_args! 宏会求值参数,并构建一个带有所有格式化文本必需信息的 Arguments 值,包括模板的预解析形式,以及对参数值的共享引用。
  • File 类型实现了 std::io::Write 特型,其 write_fmt 方法接收 Argument 参数,并进行格式化,之后再把结果写入底层流。

15.5 - 正则表达式

  • regex 包是 Rust 官方的正则表达式库,提供了常用的搜索和匹配功能。

  • 要使用 regex,需要在 Cargo.toml 文件的 [dependencies] 部分添加如下代码:

    regex = "1"
    
    • 然后在包的底层加一个 extern crate 特性项

      extern crate regex;
      

15.5.1 - 基本用法

  • Regex 值表示解析之后的正则表达式。

  • Regex::new 构造函数:将传入的 &str 作为正则表达式解析,返回 Result

    use regex::Regex;
    
    // 使用r"..."原始字符串语法,可以避免多反斜杠的转义
    let semver = Regex::new(r"(\d+)\.(\d+)\.(\d+) (-[-.[: alnum:]] *)?")?;
    
    // 简单搜索,返回布尔值结果
    let haystack = r#"regex = "0.2.5""#;
    assert!(semver.is_match(haystack));
    
  • Regex::captures 方法:

    • 从字符串中搜索第一个匹配,返回的 regex::Captures 值,包含正则表达式中每一组对应的匹配信息。

      let captures = semver.captures(haystack)
          .ok_or("semver regex should have matched")?;
      assert_eq!(&captures[0], "0.2.5");
      assert_eq!(&captures[1], "0");
      assert_eq!(&captures[2], "2");
      assert_eq!(&captures[3], "5");
      
    • 要测试某个特定组是否有匹配结果,可以调用 Captures::get,会返回 Option<regex::Match>Match 值记录一个捕获组的匹配信息。

      assert_eq!(captures.get(4), None);
      assert_eq!(captures.get(3).unwrap().start(), 13);
      assert_eq!(captures.get(3).unwrap().end(), 14);
      assert_eq!(captures.get(3).unwrap().as_str(), "5");
      
  • 可以遍历一个字符串中的所有匹配:

    let haystack = "In the beginning there was 1.0.0. \
        For a while, we used 1.0.1-beta, \
        but in the end, we settled on 1.2.4.";
    let matches: Vec<&str> = semver.find_iter(haystack)
        .map(|match_| match_.as_str())
        .collect();
    assert_eq!(matches, vec!["1.0.0", "1.0.1-beta", "1.2.4"]);
    
    • find_iter 迭代器:为表达式的每个不重叠匹配,分别生成一个 Match 值,从字符串开头到末尾。
    • captures_iter 方法也能实现上述功能,但是生成的记录是所有捕获组的 Captures 值。记录捕获组会导致搜索变慢。

15.5.2 - 构建 Regex 值

  • Regex::new 构造函数的性能较差,最好不要在大计算量的循环中构建 Regex,建议只构建一次,然后重用。

  • lazy_static 包提供了首次使用时,懒构建静态值的方法。要使用这个包,需要在 Cargo.toml 中加上:

    [dependencies]
    lazy_static = "1.4.0"
    
  • lazy_static 包提供了声明变量用的宏:

    #[marcro_use]
    extern crate lazy_static;
    
    lazy_static! {
          
          
        static ref SEMVER: 
            Regex = Regex::new(r"(\d+)\.(\d+)\.(\d+) (-[-.[: alnum:]] *)?")
            .expect("error parsing regex");
    }
    
  • 调用 SEMVER:正则表达式只会在程序运行时编译一次。

    use std::io::BufRead;
    
    let stdin = std::io::stdin();
    for line in stdin.locak().lines() {
          
          
        let line = line?;
        if let Some(match_) = SEMVER.find(&line) {
          
          
            println!("{}", match_.as_str());
        }
    }
    

15.6 - 规范化

  • 规范化:如果两个字符串按照 Unicode 的规则应该判定为等价,那么它们规范化的形式应该每个字符都相同。
    • 在以 UTF-8 编码的情况下,每个字节都相同。
    • 可以使用 == 来比较规范化的字符串。
    • 可以将它们作为 HashMapHashSet 的键。
  • 不进行规范化,会导致安全隐患。

15.6.1 - 规范化形式

  • 分解表示形式:更适合显示文本或搜索,因为可以呈现更多细节。
    • 对于越南语 Phô,把基本字符跟它的两个记号分开表示成 3 个独立的 Unicode 字符。
    • 'o'\u{31b}(COMBINING HORN,组合角号)和 \u{309}(COMBINING HOOK ABOVE,组合上钩号)。
    • 最终结果是 Pho\u{31b}\u{309}
  • 组合表示形式:format! 宏之类的简单字符串格式化特型。
  • 将文本规范化为兼容性等效形式,有可能丢失重要信息。比如 2^5,可能会忽略上标 5 的格式,而直接保存为 25
  • 4 种规范化形式:
    • Unicode NFC(Normalization From C,规范化形式 C):对每个字符串应用最大化组合。W3C 建议对所有内容使用 NFC。
    • Unicode NFD(Normalization From D,规范化形式 D):对每个字符串应用最大化分解。
    • Unicode NFKC:将所有兼容性等效序列规范化为组合形式。建议对编程语言的标识符使用。
    • Unicode NFKD:将所有兼容性等效序列规范化为分解形式

15.6.2-unicode-normalization 包

  • unicode-normalization 包的特型,可以给 &str 添加把文本转换为任意 4 中规范化形式的方法。

    [dependencies]
    unicode-normalization = "0.1.8"
    
  • 包的顶部文件,需要添加 extern crate 声明:

    extern crate unicode_normalization;
    
  • 如此可以对 &str 调用相应转化为不同规范化形式的迭代器的方法。

    use unicode_normalization::UnicodeNormalization;
    
    assert_eq!("Phô".nfd().collect::<String>(), "Pho\u{31b}\u{309}");
    assert_eq!("Phô".nfc().collect::<String>(), "Pho\u{1edf}");
    
    // "ffi"连字符
    assert_eq!("① Di\u{fb03culty}".nfkc().collect::<String>(), "1 Difficulty");
    
  • 两个规范化的字符串拼接起来,未必是规范化的。

  • Unicode 规则:只要文本在规范化时没有使用未分配的码点,那其规范化形式在标准的未来版本中就不会改变。

  • 规范化形式通常适合持久存储数据。


详见《Rust 程序设计》(吉姆 - 布兰迪、贾森 - 奥伦多夫著,李松峰译)第十七章
原文地址

猜你喜欢

转载自blog.csdn.net/feiyanaffection/article/details/125575203
今日推荐