Rust语言的打印操作主要是通过在std::fmt里面定义的一系列宏来处理。主要包括:

  • format!: 将格式化文本存入字符串。
  • print!: 与format!类似,但是把文本输出到控制台(io::stdout)
  • println!: 与print!类似,但是输出结果末尾会追加换行符。
  • eprint!: 与format!类似,但是把文本输出到标准错误(std::stderr)。
  • eprintln!: 与eprint!类似,但是输出结果末尾会追加换行符。
  • write!: 与format!类似,但是把文本输出到&mut io::Write
  • writeln!: 与write!,但是输出结果末尾会追加换行符。

这些宏都会以一致的规则对文本进行解析,格式化的正确性会在编译器进行检查

格式化规则

位置参数

每个格式化参数可以按照顺序进行格式化,也可以按照指定的顺序对其进行格式化。

  • {}: 没有指定索引位置,会按照参数迭代逐个使用
  • {n}: 指定了索引位置,就会使用指定索引的参数
fn main() {
    // 按照顺序进行格式化
    // 输出: 10 is a number, 'hello world' is a string
    println!("{} is a number, '{}' is a string", 10, "hello world");

    // 按照索引进行格式化
    // 输出: 10 is a number, 'hello world' is a string
    println!("{1} is a number, '{0}' is a string", "hello world", 10);

    // 按照顺序与索引混合进行格式化
    // 没有索引的{}会按照迭代顺序进行格式化
    // 输出: 2 1 1 2
    println!("{1} {} {0} {}", 1, 2)
}

具名参数

对于已经存在的变量,可以按照变量名字输出。

如果不存在,也可以进行指定名称。

fn main() {
    // 已存在的变量
    // 输出:My name is Bob.
    let name = "Bob";
    println!("My name is {name}.");

    // 格式化是指定名称,此处并不会覆盖原有的定义
    // 输出:My name is Alice, his name is Jeff.
    println!(
        "My name is {name}, his name is {name1}.",
        name = "Alice",
        name1 = "Jeff"
    );

    // 输出:name: Bob
    println!("name: {name}");
}

格式化参数

我们可以指定一些格式化的参数,来对格式化的输出进行个性化的定制。

指定宽度

需要注意的是,这里指定的是最小宽度,如果字符串不足宽度会进行填充,如果超出并不会截断。

  • {:n}: 通过数字直接指定宽度,比如{:5}
  • {:n$}: 通过参数索引指定宽度,比如{:1$}
  • {:name$}: 通过具名参数指定宽度,比如{:width$}
fn main() {
    // 不足长度5,会填充空格
    // 输出: Hello a    !
    println!("Hello {:5}!", "a");

    // 超出长度1,仍然完整输出
    // 输出: Hello abc!
    println!("Hello {:1}!", "abc");

    // 因为未指定索引,所以按照顺序位置上引用的是"abc"
    // 通过$符指定了宽度的索引为1,即宽度为5
    // 输出: Hello abc  !
    println!("Hello {:1$}!", "abc", 5);
    // 指定了位置索引为1,使用$符指定了宽度的索引为0,即宽度为5
    // 输出: Hello abc  !
    println!("Hello {1:0$}!", 5, "abc");

    // 通过具名参数指定宽度为5
    // 输出:Hello abc  !
    println!("Hello {:width$}!", "abc", width = 5);
    let width = 5;
    println!("Hello {:width$}!", "abc");
}

填充与对齐

在格式化字符串内,一般按照 : + 填充字符 + 对齐方式 来排列。

  • [fill]<: 左对齐
  • [fill]^: 居中对齐
  • [fill]>:右对齐

非数字时,默认情况下填充使用空格+左对齐。

数字时,默认情况下填充使用空格+右对齐。

特别要注意的,某些类型可能不会实现对齐。特别是对于Debug trait,通常不会实现该功能。确保应用填充的一种好方法是格式化输入,再填充此结果字符串以获得输出。

fn main() {
    // 非数字默认左对齐,空格填充
    assert_eq!(format!("Hello {:7}!", "abc"), "Hello abc    !");

    // 左对齐
    assert_eq!(format!("Hello {:<7}!", "abc"), "Hello abc    !");

    // 左对齐,使用-填充
    assert_eq!(format!("Hello {:-<7}!", "abc"), "Hello abc----!");

    // 右对齐,使用-填充
    assert_eq!(format!("Hello {:->7}!", "abc"), "Hello ----abc!");

    // 中间对齐,使用-填充
    assert_eq!(format!("Hello {:-^7}!", "abc"), "Hello --abc--!");

    // 数字默认右对齐,空格填充
    assert_eq!(format!("Hello {:7}!", 7), "Hello       7!");

    // 左对齐
    assert_eq!(format!("Hello {:<7}!", 7), "Hello 7      !");

    // 居中对齐
    assert_eq!(format!("Hello {:^7}!", 7), "Hello    7   !");

    // 填充0
    assert_eq!(format!("Hello {:07}!", 7), "Hello 0000007!");

    // 负数填充0,负号会占用一位
    assert_eq!(format!("Hello {:07}!", -7), "Hello -000007!");
}

格式化标识

我们还可以使用一些格式化标识对字符串进行格式化。

  • +: 数字打印符号,默认情况下数字不会打印正号,加了该标识符可以强制打印正负号。
  • #?: 打印Debug格式,同时会添加换行符和缩进。
  • #x: 以十六进制小写形式打印
  • #X: 以十六进制大写形式打印
  • #b: 以二进制形式打印
  • #o: 以八进制形式打印
#[allow(dead_code)]
#[derive(Debug)]
struct User {
    name: String,
    age: u32,
}

fn main() {
    // 打印正负号
    assert_eq!(format!("Hello {:+7}!", 5), "Hello      +5!");
    assert_eq!(format!("Hello {:+7}!", -5), "Hello      -5!");

    let user: User = User {
        name: "John".to_string(),
        age: 30,
    };
    // 输出Debug格式
    assert_eq!(
        format!("Hello {:?}!", user),
        "Hello User { name: \"John\", age: 30 }!"
    );
    // 输出美化后的Debug格式
    assert_eq!(
        format!("Hello {:#?}!", user),
        "Hello User {\n    name: \"John\",\n    age: 30,\n}!"
    );

    // 以16进制输出
    assert_eq!(format!("Hello {:#x}!", 10251), "Hello 0x280b!");
    assert_eq!(format!("Hello {:#X}!", 10251), "Hello 0x280B!");
    // 以二进制输出
    assert_eq!(format!("Hello {:#b}!", 10251), "Hello 0b10100000001011!");
    // 以八进制输出
    assert_eq!(format!("Hello {:#o}!", 10251), "Hello 0o24013!");
}

精度控制

这里的精度控制其实就是最大宽度,超出将会进行截断。

对于整数类型,这里会被忽略。

对于浮点类型,指示小数点后打印多少位。

  • .n: 整数n就是精度
  • .n$: 使用参数n作为精度,这里也可以是具名参数。
  • .*: 与两个格式输入关联,第一个输入是要保存的精度,第二个是要打印的值。
fn main() {
    // 直接指定精度
    assert_eq!(
        format!("Hello {} is {:.5}", "abc", 0.01),
        "Hello abc is 0.01000"
    );

    // 直接指定精度
    assert_eq!(
        format!("Hello {0} is {1:.5}", "abc", 0.01),
        "Hello abc is 0.01000"
    );

    // 通过参数索引指定精度
    assert_eq!(
        format!("Hello {0} is {1:.2$}", "abc", 0.01, 5),
        "Hello abc is 0.01000"
    );

    // 通过参数名称指定精度
    assert_eq!(
        format!("Hello {0} is {1:.precision$}", "abc", 0.01, precision = 5),
        "Hello abc is 0.01000"
    );
    assert_eq!(
        format!(
            "Hello {} is {number:.precision$}",
            "abc",
            number = 0.01,
            precision = 5
        ),
        "Hello abc is 0.01000"
    );

    // 通过星号,从参数内读取精度和要处理的数字
    assert_eq!(
        format!("Hello {} is {:.*}", "abc", 5, 0.01),
        "Hello abc is 0.01000"
    );

    // 通过星号,从参数内读取精度,数字指定了索引
    assert_eq!(
        format!("Hello {} is {2:.*}", "abc", 5, 0.01),
        "Hello abc is 0.01000"
    );

    // 通过星号,从参数内读取精度,通过名称指定数字
    assert_eq!(
        format!("Hello, {number:.*}", 3, number = 123.45),
        "Hello, 123.450"
    );
    // 数字为字符串,所以直接截断
    assert_eq!(
        format!("Hello, {number:.*}", 3, number = "123.45"),
        "Hello, 123"
    );
    // 配合最小宽度做填充
    assert_eq!(
        format!("Hello, {number:>8.*}", 3, number = 123.45),
        "Hello,  123.450"
    );
}

转义

字面量字符{}可以通过添加相同字符进行转义,即{字符使用{{进行转义,而}字符使用}}进行转义。

assert_eq!(format!("Hello {{}}"), "Hello {}");
assert_eq!(format!("{{ Hello"), "{ Hello");

语法顺序

format_string := text [ maybe_format text ] *
maybe_format := '{' '{' | '}' '}' | format
format := '{' [ argument ] [ ':' format_spec ] '}'
argument := integer | identifier

format_spec := [[fill]align][sign]['#']['0'][width]['.' precision]type
# 填充字符串
fill := character
# 对齐方式
align := '<' | '^' | '>'
# 正负号
sign := '+' | '-'
# 宽度
width := count
# 精度
precision := count | '*'
# 输出类型
type := '' | '?' | 'x?' | 'X?' | identifier
count := parameter | integer
parameter := argument '$'

格式化的trait

  • nothing => Display
  • ? => Debug
  • x? => Debug 带有小写十六进制整数
  • X? => Debug 带有大写十六进制整数
  • o => Octal, 八进制
  • x => LowerHex, 小写十六进制
  • X => UpperHex, 大写十六进制
  • p => Pointer, 指针
  • b => Binary, 二进制
  • e => LowerExp,小写e的科学计数法
  • E => UpperExp,大写E的科学计数法

Debug trait

Debug 是为开发者调试打印数据结构所设计的,在使用的时候,Debug 用 {:?} 来打印。在大多数情况下,建议使用 #[derive(Debug)] 派生宏直接生成就足够了。

pub trait Debug {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error>;
}

Display trait

Display 是给用户显示数据结构所设计的,在使用的时候,Display 用 {} 打印。自定义类型的Display必须自己实现。

pub trait Display {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error>;
}

实现示例:

#[allow(dead_code)]
#[derive(Debug)]
struct User {
    name: String,
    age: u32,
}

impl Display for User {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "name: {}, age: {}", self.name, self.age)
    }
}

fn main() {
    let user = User {
        name: "John".to_string(),
        age: 20,
    };
    // 输出: name: John, age: 20
    println!("{}", user);
}