Rust程式語言的標準函式庫中的std::fmt模組提供了多種巨集來格式化或是印出字串。在這個章節中,我們將會學習format!write!writeln!print!println!eprint!eprintln!巨集的詳細使用方式。



先來看看以上提及的巨集,它們的用途:

  • format!:可以將文字格式化成String結構實體。
  • write!:可以將格式化之後的文字輸出到某個緩衝空間。
  • writeln!:可以將格式化之後的文字最後再多加一個換行字元輸出到某個緩衝空間。
  • print!:可以將格式化之後的文字輸出到標準輸出(stdout)。
  • println!:可以將格式化之後的文字最後再多加一個換行字元輸出到標準輸出(stdout)。
  • eprint!:可以將格式化之後的文字輸出到標準錯誤(stderr)。
  • eprintln!:可以將格式化之後的文字最後再多加一個換行字元輸出到標準錯誤(stderr)。

格式化文字的用法

Rust程式語言的標準函式庫提供的文字格式化方式,是利用字串定數來撰寫文字格式化的範本(template),接著將字串定數之後傳入的參數代入至範本中由{}組成的空位,重新組合成新的字串。編譯器會在編譯階段的時候,就去編譯文字格式化的範本,因此這樣的使用方式對於程式執行階段的運算資源負擔並不大。

文字格式化的範例如下:

format!("Hello");                                     // => "Hello"
format!("Hello, {}!", "world");                       // => "Hello, world!"
format!("The number is {}", 1);                       // => "The number is 1"
format!("{:?}", (3, 4));                              // => "(3, 4)"
format!("{1:?} {0:?}", (3, 4), (5, 6));               // => "(5, 6) (3, 4)"
format!("{value}", value = 4);                        // => "4"
format!("{value1} {value2}", value1 = 4, value2 = 5); // => "4 5"
format!("{name} {}", 1, name = "MagicLen");           // => "MagicLen 1"
format!("{} {}", 1, 2);                               // => "1 2"
format!("{0} {1}", 1, 2);                             // => "1 2"
format!("{1} {0}", 1, 2);                             // => "2 1"
format!("{1} {} {2} {} {0} {}", 1, 2, 3);             // => "2 1 3 2 1 3"
format!("{:4}", 42);                                  // => "  42" with leading spaces
format!("{:<4}", 42);                                 // => "42  " with trailing spaces
format!("{:04}", 42);                                 // => "0042" with leading zeros
format!("{:0<4}", 42);                                // => "4200" with trailing zeros
format!("{:+}", 42);                                  // => "+42"
format!("{:b}", 42);                                  // => "101010"
format!("{:#b}", 42);                                 // => "0b101010"
format!("{:.*}", 2, 1.2345);                          // => "1.23"
format!("{:.*}", 3, 1.2345);                          // => "1.234"
format!("{:.*}", 3, 1.2335);                          // => "1.234"
format!("{:.3}", 1.2335);                             // => "1.234"
format!("{:7.3}", 1.2335);                            // => "  1.234" with leading spaces
format!("{:<7.3}", 1.2335);                           // => "1.234  " with trailing spaces
format!("{:07.3}", 1.2335);                           // => "001.234" with leading zeros
format!("{:0>7.3}", 1.2335);                          // => "001.234" with leading zeros
format!("{:<07.3}", 1.2335);                          // => "001.234" with leading zeros
format!("{:0<7.3}", 1.2335);                          // => "1.23400" with trailing zeros
format!("{:07.3}", -1.2335);                          // => "-01.234" with leading spaces after the sign character
format!("{:0>7.3}", -1.2335);                         // => "0-1.234" with leading spaces before the sign character
format!("{:<07.3}", -1.2335);                         // => "-01.234" with leading zeros after the sign character
format!("{:0<7.3}", -1.2335);                         // => "-1.2340" with trailing zeros
format!("{:@>5}", "Hi");                              // => "@@@Hi" with leading '@'s
format!("{:@<5}", "Hi");                              // => "Hi@@@" with trailing '@'s
format!("{:@^5}", "Hi");                              // => "@Hi@@" with leading and trailing '@'s (center alignment)
format!("{3:.2$} {1:.0$}", 2, 1.2345, 1, 1.2345);     // => "1.2 1.23"
format!("{second:.second_decimal$} {first:.first_decimal$}", first_decimal = 2, first = 1.2345, second_decimal = 1, second = 1.2345); // => "1.2 1.23"

幾個比較特別需要注意的地方是,如果要同時使用一般依照位置來填入的參數和依照名稱來填入的參數,依照名稱來填入的參數必須要寫在依照位置來填入的參數之後,否則會編譯錯誤。例如以下程式會編譯錯誤:

format!("{name} {}", name = "MagicLen", 1);

{}類似{0}{1}等用法,只是它不明確指定參數的位置,而是讓編譯器在編譯階段時,由其在範本中從左到右的順序來推斷它的參數位置。也就是說,{1} {} {2} {} {0} {}會被視為{1} {0} {2} {1} {0} {2}。如果{}內有冒號:的話,在:的左邊可以指定參數明確的位置或是名稱,在:的右邊可以使用一些比較特別的格式化方式,格式如下:

:[[填充字元](>|<|^)][+|-][#][[0]寬度][.精準度][特性符號]

當格式化出來的文字寬度小於格式化方式所設定的寬度,就會用填充字元來填充不足的部份,而填充的位置則是依照><^來決定。>表示要將字元填充到文字左邊(原本的文字靠右對齊);<表示要將字元填充到文字右邊(原本的文字靠左對齊);^表示要將字元填充到文字兩邊(原本的文字置中對齊)。填充字元預設的字元是空格 。至於預設對齊方式,不同型別有不同的預設對齊方式,舉例來說,如果是數值的話為靠右對齊>,如果是字串的話則為靠左對齊<

+-,前者可以用來指定數值就算是正數也要將正號+寫在文字中,而後者就比較不常用了。-會被用到的地方可能是要將Foo(負數)這類的值在格式化的時候寫成-Foo(正數)

#可以用來顯示更多的文字,有點像是第二種格式化的模式。

寬度前可以加上0,可以將數值開頭的部份以字元0來填充,如果數值有正負號,字元0會被填充在正負號之後。

精準度可以用來指定有小數的數值在格式化之後最多要顯示幾位小數。

寬度精準度可以讀取參數中傳入的usize型別的值,這個參數必須要有名稱,且名稱寫在範本中寬度精準度的位置時,結尾需要加上$字元。

特性符號列表如下:

  • 無:使用Display特性的fmt方法。
  • ?:使用Debug特性的fmt方法。
  • x:使用LowerHex特性的fmt方法。
  • X:使用UpperHex特性的fmt方法。
  • x?:如果參數擁有LowerHex特性,就使用LowerHexfmt方法。否則使用Debug特性的fmt方法。
  • X?:如果參數擁有UpperHex特性,就使用UpperHexfmt方法。否則使用Debug特性的fmt方法。
  • o:使用Octal特性的fmt方法。
  • p:使用Pointer特性的fmt方法。
  • b:使用Binary特性的fmt方法。
  • e:使用LowerExp特性的fmt方法。
  • E:使用UpperExp特性的fmt方法。

如果在使用xXx?X?ob特性符號時,有加上井字號#使用其第二種格式化模式的話,格式化出來的數值文字會分別以0x0o0b為開頭。

而如果在使用?特性符號時,有加上井字號#,則會美化原先Debug特性格式化出來的文字。舉例來說:

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


fn main() {
    let origin = Point { x: 0, y: 0 };

    println!("{:?}", origin);
}

不加上井字號#,程式執行結果如下:

Point { x: 0, y: 0 }

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


fn main() {
    let origin = Point { x: 0, y: 0 };

    println!("{:#?}", origin);
}

加上井字號,程式執行結果如下:

Point {
    x: 0,
    y: 0
}

範本中的空位皆是由{}組成,那如果我們真的需要在文字中使用到大括號{}要怎麼辦呢?那就只能用兩個相同連續的大括號來表示一個大括號文字了,{{可以表示{}}可以表示}。例如:

fn main() {
    println!("{{}}");
}

程式執行結果如下:

{}

format!

fn main() {
    let mut hello = format!("{}, ", "Hello");
    hello.push_str("world.");
}

write!

use std::io::{self, Write};

fn main() {
    let mut w = Vec::new();
    write!(&mut w, "Hello {}!", "world").unwrap();
}
use std::io::{self, Write};

fn main() {
    let mut stdout = io::stdout();
    write!(&mut stdout, "Hello {}!", "world").unwrap();
}

writeln!

use std::io::{self, Write};

fn main() {
    let mut w = Vec::new();
    writeln!(&mut w, "Hello {}!", "world").unwrap();
}
use std::io::{self, Write};

fn main() {
    let mut stdout = io::stdout();
    writeln!(&mut stdout, "Hello {}!", "world").unwrap();
}

print!

fn main() {
    print!("Hello {}!", "world");
}

println!

fn main() {
    println!("Hello {}!", "world");
}

eprint!

fn main() {
    eprint!("Hello {}!", "world");
}

eprintln!

fn main() {
    eprintln!("Hello {}!", "world");
}

其它會格式化文字的巨集

標準函式庫中有些不在std::fmt模組內的巨集也會去格式化文字,例如在前面的章節使用過的panic!assert!assert_eq!assert_ne!

舉例來說:

fn main() {
    assert_eq!(0, 1, "Hello {}!", "world");
}

然而,std::fmt模組外的巨集,如果「範本」後面沒有再接上任何的參數,那麼原先應該要被當作是「範本」的字串定數,就只會是一個普通的字串定數。舉例來說:

fn main() {
    print!("{}");
}

以上程式會編譯失敗,因為print!巨集的範本需要有一個參數。

fn main() {
    panic!("{}");
}

以上程式會編譯成功,因為panic!巨集在「範本」後並沒有加上任何參數,因此這個「範本」會直接以普通的字串定數來對待。

結論

Rust程式語言的標準函式庫提供的巨集時常會用到格式化文字的功能,也因此清楚了解格式化文字的方式將有助於我們使用這些巨集,甚至是撰寫出我們自己的巨集。

在下一章節,我們就是要來學習如何建立出自己的巨集。

下一章:巨集