[譯] 我經常使用的 Rust 小 Crate

PrivateRookie發表於2019-07-10

原文連結: Karol Kuczmarski's Blog – Small Rust crates I (almost) always use


因為 Rust 相對貧瘠的標準庫,使用 Rust 不可避免地會引入不少第三發依賴。

這些第三方依賴用於解決一些“自帶電池”更豐富的依靠內建庫就可以解決的問題。
一個好例子就是 Python 的 re 模組,這相當於 Rust regex crate。

正規表示式之類的問題是一類相對大的問題,擁有專門的庫一點也不奇怪。對於一門語言,提供一個小庫來解決一個很特化的則不那麼常見。

就如同,一個函式/型別/宏 之類的問題,或者只比他們大一點。

在這篇部落格,我們會快速瀏覽一系列必需的“小型庫”

either

Rust 有內建的 Result 型別,這是 OkErr 的集合。它構成了 Rust 中一般錯誤處理的基礎。

從結構上來說,Result<T, E> 只是提供了 TE 的替代。你可能想將這樣
一個列舉類用於不同用途來代表錯誤處理。不幸的是,由於 Result 強烈的內在意
義,這種用法不符合 Rust 風格同時也令人疑惑(其實就是 Result 從名字到用法
都是高度明確的語義,如果使用在其他地方反而會造成疑惑)

這也是需要 either crate。它包含了下面的 Either 型別:

enum Either<L, R> {
    Left(L),
    Right(L),
}

雖然它與 Result 同構,但它並不帶有強制的錯誤處理語義。而且它還提供了對稱
組合器方法如 map_leftright_and_then 用於鏈式計算 Eigther 包含的值

lazy_static

因為語言設計,Rust 不允許安全地使用全域性可變變數。將全域性可變變數引入你的程式碼
半標準方法是使用 lazy_static crate

但是,這個 crate 最重要的用法是宣告延遲初始化複雜常量:

lazy_static! {
    static ref TICK_INTERVAL: Duration = Duration::from_secs(7 * 24 * 60 * 60);
}

這個技巧並不是完全透明,但直到 Rust 擁有執行時表示式,這就是你所能想到最好的辦法

maplit

為了與上述 crate 更好地配合,且使用與標準庫中的 vec![] 類似語法,我們可以使用 maplit

它透過定義一些非常簡單的 hashmap!hashset! 宏,讓你可以透過“字面量”
新增 HashMapHashSet

lazy_static! {
    static ref IMAGE_EXTENSIONS: HashMap<&'static str, ImageFormat> = hashmap!{
        "gif" => ImageFormat::GIF,
        "jpeg" => ImageFormat::JPEG,
        "jpg" => ImageFormat::JPG,
        "png" => ImageFormat::PNG,
    };
}

hashmap! 宏內部,hashmap! 會根據傳入的字面量呼叫 HashMap::insert,接著返回已包含傳入字面量的 HashMap

try_opt

在 Rust 引入 ? 運算子之前(目前已可以使用),在處理 Result 時傳播錯誤的慣用手法是使用 try! 宏。

try_opt 為Option型別實現類似的宏,用於傳播 None,這個宏的使用方法也相當直觀:

fn parse_ipv4(s: &str) -> Option<(u8, u8, u8, u8)> {
    lazy_static! {
        static ref RE: Regex = Regex::new(
            r"^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$"
        ).unwrap();
    }
    let caps = try_opt!(RE.captures(s));
    let a = try_opt!(caps.get(1)).as_str();
    let b = try_opt!(caps.get(2)).as_str();
    let c = try_opt!(caps.get(3)).as_str();
    let d = try_opt!(caps.get(4)).as_str();
    Some((
        try_opt!(a.parse().ok()),
        try_opt!(b.parse().ok()),
        try_opt!(c.parse().ok()),
        try_opt!(d.parse().ok()),
    ))
}

直到 Rust 支援 ?try_opt! 不失為一個可接受的 workaroud。

exitcode

基本上每個主流作業系統中的一個常見約定,如果一個程式以不同於0(零)的程式碼退出,
表示程式發生錯誤,Linux進一步劃分錯誤程式碼的空間,並且與BSD一起它還包括sysexits.h標頭檔案包含一些更專業的程式碼。

許多程式和語言都採用了這些方法。在Rust中,也可以使用那些常見錯誤的半標準名稱。需要做的就是將exitcode crate新增到您的專案依賴中:

fn main() {
    let options = args::parse().unwrap_or_else(|e| {
        print_args_error(e).unwrap();
        std::process::exit(exitcode::USAGE);
    });

除了 USAGETEMPFAIL 之類的常量之外,exitcode
還為儲存退出程式碼的整數型別定義了一個 ExitCode 別名。除其他外,也可以將它用作頂級函式的返回型別:

    let code = do_stuff(options);
    std::process::exit(code);
}

fn do_stuff(options: Options) -> exitcode::ExitCode {
    // ...
}

enum-set

在 Java 中,有一種普通 Set 的特化介面用於列舉型別:EnumSet。它的成員非常緊湊地表示為位而不是雜湊元
素。

enum_set 實現了一個相似(儘管不如那麼強)的結構。對於一個 #[repr(u32)] 列舉型別:

#[repr(u32)]
#[derive(Clone, Copy, Debug Eq, Hash, PartialEq)]
enum Weekday {
    Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday,
}

你可以建立一個其成員的 EnumSet :

let mut weekend: EnumSet<Weekday> = EnumSet::new();
weekend.insert(Weekday::Saturday);
weekend.insert(Weekday::Sunday);

只要你實現一個簡單 trait,這個 trait 宣告瞭怎麼將這個列舉值轉換為 u32 或怎麼從 u32 轉換而來:

impl enum_set::CLike for Weekday {
    fn to_u32(&self) -> u32            { *self as u32 }
    unsafe fn from_u32(v: u32) -> Self { std::mem::transmute(v) }
}

這樣的優點是具有由單個無符號32位整數表示的集合結構,所有集合操作複雜性都是O(1),這些操作包括成員資格檢查,兩套聯合,它們的交集,差異等等。

antidote

作為實現“無畏併發”諾言一部分,Rust 在 std::sync 模組定義了許多同步原語。
MutexRwLock 和它們的類似機制有一個共同之處就是,如果一個執行緒在持有它們的情況下恐慌,
它們的鎖會變得“中毒”。因此,獲取鎖定需要處理潛在的PoisonError

然而,對於許多程式來說,鎖定中毒甚至不是遙遠的,而是一種直接的不可能的情況。
如果你遵循併發資源共享的最佳實踐,你將不會持有多個指令的鎖,沒有解包或任何其他恐慌的機會!()。
不幸的是,你無法靜態地向Rust編譯器證明這一點,因此它仍然需要你處理一個不可能發生的PoisonError。

如名所示 antidote 這正是其能提供幫助的地方。
在 antidote 中,您可以找到std :: sync提供的所有相同的鎖和保護API,
只是沒有PoisonError。在許多情況下,這種刪除從根本上簡化了介面,
例如將Result <Guard,Error>返回型別轉換為Guard。

代價顯而易見,那就是你需要保證所有持有“免疫性”鎖的執行緒:

  1. 完全不會恐慌;或者
  2. 如果他們恐慌,不會將保護資源置於不一致的狀態

就像之前提到過的那樣,實現這一目標的最佳方法是將鎖定保護的關鍵部分保持在最小和絕對可靠的狀態。

matches

模式匹配是Rust最重要的特性之一,但是一些相關的語言結構具有令人尷尬的缺點。例如,if let條件不能與布林測試結合使用:

if let Foo(_) = x && y.is_good() {

因此需要額外的巢狀或完全不同的方法。

值得慶幸的是,為了幫助解決這種情況,有一個方便的matches crate 。
除了與它同名的 matches! 宏:

if matches!(x, Foo(_)) && y.is_good() {

它也暴露了斷言宏 assert_match!debug_assert_match!,這些宏可用於生產和測試程式碼。

本作品採用《CC 協議》,轉載必須註明作者和本文連結
多少事,從來急。天地轉,光陰迫。

相關文章