路径和模块系统变更

Minimum Rust version: 1.31

摘要

  • use 声明中的路径现在与其他路径的工作方式相同。

  • :: 开头的路径现在必须跟随一个外部 crate 名称。

  • pub(in path) 可见性修饰符中的路径现在必须以 crateselfsuper 开头。

动机

模块系统对于 Rust 新手来说通常是最难掌握的部分之一。当然,每个人都有需要时间才能掌握的东西,但模块系统之所以让很多人感到困惑,有一个根本原因:尽管定义模块系统的规则简单且一致,但其结果可能令人感觉不一致、违反直觉且难以理解。

因此,Rust 2018 版本引入了一些新的模块系统特性,但这些特性最终 简化 了模块系统,使其运作方式更加清晰。

简要总结如下

  • extern crate 在 99% 的情况下不再需要。

  • crate 关键字指向当前 crate。

  • 路径可以以 crate 名称开头,即使在子模块内也可以。

  • :: 开头的路径必须引用一个外部 crate。

  • 一个 foo.rs 文件和 foo/ 子目录可以共存;将子模块放在子目录中时,不再需要 mod.rs

  • use 声明中的路径与其他路径的工作方式相同。

这样说来,这些规则可能看起来是随意的新规则,但总体而言,心智模型(mental model)现在已大大简化。继续阅读以了解更多详情!

更多详情

让我们逐一讨论每个新特性。

不再需要 extern crate

这一点非常直接:你不再需要编写 extern crate 来将一个 crate 导入到你的项目中。之前

// Rust 2015

extern crate futures;

mod submodule {
    use futures::Future;
}

之后

// Rust 2018

mod submodule {
    use futures::Future;
}

现在,要将一个新的 crate 添加到你的项目中,你可以将其添加到你的 Cargo.toml 文件中,然后就没有第二步了。如果你不使用 Cargo,你之前也需要传递 --extern 标志来告诉 rustc 外部 crate 的位置,所以你只需要继续做之前的事情就可以了。

一个例外

这条规则有一个例外,那就是“sysroot” crate。这些是随 Rust 本身一起分发的 crate。

通常,这些只在非常特殊的情况下才需要。从 1.41 版本开始,rustc 接受 --extern=CRATE_NAME 标志,它以类似于 extern crate 的方式自动添加给定的 crate 名称。构建工具可以使用这个特性将 sysroot crate 注入到 crate 的 prelude 中。Cargo 没有一种通用的方式来表达这一点,但它会为 proc_macro crate 使用此功能。

需要显式导入 sysroot crate 的一些示例包括:

  • std:通常这不是必需的,因为除非 crate 被标记为 #![no_std],否则 std 会被自动导入。

  • core:通常这不是必需的,因为除非 crate 被标记为 #![no_core],否则 core 会被自动导入。例如,标准库本身使用的一些内部 crate 需要这样做。

  • proc_macro:从 1.42 版本开始,如果这是一个 proc-macro crate,Cargo 会自动导入它。如果你想支持旧版本,或者使用其他未向 rustc 传递适当的 --extern 标志的构建工具,则需要 extern crate proc_macro;

  • allocalloc crate 中的项通常通过 std crate 中的转导出(re-exports)访问。如果你正在处理一个支持分配的 no_std crate,则可能需要显式导入 alloc

  • test:这只在 nightly channel 上可用,并且通常只用于不稳定的基准测试(benchmark)支持。

extern crate 的另一个用途是导入宏;现在不再需要了。宏可以像其他任何项一样使用 use 导入。例如,以下使用 extern crate 的方式

#[macro_use]
extern crate bar;

fn main() {
    baz!();
}

可以改为如下形式

use bar::baz;

fn main() {
    baz!();
}

重命名 crate

如果你之前使用 as 来重命名 crate,像这样

extern crate futures as f;

use f::Future;

那么仅删除 extern crate 行是无效的。你需要这样做

use futures as f;

use self::f::Future;

使用 f 的任何模块都需要进行此更改。

crate 关键字指向当前 crate

use 声明和其他代码中,你可以使用 crate:: 前缀来引用当前 crate 的根。例如,在同一 crate 中的任何位置,crate::foo::bar 都将始终引用模块 foo 中的名称 bar

前缀 :: 之前可以引用 crate 根或外部 crate;现在它明确地引用外部 crate。例如,::foo::bar 总是引用外部 crate foo 中的名称 bar

外部 crate 路径

之前,在模块中不使用 use 导入而使用外部 crate 需要在路径前加上 ::

// Rust 2015

extern crate chrono;

fn foo() {
    // this works in the crate root
    let x = chrono::Utc::now();
}

mod submodule {
    fn function() {
        // but in a submodule it requires a leading :: if not imported with `use`
        let x = ::chrono::Utc::now();
    }
}

现在,外部 crate 名称在整个 crate 中都是可见的(in scope),包括子模块。

// Rust 2018

fn foo() {
    // this works in the crate root
    let x = chrono::Utc::now();
}

mod submodule {
    fn function() {
        // crates may be referenced directly, even in submodules
        let x = chrono::Utc::now();
    }
}

如果你的本地模块或项与外部 crate 同名,则以该名称开头的路径将被视为指向本地模块或项。要明确引用外部 crate,请使用 ::名称 的形式。

不再需要 mod.rs

在 Rust 2015 中,如果你有一个子模块

// This `mod` declaration looks for the `foo` module in
// `foo.rs` or `foo/mod.rs`.
mod foo;

它可以位于 foo.rsfoo/mod.rs 中。如果它本身有子模块,则它必须foo/mod.rs。因此,foo 的一个子模块 bar 将位于 foo/bar.rs

在 Rust 2018 中,取消了包含子模块的模块必须命名为 mod.rs 的限制。foo.rs 可以直接是 foo.rs,而子模块仍然是 foo/bar.rs。这消除了特殊名称,如果你在编辑器中打开了一堆文件,你可以清楚地看到它们的文件名,而不是一堆名为 mod.rs 的标签页。

Rust 2015 Rust 2018
.
├── lib.rs
└── foo/
    ├── mod.rs
    └── bar.rs
.
├── lib.rs
├── foo.rs
└── foo/
    └── bar.rs

use 路径

Minimum Rust version: 1.32

与 Rust 2015 相比,Rust 2018 简化并统一了路径处理。在 Rust 2015 中,use 声明中的路径与其他地方的路径工作方式不同。特别是,use 声明中的路径总是从 crate 根开始,而其他代码中的路径则隐式地从当前作用域开始。这些差异在顶层模块中没有任何影响,这意味着在处理一个足够大、包含子模块的项目之前,一切似乎都很直接。

在 Rust 2018 中,use 声明和其他代码中的路径工作方式相同,无论是在顶层模块还是在任何子模块中。你可以使用从当前作用域开始的相对路径,一个从外部 crate 名称开始的路径,或者一个以 ::cratesuperself 开头的路径。

看起来像这样的代码

// Rust 2015

extern crate futures;

use futures::Future;

mod foo {
    pub struct Bar;
}

use foo::Bar;

fn my_poll() -> futures::Poll { ... }

enum SomeEnum {
    V1(usize),
    V2(String),
}

fn func() {
    let five = std::sync::Arc::new(5);
    use SomeEnum::*;
    match ... {
        V1(i) => { ... }
        V2(s) => { ... }
    }
}

在 Rust 2018 中看起来完全一样,只是你可以删除 extern crate 那一行

// Rust 2018

use futures::Future;

mod foo {
    pub struct Bar;
}

use foo::Bar;

fn my_poll() -> futures::Poll { ... }

enum SomeEnum {
    V1(usize),
    V2(String),
}

fn func() {
    let five = std::sync::Arc::new(5);
    use SomeEnum::*;
    match ... {
        V1(i) => { ... }
        V2(s) => { ... }
    }
}

相同的代码在子模块中也可以完全不变地工作

// Rust 2018

mod submodule {
    use futures::Future;

    mod foo {
        pub struct Bar;
    }

    use foo::Bar;

    fn my_poll() -> futures::Poll { ... }

    enum SomeEnum {
        V1(usize),
        V2(String),
    }

    fn func() {
        let five = std::sync::Arc::new(5);
        use SomeEnum::*;
        match ... {
            V1(i) => { ... }
            V2(s) => { ... }
        }
    }
}

这使得在项目中移动代码变得容易,并避免给多模块项目引入额外的复杂性。