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


    15.4 - 格式化值

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

    • 只要实现了

      std::fmt
      
      • 1

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

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

    • 模板的

      {}
      
      • 1

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

      {which:how}
      
      • 1

      • 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());
      
      • 1

    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 }
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
    • #字符:在格式化形参中添加,用来美化打印输出结果。

      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
          )
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

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

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

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

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

    15.4.7 - 动态宽度与精度

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

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

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

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

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

    15.4.8 - 格式化自定义类型

    • 格式化宏使用

      std::fmt
      
      • 1

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

      • 如果自定义其中的一个或多个特型,就可以让自定义类型实现格式化宏的格式。
      • 格式形参的记号表示其参数类型必须实现哪个特型。
    记号示例特型用途
    {}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;
        }
        
        • 1
        • 2
        • 3
      • 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))
            }
        }
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12

    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 编译时,format_args! 宏会解析模板字符串,并按照参数类型对其进行检查,并在发现问题时报错。
    • 运行时,format_args! 宏会求值参数,并构建一个带有所有格式化文本必需信息的 Arguments 值,包括模板的预解析形式,以及对参数值的共享引用。
    • File 类型实现了 std::io::Write 特型,其 write_fmt 方法接收 Argument 参数,并进行格式化,之后再把结果写入底层流。

    15.5 - 正则表达式

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

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

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

        extern crate regex;
        
        • 1

    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));
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
    • 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");
        
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
      • 要测试某个特定组是否有匹配结果,可以调用 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");
        
        • 1
        • 2
        • 3
        • 4
    • 可以遍历一个字符串中的所有匹配:

      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"]);
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • find_iter 迭代器:为表达式的每个不重叠匹配,分别生成一个 Match 值,从字符串开头到末尾。
      • captures_iter 方法也能实现上述功能,但是生成的记录是所有捕获组的 Captures 值。记录捕获组会导致搜索变慢。

    15.5.2 - 构建 Regex 值

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

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

      [dependencies]
      lazy_static = "1.4.0"
      
      • 1
      • 2
    • 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");
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
    • 调用 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());
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9

    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"
      
      • 1
      • 2
    • 包的顶部文件,需要添加 extern crate 声明:

      extern crate unicode_normalization;
      
      • 1
    • 如此可以对 &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");
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
    • 两个规范化的字符串拼接起来,未必是规范化的。

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

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


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

  • 相关阅读:
    【git】GitHub仓库没有 Contribution activity
    跨境电商短视频营销:这7点必须做好
    【Research】Wafer晶圆异常检测研究
    电脑启动过程(超详细过程)
    IOS开发学习日记(十七)
    删除不成功的免密登录重新做免密
    Android Launcher3简介
    代码随想录训练营补充|图论|Golang
    虹科示波器 | 汽车免拆检修 | 2014款保时捷卡宴车行驶中发动机偶尔自动熄火
    蓝桥杯打卡Day13天
  • 原文地址:https://blog.csdn.net/feiyanaffection/article/details/125575203