Rust 并没有采用 C 风格的格式化占位符的语法,可能是 C 的这种风格表现力不够,刚开始学习的 Rust 的我,格式化的时候只会使用 {},不过如果去了解其中的语法,发现并不是很难。

指定格式化参数

最简单的格式化:

println!("hello {}","world");

参数部分默认是从零开始的序列,但是占位符可以手动指定使用哪个参数。

println!("hello {0}","teemo");

或者使用命名参数,这个是格式化宏特有的语法:

println!("hello {name}", name = "teemo");

命名参数甚至可以不用显示传递,而是在当前作用域查询:

let argument = 2 + 2;
format!("{argument}");   // => "4"

fn make_string(a: u32, b: &str) -> String {
    format!("{b} {a}")
}

格式化方式

定位占位符和占位符格式化方式采用:分割,:左边为参数的索引或是变量标识,右边是格式化的一些细节。总的语法可以这里查看,:右边的可以放置的内容如下:

  • 对齐方式还有填充内容

  • 数字符号 + / -,对于数字类型可用

  • 打印的类型,打印对象的描述信息或是十六进制展示对象之类的,也就是使用哪个 trait

  • 打印出来字符串的宽度

  • 打印浮点数的精度

对齐方式、宽度

: 右边添加数字,表示宽度,指定宽度之后可以指定对齐方式,还有填充内容。对齐方式有:

  • < 左边对齐
  • ^ 剧中对齐
  • > 右边对齐

可以在这三个符号前面添加字符用来在长度不够的时候填充内容。简单的例子:

  • {url} 打印名称为 url 的参数

  • {:10} 打印宽度为 10

  • {url:->50} 打印名称为 url 的参数,宽度 50 ,右边对齐,使用 - 补全。

sign / 0 / #

添加 + 会对数字类型添加正负符号。

println!("{:+}",-0.1);

指定宽度的时候,前面添加了0,表示使用0填充到指定宽度

println!("{:010}",6);

# 用来美化输出

  • #?Debug 特征自动添加缩减
  • #x/#X/#b/#o 自动在对应的数据前面添加 0x/0X/0b/0o
println!("{:+#010x}",6); // +0x0000006

浮点数进度

.后面添加一个数字,表示打印多少位的精度,也可以添加一个 $ 符号,表示动态设定打印浮点数的精度。

  • {price:.2} 打印 price参数,并且小数部分只展示两位
  • {price:.2$} 打印 price参数,并且小数部分根据传入的第三个参数决定,这个参数必须是 usize 类型。

格式化方式

Rust 需要格式化数据的时候,默认情况下,对象需要实现 fmt::Display。这是因为 Rust 格式化的时候默认是外部实现的,不同于 C 语言 printf 直接展示数据本身,对于自定义的数据,需要先实现对应的 trait,然后才能实现格式化。

符号接口说明
默认Display如果不指定类型,就会默认使用这个接口展示对象
?DebugDisplay区别是可以通过#[derive(Debug)]注解一个类型,实现这个接口。
bBinary二进制格式
oOctal八进制格式
xLowerHex小写十六进制
XUpperHex大写十六进制
pPointer打印指针,引用也是一种指针,这里可以直接打印。
eLowerExp使用科学计数法打印数字,小写的 e
EUpperExp使用科学计数法打印数字,大写的 E

相关操作宏

  • print! / println! / eprint! / eprintln! 打印到标准输出、错误输出,ln表示在结尾处添加一个 \n
  • format! 格式化字符串
  • write! /writeln! 格式化写入
  • format_args!

实例

对于自定义结构体,无法直接打印出来:

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 0, y: 0 };
    println!("coords: {}", p);
}

上面的代码不能通过编译,会出现下面的错误,由于结构体 Point没有实现 std::fmt::Display接口

error[E0277]: `Point` doesn't implement `std::fmt::Display`
 --> src\main.rs:8:28
  |
8 |     println!("coords: {}", p);
  |                            ^ `Point` cannot be formatted with the default formatter
  |
  = help: the trait `std::fmt::Display` is not implemented for `Point`
  = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
  = note: this error originates in the macro `$crate::format_args_nl` (in Nightly builds, run with -Z macro-backtrace for more info)

简单的方法就是就是实现缺失的实现:

struct Point {
    x: i32,
    y: i32,
}

impl Display for Point {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        f.write_fmt(format_args!("x=> {} y=> {}", self.x, self.y))
    }
}

或者直接添加 Debug

#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 0, y: 0 };
    println!("coords: {:?}", p);
}

大部分时候,添加 Debug 已经足够详细的打印出需要的内容,上面会输出:

coords: Point { x: 0, y: 0 }

format_args!

其他几个打印宏比较简单,format_args!这个宏就会让人比较迷糊。std::fmt::Arguments是一个结构体,可以调用 as_str或是 to_string转换成字符串,其本身也已经实现Debug/Display接口,可以作为一个参数打印出来:

fn main() {
    weather(format_args!("温度: {}° ~ {}°,降水概率: {}%,风速: {} 公里/时", 24, 29, 70, 11))
}

fn weather(args: Arguments) {
    println!("今天的天气:{}", args.to_string())
    // 今天的天气:温度: 24° ~ 29°,降水概率: 70%,风速: 11 公里/时
}

创建Arguments实例的唯一方法是使用 format_args!宏。格式化的时候,格式是固定的,参数也是固定的,参数的内容不固定,因此能在编译期间就解析好格式,还有参数的格式风格等信息,储存这些信息就是Arguments,在运行时就不需要再去解析这些内容,减少不必要的开销。而且这个宏不能单独一行使用,如果赋值到临时变量,无法通过编译:

let args = format_args!("温度: {}°", 24);
say(args)

上面的代码会报错:

   |
12 |         let args = format_args!("温度: {}°", 24);
   |                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^- temporary value is freed at the end of this statement
   |                    |
   |                    creates a temporary which is freed while still in use
13 |         say(args)
   |             ---- borrow later used here
   |

这个错误是因为返回内容引用了一个临时变量,然后这个临时变量生命周期没有超过返回引用,导致报错E0716

    let arr = String::from("abc").as_str();
//            ^^^^^^^^^^^^^^^^^^^  这里创建了一个临时对象 String,返回 &str 指向 String,这一行结束之后 String 将会回收。
    println!("{}", arr)

但是如果作为参数传递,则能避免上面的问题:

fn main() {
    print(String::from("hello").as_str());
    //    ^^^^^^^^^^^^^^^^^^^^^ 这里的 String 对象会等到 print 结束之后才会进行回收
}

fn print(s: &str) {
    println!("{}", s)
}