给Rust初学者的5个小建议

BobAnkh published on
10 min, 1961 words

本篇文章主要介绍给Rust初学者的5个小建议。

原视频来自Youtube: 5 Tips for Rust Beginners, 本文对其进行翻译简化.

已获得原作者许可。未经允许,请勿随意转载。

建议1: 利用Cargo工具

Rust有很多好用的开发工具,你可以使用cargo --list来查看所有可用命令的列表(其中有部分是译者所安装的额外工具):

cargo --list
Installed Commands:
    b                    alias: build
    bench                Execute all benchmarks of a local package
    build                Compile a local package and all of its dependencies
    c                    alias: check
    check                Check a local package and all of its dependencies for errors
    chef
    clean                Remove artifacts that cargo has generated in the past
    clippy               Checks a package to catch common mistakes and improve your Rust code.
    config               Inspect configuration values
    d                    alias: doc
    doc                  Build a package's documentation
    fetch                Fetch dependencies of a package from the network
    fix                  Automatically fix lint warnings reported by rustc
    fmt                  Formats all bin and lib files of the current crate using rustfmt.
    generate-lockfile    Generate the lockfile for a package
    git-checkout         This subcommand has been removed
    init                 Create a new cargo package in an existing directory
    install              Install a Rust binary. Default location is $HOME/.cargo/bin
    locate-project       Print a JSON representation of a Cargo.toml file's location
    login                Save an api token from the registry locally. If token is not specified, it will be read from stdin.
    logout               Remove an API token from the registry locally
    metadata             Output the resolved dependencies of a package, the concrete used versions including overrides, in machine-readable format
    mir-checker
    miri
    new                  Create a new cargo package at <path>
    owner                Manage the owners of a crate on the registry
    package              Assemble the local package into a distributable tarball
    pkgid                Print a fully qualified package specification
    publish              Upload a package to the registry
    r                    alias: run
    read-manifest        Print a JSON representation of a Cargo.toml manifest.
    report               Generate and display various kinds of reports
    run                  Run a binary or example of the local package
    rustc                Compile a package, and pass extra options to the compiler
    rustdoc              Build a package's documentation, using specified custom flags.
    search               Search packages in crates.io
    t                    alias: test
    tarpaulin
    test                 Execute all unit and integration tests and build examples of a local package
    tree                 Display a tree visualization of a dependency graph
    udeps
    uninstall            Remove a Rust binary
    update               Update dependencies as recorded in the local lock file
    vendor               Vendor all dependencies for a project locally
    verify-project       Check correctness of crate manifest
    version              Show version information
    yank                 Remove a pushed crate from the index

有很多命令比如build或者run是大家所熟知的,但也有一些命令对于初学者来说可能是相对陌生的,比如cargo doc来构建包文档,又比如cargo clippy来检查常见错误以及提升你的rust代码质量。

这里简单介绍一下clippy的使用。clippy是官方rust-lang所维护的工具,仓库在rust-lang/rust-clippy,如果需要自己安装的话,在安装了rustup的情况下,可以使用rustup component add clippy来安装。

安装之后可以运行cargo clippy来进行检查,或cargo clippy --fix来检查并自动修正部分问题。全部检查规则可见clippy-lints

以下举一个使用clippy的例子:

fn main() {
    let my_vec: Vec<String> = Vec::new();
    if my_vec.len() == 0 {
        println!("my_vec is empty");
    }
}

如果运行cargo clippy,就会提示你如下建议:

warning: length comparison to zero
 --> src/main.rs:3:8
  |
3 |     if my_vec.len() == 0 {
  |        ^^^^^^^^^^^^^^^^^ help: using `is_empty` is clearer and more explicit: `my_vec.is_empty()`
  |
  = note: `#[warn(clippy::len_zero)]` on by default
  = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#len_zero

这个建议就是让你使用is_empty()这个函数来替代len()来检查长度是否为0.

那么我们修改后的程序就可以变为:

fn main() {
    let my_vec: Vec<String> = Vec::new();
    if my_vec.is_empty() {
        println!("my_vec is empty");
    }
}

建议2: 不要有哨兵值(Sentinel Values)

核心就是用Option代替空值返回

其他语言比如Javascript,在某些情况下会利用哨兵值返回空值,比如如下这个例子中,在找不到条件值的情况下,会返回-1以告诉你没有找到,这个值就是哨兵值,那么如果我们需要利用返回结果时,则需要对于这个值做额外检查:

const arr = [1,2,3]
arr.find(item => item === 4)

而Rust则具有一个更好的类型系统来应对这个问题,你不需要再依赖哨兵值了,可以直接使用Option来代替,如下:

  • 使用哨兵值的写法(不推荐):
fn maybe_get_name(x: bool) -> &'static str {
    if x {
        "Bob"
    } else {
        ""
    }
}

fn main() {
    let name = maybe_get_name(true);
    println!("first name is {}", name);
    let name = maybe_get_name(false);
    println!("second name if {}", name);
}
  • 使用Option的写法(推荐):
fn maybe_get_name(x: bool) -> Option<&'static str> {
    if x {
        Some("Bob")
    } else {
        None
    }
}

fn main() {
    let name = maybe_get_name(true);
    if let Some(name) = name {
        println!("first name is {}", name);
    }
    let name = maybe_get_name(false);
    if let Some(name) = name {
        println!("second name is {}", name);
    }
}

可以看到,在第一个例子中,第一次打印出来了Bob,第二次打印出来了空,但其实这种情况下是很难判断是否为一个空值的;而在第二个例子中,第一次同样打印出来了Bob,但第二次就没有进行打印了,这里就是明确发现了是个空值的情况。利用Option,可以让我们更好的去判断是否为空值,而不需要知道或定义一个哨兵值了。

建议3: 使用impl创建更灵活的API参数

这里我们创建一个打印出文件内容的函数作为例子,这个函数可以接受一个文件名作为参数,会打印出文件内容:

use std::fs;

fn print_file_content(path: &str) {
    let content = fs::read_to_string(path).unwrap();
    println!("{}", content);
}

fn main() {
    print_file_content("README.md");
}

如果我们只是这样定义函数参数,那么我们必须要传入一个字符串slice作为文件名称,而不能够传入其他的同样可以指定到文件的参数,例如Path::new("README.md")。所以更好的做法是:

use std::fs;

fn print_file_content(path: impl AsRef<Path>) {
    let content = fs::read_to_string(path).unwrap();
    println!("{}", content);
}

fn main() {
    print_file_content("README.md");
    let path = Path::new("README.md");
    print_file_content(path);
}

建议4: 为你的结构体实现一些例如Debug, Default等通用的Traits

例如我们有一个这样的结构体:

struct SomeStruct {
    inner: Option<Box<SomeStruct>>
}

然后我们在main函数里面初始化了一个这样的变量:

fn main() {
    let nested_struct = SomeStruct {
        inner: Some(Box::new(SomeStruct {
            inner: Some(Box::new(SomeStruct {inner: None}))
        })),
    };
}

然而我们这样没法直接打印出这个变量。这个时候我们就应该给这个变量实现一个Debug的trait:

#[derive(Debug)]
struct SomeStruct {
    inner: Option<Box<SomeStruct>>
}

这样我们就可以在main函数中打印出这个变量了:

fn main() {
    let nested_struct = SomeStruct {
        inner: Some(Box::new(SomeStruct {
            inner: Some(Box::new(SomeStruct {inner: None}))
        })),
    };
    // 连缀在一起的打印
    println!("{:?}", nested_struct);
    // 格式化的打印
    println!("{:#?}", nested_struct);
}

运行cargo run打印出来的结果如下:

SomeStruct { inner: Some(SomeStruct { inner: Some(SomeStruct { inner: None }) }) }
SomeStruct {
    inner: Some(
        SomeStruct {
            inner: Some(
                SomeStruct {
                    inner: None,
                },
            ),
        },
    ),
}

然后如果我们还想看到我们打印的位置和变量的名称,那么可以使用dbg!宏来打印:

fn main() {
    let nested_struct = SomeStruct {
        inner: Some(Box::new(SomeStruct {
            inner: Some(Box::new(SomeStruct {inner: None}))
        })),
    };
    dbg!(nested_struct);
}

运行cargo run打印结果如下:

[src/main.rs:12] nested_struct = SomeStruct {
    inner: Some(
        SomeStruct {
            inner: Some(
                SomeStruct {
                    inner: None,
                },
            ),
        },
    ),
}

我们还可以为SomeStruct实现一个Default的trait:

#[derive(Debug, Default)]
struct SomeStruct {
    inner: Option<Box<SomeStruct>>
}

这样我们就可以直接使用默认值来初始化这个变量了:

fn main() {
    let nested_struct = SomeStruct {
        ..Default::default()
    };
    dbg!(nested_struct);
}

运行cargo run打印结果如下:

[src/main.rs:10] nested_struct = SomeStruct {
    inner: None,
}

建议5: 命名规范

详细可见官方api-guidelines/naming

通常, Rust 倾向于在“类型级别”(类型和traits)使用 UpperCamelCase, 在值级别使用 snake_case 。更加精准的说明可见下表:

ItemConvention
Cratesunclear, kebab-casesnake_case一般认为均可
Modulessnake_case
TypesUpperCamelCase
TraitsUpperCamelCase
Enum variantsUpperCamelCase
Functionssnake_case
Methodssnake_case
General constructorsnew or with_more_details
Conversion constructorsfrom_some_other_type
Macrossnake_case!
Local variablessnake_case
StaticsSCREAMING_SNAKE_CASE
ConstantsSCREAMING_SNAKE_CASE
Type parametersconcise UpperCamelCase, usually single uppercase letter: T
Lifetimesshort lowercase, usually a single letter: 'a, 'de, 'src
Featuresunclear

更多的详细描述请见官方指南。

其实cargo check已经对于命名会做出一些检查,所以在编码完成之后可以运行该命令,会给出不够规范的命名的修改建议。