返回文章列表

Rust 格式化輸出技巧與巨集應用

本文探討 Rust 的格式化輸出機制,涵蓋基本語法、數字格式化、自定義格式、巨集應用以及正規表示式函式庫的使用,提供開發者更全面的 Rust 格式化輸出技巧。

程式語言 Rust

Rust 的格式化輸出系統提供豐富的選項,從基本型別到自定義型別,都能精確控制輸出格式。println! 巨集簡化了常見的控制檯輸出,而 format! 巨集則能構建格式化字串。除了內建的格式化規範,Rust 還允許開發者透過實作 DisplayDebug 等 trait 自定義型別的輸出格式,滿足特定需求。此外,format_args! 巨集和 std::fmt::Arguments 型別,搭配巨集設計,可建立更進階的日誌系統或其他格式化工具,提升程式碼的可讀性和可維護性。最後,regex crate 提供了安全且高效的正規表示式功能,方便開發者處理字串模式匹配。

格式化值(Formatting Values)

在整本文中,我們一直在使用像 println! 這樣的文字格式化巨集:

println!("{:.3}μs: relocated {} at {:#x} to {:#x}, {} bytes",
         0.84391, "object",
         140737488346304_usize, 6299664_usize, 64);

這段程式碼會產生以下的輸出:

0.844μs: relocated object at 0x7fffffffdcc0 to 0x602010, 64 bytes

內容解密:

  1. println! 巨集中的字串範本用於輸出格式化。
  2. 每個 {...} 會被後續引數的格式化形式取代。
  3. 範本字串必須是常數,以便 Rust 在編譯時檢查引數型別。
  4. 每個引數都必須被使用;否則,Rust 會在編譯時報告錯誤。

Rust 的格式化功能是開放式的,你可以透過實作 std::fmt 模組中的格式化特徵來擴充套件這些巨集,以支援自定義型別。同時,你也可以使用 format_args! 巨集和 std::fmt::Arguments 型別,讓自定義的函式和巨集支援格式化語言。

格式化巨集的行為

  • 所有格式化巨集都會借用其引數的分享參考;它們不會取得所有權或修改引數。

格式引數的結構

格式引數的形式為 {which:how},兩部分都是可選的;{} 是常見的使用形式。

  • which 值決定了哪個引數應該替換該引數。你可以透過索引或名稱選擇引數。如果沒有 which 值,則引數會按照從左到右的順序與引數配對。
  • how 值決定了引數應該如何被格式化,例如填充多少、精確度如何、採用哪種數字基數等。如果存在 how,則需要有冒號作為字首。

使用範例

以下是一些使用範例及其結果:

println!("number of {}: {}", "elephants", 19);
// 結果:number of elephants: 19

println!("from {1} to {0}", "the grave", "the cradle");
// 結果:from the cradle to the grave

println!("v = {:?}", vec![0,1,2,5,12,29]);
// 結果:v = [0, 1, 2, 5, 12, 29]

println!("{:8.2} km/s", 11.186);
// 結果:   11.19 km/s

println!("{:20} {:02x} {:02x}", "adc #42", 105, 42);
// 結果:adc #42              69   2a

內容解密:

  1. 每個 {}{which:how} 對應一個引數,並根據 how 的設定進行格式化。
  2. {:?} 用於除錯格式輸出,通常用於顯示向量等集合型別的值。
  3. {:#x} 表示以十六進位制格式輸出,並帶有 0x 字首。
  4. {:8.2} 表示浮點數輸出,總寬度為8,小數點後保留2位。

輸出特殊字元

如果你想在輸出中包含 {},需要在範本中將這些字元重複一次:

assert_eq!(format!("{{a, c}}{{a, b, c}}"), "{a, c} ⊂ {a, b, c}");

格式化文字值(Formatting Text Values)

當格式化像 &strString 這樣的文字型別時,引數的 how 部分有多個可選元件:

  1. 文字長度限制:如果你的引數超過這個限制,Rust 會截斷它。
  2. 最小欄位寬度:在截斷後,如果你的引數短於這個寬度,Rust 會在右邊用空格填充(預設)。
  3. 對齊方式:指定文字在欄位中的對齊方式,可以是 <(左對齊)、^(居中對齊)或 >(右對齊)。
  4. 填充字元:指定用於填充的字元,如果省略,Rust 使用空格。

使用範例

以下是一些範例及其結果,所有範例都使用相同的8字元引數 "bookends"

println!("{}", "bookends");
// 結果:bookends

println!("{:4}", "bookends");
// 結果:bookends(無變化,因為原始文字長度已超過4)

println!("{:12}", "bookends");
// 結果:bookends     (右邊填充空格)

println!("{:.4}", "bookends");
// 結果:book(截斷為前4個字元)

println!("{:12.4}", "bookends");
// 結果:book        (截斷後再填充空格)

println!("{:<12}", "bookends");
// 結果:bookends     (左對齊並填充)

println!("{:^12}", "bookends");
// 結果:   bookends   (居中對齊並填充)

println!("{:>12}", "bookends");
// 結果:     bookends (右對齊並填充)

println!("{:=^12}", "bookends");
// 結果:==bookends== (居中對齊並用 '=' 填充)

內容解密:

  1. 文字格式化允許你控制輸出的長度、對齊方式和填充字元。
  2. 如果指定了最小欄位寬度,Rust 會根據需要填充文字。
  3. 對齊方式和填充字元可以組合使用,以實作不同的輸出效果。

Unicode 處理的限制

Rust 的格式化工具對於 Unicode 文字寬度的理解比較簡單,它假設每個字元佔用一個列,而不考慮組合字元、半寬假名、零寬空格等 Unicode 的複雜性。因此,在處理某些 Unicode 字串時,可能會出現對齊不正確的問題。

assert_eq!(format!("{:4}", "th\u{e9}"), "th\u{e9} ");
assert_eq!(format!("{:4}", "the\u{301}"), "the\u{301}");

雖然這兩個字串在 Unicode 中被視為等價於 "thé",但 Rust 的格式化工具無法正確處理組合字元 \u{301},導致填充不正確。

格式化數字(Formatting Numbers)

當格式化具有數字型別的引數時(如 usizef64),引數的 how 部分有以下可選元件:

  1. 填充和對齊:與文字型別類別似,可以指定填充字元和對齊方式。
  2. 數字格式:可以指定數字輸出的格式,如小數點後的位數、數字基數等。

這些功能使你能夠靈活地控制數字輸出的格式,以滿足不同的需求。

Rust 中的格式化輸出

Rust 提供了一套強大的格式化輸出系統,能夠根據不同的需求對各種資料型別進行格式化。本文將介紹 Rust 中格式化輸出的基本用法和進階功能。

格式化輸出的基本元素

在 Rust 中,格式化輸出主要透過 format!println!write! 等巨集來實作。這些巨集使用一個範本字串來指定輸出的格式,範本字串中可以包含格式規範。

格式規範的基本形式為 {},在其後可以新增各種修飾符來控制輸出的格式。主要的修飾符包括:

  • +:強制顯示數值的符號。
  • #:請求明確的基數字首,如 0x0b
  • 0:請求使用前導零來滿足最小欄位寬度。
  • 最小欄位寬度:指定輸出的最小寬度,如果實際輸出內容不足該寬度,則會使用預設的填充字元(通常是空格)進行填充。
  • 精確度:對於浮點數,指定小數點後的有效數字位數。
  • 進位制:對於整數,可以指定二進位制(b)、八進位制(o)或十六進位制(xX);對於浮點數,可以指定科學計數法(eE)。

格式化整數

以下是一些格式化整數的例子:

預設輸出

println!("{}", 1234); // 輸出 "1234"

強制顯示符號

println!("{:+}", 1234); // 輸出 "+1234"

指定最小欄位寬度

println!("{:12}", 1234); // 輸出 "        1234"

使用前導零填充

println!("{:012}", 1234); // 輸出 "000000001234"

二進位制、八進位制和十六進位制輸出

println!("{:b}", 1234); // 輸出 "10011010010"
println!("{:o}", 1234); // 輸出 "2322"
println!("{:x}", 1234); // 輸出 "4d2"
println!("{:X}", 1234); // 輸出 "4D2"

格式化浮點數

對於浮點數,可以指定小數點後的有效數字位數:

預設輸出

println!("{}", 1234.5678); // 輸出 "1234.5678"

指定精確度

println!("{:.2}", 1234.5678); // 輸出 "1234.57"
println!("{:.6}", 1234.5678); // 輸出 "1234.567800"

指定最小欄位寬度和精確度

println!("{:12.2}", 1234.5678); // 輸出 "      1234.57"

科學計數法

println!("{:e}", 1234.5678); // 輸出 "1.2345678e3"
println!("{:.3e}", 1234.5678); // 輸出 "1.235e3"

格式化其他型別

Rust 的格式化系統不僅支援基本資料型別,也支援許多標準函式庫中的型別,例如錯誤型別、網路位址型別等。

錯誤型別

use std::error::Error;

let err = std::io::Error::new(std::io::ErrorKind::Other, "example error");
println!("{}", err); // 輸出錯誤訊息

網路位址型別

use std::net::{IpAddr, Ipv4Addr};

let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
println!("{}", ip); // 輸出 "127.0.0.1"

除錯輸出

Rust 提供了一種特殊的格式化方式用於除錯,即使用 {:?}{:#?}。這可以讓你以一種對開發者友好的方式輸出複雜資料結構的內容。

基本除錯輸出

use std::collections::HashMap;

let mut map = HashMap::new();
map.insert("Portland", (45.5237606, -122.6819273));
map.insert("Taipei", (25.0375167, 121.5637));
println!("{:?}", map);
// 輸出 {"Taipei": (25.0375167, 121.5637), "Portland": (45.5237606, -122.6819273)}

美化除錯輸出

println!("{:#?}", map);
// 輸出
// {
//     "Taipei": (
//         25.0375167,
//         121.5637,
//     ),
//     "Portland": (
//         45.5237606,
//         -122.6819273,
//     ),
// }

要讓自定義型別支援除錯輸出,可以使用 #[derive(Debug)]

#[derive(Debug)]
struct Complex {
    r: f64,
    i: f64,
}

let complex = Complex { r: 1.0, i: 2.0 };
println!("{:?}", complex); // 輸出 Complex { r: 1.0, i: 2.0 }

Rust 中的格式化輸出

Rust 提供了多種格式化輸出的方法,包括 DisplayDebugBinaryOctalHex 等 trait。本篇文章將探討 Rust 中的格式化輸出,並提供一些實用的範例。

格式化基礎

Rust 中的格式化輸出主要透過 format!println! 等宏來實作。這些宏使用特定的語法來指定輸出的格式。

基本格式化

最基本的格式化是使用 {} 來佔位。例如:

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

這將輸出 Hello, world!

格式化數字

Rust 提供了多種格式化數字的方法,包括二進位制、八進位制、十六進位制等。例如:

println!("{:b}", 10); // 二進位制輸出:1010
println!("{:o}", 10); // 八進位制輸出:12
println!("{:x}", 10); // 十六進位制輸出:a
println!("{:X}", 10); // 十六進位制輸出:A

格式化浮點數

Rust 也提供了多種格式化浮點數的方法,包括科學記號等。例如:

println!("{:.3e}", 3.14159); // 科學記號輸出:3.142e0
println!("{:.3E}", 3.14159); // 科學記號輸出:3.142E0

自定義格式化

Rust 允許開發者自定義型別的格式化輸出。這需要實作特定的 trait,例如 DisplayDebug 等。

實作 Display trait

Display trait 是最常用的格式化 trait。它提供了一個 fmt 方法,用於產生型別的字串表示。例如:

use std::fmt;

struct Complex {
    r: f64,
    i: f64,
}

impl fmt::Display for Complex {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{} + {}i", self.r, self.i)
    }
}

fn main() {
    let c = Complex { r: -0.5, i: f64::sqrt(0.75) };
    println!("{}", c); // 輸出:-0.5 + 0.8660254037844386i
}

實作 Debug trait

Debug trait 用於產生型別的 debug 表示。通常使用 {:?}{:#?} 來格式化。例如:

#[derive(Debug)]
struct Complex {
    r: f64,
    i: f64,
}

fn main() {
    let c = Complex { r: -0.5, i: f64::sqrt(0.75) };
    println!("{:?}", c); // 輸出:Complex { r: -0.5, i: 0.8660254037844386 }
}

指標的格式化

Rust 也提供了指標的格式化輸出。使用 {:p} 可以輸出指標的位址。例如:

use std::rc::Rc;

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

這將輸出三個指標的位址。

動態寬度和精確度

Rust 的格式化輸出也支援動態寬度和精確度。例如:

fn main() {
    let content = "Hello, world!";
    let width = 20;
    println!("{:>1$}", content, width); // 輸出:         Hello, world!
}

這將輸出 content 字串,寬度為 width

使用自訂格式化語言的函式與巨集

在Rust中,我們可以利用format_args!巨集和std::fmt::Arguments型別,撰寫接受格式化範本和引數的函式和巨集。以下是一個範例,展示如何實作一個日誌系統,利用Rust的文字格式化語言來產生日誌訊息。

實作日誌函式

首先,我們需要定義一個檢查日誌是否啟用的函式和一個寫入日誌專案的函式:

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

fn logging_enabled() -> bool {
    // 實際實作中,這裡應該檢查日誌是否啟用
    true
}

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("開啟日誌檔案失敗");
        
        log_file.write_fmt(entry)
            .expect("寫入日誌失敗");
    }
}

內容解密:

  1. logging_enabled函式:檢查日誌系統是否啟用,實際實作中應根據組態或條件傳回布林值。
  2. write_log_entry函式:接受一個std::fmt::Arguments型別的引數,該引數包含了格式化後的文字。
    • 檢查日誌是否啟用。
    • 開啟(或建立)一個名為"log-file-name"的檔案,並將日誌訊息追加到檔案末尾。
    • 使用write_fmt方法將格式化後的訊息寫入檔案。

使用format_args!巨集

呼叫write_log_entry時,可以使用format_args!巨集來傳遞格式化字串和引數:

let mysterious_value = "某些神秘的值";
write_log_entry(format_args!("Hark! {:?}\n", mysterious_value));

內容解密:

  • format_args!巨集:在編譯時解析格式化字串,並檢查引數型別。如果有任何問題,會報告錯誤。
  • 在執行時,評估引數並建立一個Arguments值,該值包含了格式化文字所需的所有資訊。

定義log!巨集

為了簡化呼叫過程,可以定義一個log!巨集:

macro_rules! log {
    ($format:tt, $($arg:expr),*) => {
        write_log_entry(format_args!($format, $($arg),*))
    }
}

內容解密:

  • macro_rules!:定義巨集的語法。
  • log!巨集:接受格式化字串和引數,將它們傳遞給format_args!巨集,並呼叫write_log_entry函式。

使用log!巨集

現在,可以使用log!巨集來記錄日誌:

let mysterious_value = "某些神秘的值";
log!("O day and night, but this is wondrous strange! {:?}\n", mysterious_value);

內容解密:

  • log!巨集的使用:簡化了日誌記錄的過程,使其更易讀且方便使用。

正規表示式簡介

Rust的官方正規表示式函式庫是regexcrate。它提供了常見的搜尋和匹配功能,並對Unicode有良好的支援。雖然它不支援一些其他正規表示式套件中的功能,如反向參照和環視模式,但這些簡化使得regex可以確保搜尋時間與表示式大小和被搜尋文字長度成線性關係。

要在專案中使用regex,需要在Cargo.toml[dependencies]部分新增:

regex = "0.2.2"

然後在crate的根目錄中新增extern crate專案:

extern crate regex;

內容解密:

  • regexcrate:Rust的官方正規表示式函式庫,保證了搜尋操作的安全性和效能。
  • 新增依賴:透過修改Cargo.toml和使用extern crate來引入regexcrate。