【译】12 条你可能还不知道的 Rust 提示和技巧

Rust 是一种伟大的编程语言: 可靠、快速、令人愉快,但也相当复杂。在过去的两年里,我一直在专业和业余项目(比如 Espanso)中使用它。在那段时间里,我偶然发现了许多有用的模式和 crate,我希望在刚开始学习它的时候就知道它。

使用 Cow<str> 作为返回类型

有时你需要编写接受字符串片段(&str)并有条件地返回其修改版本或原始版本的方法。对于这些情况,你可以使用 Cow<str>,以便只在必要时分配新内存。

use std::borrow::Cow;

fn capitalize(name: &str) -> Cow<str> {
    match name.chars().nth(0) {
        Some(first_char) if first_char.is_uppercase() => {
            // No allocation is necessary, as the string
            // already starts with an uppercase char
            Cow::Borrowed(name)
        }
        Some(first_char) => {
            // An allocation is necessary, as the old string
            // does not start with an uppercase char
            let new_string: String = first_char.to_uppercase()
              .chain(name.chars().skip(1))
              .collect();

            Cow::Owned(new_string)
        },
        None => Cow::Borrowed(name),
    }
}

fn main() {
    println!("{}", capitalize("bob"));   // Allocation
    println!("{}", capitalize("John"));  // No allocation
}

使用 Crossbeam channel 而非标准库中的 channel

crossbeam crate 提供了一个强大的替代标准 channel,支持 select 操作,超时等等。类似于你在 Golang 和传统的 Unix 套接字的使用。

use crossbeam_channel::{select, unbounded};
use std::time::Duration;

fn main() {
    let (s1, r1) = unbounded::<i32>();
    let (s2, r2) = unbounded::<i32>();
    s1.send(10).unwrap();

    select! {
        recv(r1) -> msg => println!("r1 > {}", msg.unwrap()),
        recv(r2) -> msg => println!("r2 > {}", msg.unwrap()),
        default(Duration::from_millis(100)) => println!("timed out"),
    }
}

Golang 风格的 Scopeguard 操作符

如果你从 Golang 转过来,你可能会错过某些用例的 “defer” 操作符(比如使用原始指针时释放内存或关闭套接字)。

在 Rust (除了 RAII 模式之外)中,你可以使用作用域保护(scopeguard)框来轻松实现“清理”逻辑。

#[macro_use(defer)] extern crate scopeguard;

fn main() {
    println!("start");
    {
        // This action will run at the end of the current scope
        defer! {
           println!("defer");
        }

        println!("scope end");
    }
    println!("end");

    // Output:
    // start
    // scope end
    // defer
    // end
}

使用 Cargo-make 进行打包

构建脚本对很多场景都很有用,但通常不适用于打包。我最喜欢的解决方案是 sagiegurariCargo Make,一个基于 Rust 的任务运行器和构建工具。

自定义并链接 panic 处理程序

应急处理程序(Panic handlers)(也称 hooks)可以被重写和链接,这在为应用程序设置自定义错误报告和日志记录时特别有用。

use std::panic::{set_hook, take_hook};

fn main() {
    let prev_hook = take_hook();

    set_hook(Box::new(move |panic| {
        println!("custom logging logic {}", panic);

        prev_hook(panic);
    }));

    let prev_hook = take_hook();

    set_hook(Box::new(move |panic| {
        println!("custom error reporting logic {}", panic);

        prev_hook(panic);
    }));

    panic!("test")

    // Output:
    // custom error reporting logic panicked at 'test', src/main.rs:20:5
    // custom logging logic panicked at 'test', src/main.rs:20:5
}

使用 VSCode 中的 Rust Analyzer 插件开发

matklad 写的 Rust Analyzer 扩展是显着优于“官方” 的 Rust 插件之一。不幸的是,它仍然作为第二个查询结果出现在扩展市场上,误导了很多初学者。

使用闭包时使用 impl Trait

如果可能的话,倾向于将闭包传递给一个函数(称为 impl Trait) ,而不是通用函数,以保持签名的干净。对于非常见的情况,你可能需要用 Box<Fn()> 包装闭包,但请记住这将会有额外的开销。

// Instead of this

fn setup_teardown_generic<A: FnOnce()>(action: A) {
    println!("setting up...");

    action();

    println!("tearing down...")
}

// Use this

fn setup_teardown(action: impl FnOnce()) {
    println!("setting up...");

    action();

    println!("tearing down...")
}

// As a note, this pattern is very useful inside tests
// to create/destroy resources.

fn main() {
    setup_teardown(|| {
        println!("Action!");
    })

    // Output:
    // setting up...
    // Action!
    // tearing down...
}

使用 VSCode 时,配置保存时启用 Clippy

如果你正在使用 VSCode + RA,我强烈建议进入设置 > RA > 检查保存: 命令和设置“ clippy”作为新的默认值,而不是“检查”。同样的用户体验,更好的警告。

在常用错误处理中使用“thiserror”和“anyway”

使用 thiserror 和邋遢板条箱处理惯用错误。当消费者需要根据错误有条件地采取行动时,你应该使用此错误,否则无论如何。一个很好的指导方针是“对库和应用程序使用
这个错误”。

使用 dbg!() 宏代替 println!()

在调试时使用 dbg!() 宏而不是 println!()。代码更少,信息更有用。

fn main() {
    let var1 = 2;

    println!("{}", 2); // Output: 2
    dbg!(var1);        // Output: [src/main.rs:5] var1 = 2
    dbg!(var1 * 2);    // Output: [src/main.rs:6] var1 * 2 = 4
}

include_str!() 和 include_bytes!() 宏

使用 include_str!()include_bytes!() 宏在编译时读取文件的内容并将其存储在 const 中。有助于避免混乱使用多行字符串文本。

// Both of these files are read at *compile time*
const FILE_CONTENT: &str = include_str!("./path/to/the/file.txt");
const BINARY_FILE_CONTENT: &[u8] = include_bytes!("./path/to/image.png");

fn main() {
    println!("{}", FILE_CONTENT);  // Output: file content as string
}

与 c/c++ 代码的集成

如果你需要将 c/c++ 代码与 Rust 集成在一起,那么 cc crate 和适当的构建脚本可以让你更加得心应手。例如,我使用它们将流行的 c++ gui 框架 wxWidgets 与我的项目 Espanso 集成(参见构建脚本

// Inside the build script (build.rs)

fn main() {
  println!("cargo:rerun-if-changed=src/native.c");
  println!("cargo:rerun-if-changed=src/native.h");
  cc::Build::new()
    .include("src/native.h")
    .file("src/native.c")
    .compile("nativemodule");
  println!("cargo:rustc-link-lib=static=nativemodule");
}

// Then, in another Rust file (for example, ffi.rs)

#[link(name = "nativemodule", kind = "static")]
extern "C" {
  pub fn your_cool_c_module();
}

谢谢阅读!如果你喜欢这些话题,请务必在 TwitterYouTube 上关注我,随时了解最新的文章和视频。

posted @ 2022-02-18 09:29  suhanyujie  阅读(467)  评论(0编辑  收藏  举报