Bootstrap

Rust从0到1-代码组织-use关键字

用于调用函数的路径都很长,而且每次调用函数的时候都要写一遍,很不方便。譬如,前面的例子中, 不管是 add_to_waitlist 函数的绝对路径还是相对路径,都包含了 front_of_house 和 hosting 两部分,十分冗长。Rust提供了一种简化的方法让我们可以一次性将路径引入作用域,即使用 use 关键字,后面调用的时候就可以向调用本地函数一样:

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
}

例子中我们将 crate::front_of_house::hosting 模块引入了 eat_at_restaurant 函数的作用域,因此我们只需要使用 hosting::add_to_waitlist 即可在 eat_at_restaurant 中调用 add_to_waitlist 函数。

use 关键字类似于文件系统中的软连接。通过 use crate::front_of_house::hosting 让 hosting 在 eat_at_restaurant 作用域中变为有效的,就像 hosting 模块是在 crate root 中定义的一样。注意,模块的私有性还是保持不变的,还会按照模块本身定义的私有性进行检查。除了使用绝对路径以外,我们还可以使用相对路径来将函数引入作用域:

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

use self::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
}

use 使用惯例

在前面的例子中,我们不禁会想直接使用 add_to_waitlist 函数不是比 hosting::add_to_waitlist 更简短:

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

use crate::front_of_house::hosting::add_to_waitlist;

pub fn eat_at_restaurant() {
    add_to_waitlist();
    add_to_waitlist();
    add_to_waitlist();
}

就像上面的例子,我们可以直接将 add_to_waitlist 方法引入作用域,但是将其所在模块引入作用域才是惯用或推荐的做法,这样我们在调用函数时就需要带上其父模块,可以表明函数所属,增加可读性,同时减少路径长度,

相反,使用 use 引入结构体、枚举或其他外部引用时,惯用做法是直接引用他们:

use std::collections::HashMap;

fn main() {
    let mut map = HashMap::new();
    map.insert(1, 2);
}

这个习惯用法有一个例外,就是我们想使用 use 引入两个具有相同名称的结构体、枚举或其他外部引用时(这个在语法上不允许,因为无法知道具体是需要使用哪一个),这时我们就需要带上父模块用以区分(还有另一种处理方法,我们后面将介绍):

use std::fmt;
use std::io;

fn function1() -> fmt::Result {
    // --snip--
}

fn function2() -> io::Result<()> {
    // --snip--
}

当然惯例不是硬性要求,只是大部人已经习惯了以这种方式阅读和编写 Rust 代码,就像我们用一种大部人都习惯的表达方式去和别人沟通。

使用 as 

将两个同名类型引入作用域还有另一个解决办法:使用 as 关键字指定一个新的名称(别名):

use std::fmt::Result;
use std::io::Result as IoResult;

fn function1() -> Result {
    // --snip--
}

fn function2() -> IoResult<()> {
    // --snip--
}

这和前面带上父模块的做法都是惯用方式,因此,我们可以根据自己的实际情况选用。

使用 pub use

当使用 use 将函数、结构体和枚举等引入作用域时,他们新的名字(我们在使用 use 引入后,调用他们使用的路径)在新作用域中是私有的。为了让外部在使用我们代码时可以按照我们使用的新名字调用我们引入的函数,可以使用一种称作 “重导出(re-exporting)”的技术,即结合 pub 和 use 关键字使用(这个感觉比较特别,在其它语言中比较少见):

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

pub use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
    hosting::add_to_waitlist();
}

上例中我们通过 pub use 将 hosting::add_to_waitlist 重导出,这样外部代码就可以通过新名字 hosting::add_to_waitlist 来调用 add_to_waitlist 函数。如果仅使用useeat_at_restaurant 函数可以通过 hosting::add_to_waitlist 调用函数,但外部代码则不能使用这个新路径。

当我们根据我们所处的领域对代码进行组织,而使用我们代码的程序员所处的领域不同时,重导出将会很有用(我理解是大家所处的行业等背景不同,对代码的组织就会不同,就像DDD里所提倡的理念,保持相同的领域语言和概念进行沟通)。譬如,在前面餐馆的例子中,从经营餐馆的角度的会使用“前台”和“后台”的术语,但对于光顾一家餐馆的顾客,一般就不会使用这些术语来定义一家餐馆。通过使用 pub use,我们可以在内部使用一种代码组织结构,另外,可以对外暴露看上去不同的结构。这样可以在不改变我们内部的领域语言的同时,让开发这个库的程序员可以通过使用这个库的程序员的领域语言进行沟通(根据我的经验,沟通特别重要,也是最经常碰到的问题)。

使用外部包

我们可以通过在 Cargo.toml 中加入依赖的描述来告诉 Cargo 要从 crates.io 下载相关其依赖(避免重复造轮子;)):

[dependencies]
rand = "0.8.3"

接着,我们可以通过 use 将我们需要使用到的依赖库中的函数、结构体或枚举等引入我们代码的作用域(注意,这里的 Rng 是trait,在Rust中 trait 必须显示的进行引入,才能使用其方法,这可能是因为 trait 方法名称冲突的可能性比较大):

use std::io;
use rand::Rng;

fn main() {
    println!("Guess the number!");
    let secret_number = rand::thread_rng().gen_range(1, 101);
    println!("The secret number is: {}", secret_number);
    println!("Please input your guess.");
    let mut guess = String::new();
    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");
    println!("You guessed: {}", guess);
}

另外,需要注意的是标准库(std)也属于外部依赖,但是因为标准库包含在 Rust 的默认分发中,因此无需修改 Cargo.toml 来引入,不过仍然需要通过 use 将标准库中的定义引入作用域中来使用它们,比如 HashMap(前面我们说过,其中还有一部分已经默认引入了,可以直接使用,譬如枚举中的 Some):

#![allow(unused)]
fn main() {
use std::collections::HashMap;
}

路径嵌套

当需要引入很多定义于相同包或相同模块的内容时,为每一项单独列出一行会让源码文件显得很长,也不方便阅读,譬如:

use rand::Rng;
// --snip--
use std::cmp::Ordering;
use std::io;
// --snip--

fn main() {
    // --snip--
}

为了解决这个问题,Rust提供了路径嵌套的方式将相关依赖项通过一行代码引入作用域,并且可以在路径的任何层级使用路径嵌套。在比较大的源码文件中,使用路径嵌套的方式从相同包或模块中引入依赖,可以明显的减少 use 语句的行数(从语法层面提供了解决方案,不像利用IDE的特性把引入外部依赖部分隐藏。有时候我们还是需要看一下的):

use std::{cmp::Ordering, io};
use std::io::{self, Write};

Glob操作符 

如果我们想将一个路径下所有公有的内容引入作用域,可以其父路径后跟 *(glob操作符):

#![allow(unused)]
fn main() {
  use std::collections::*;
}

例子中的 use 语句将 std::collections 中定义的所有公有内容引入当前作用域。使用 glob 操作符时需要注意:Glob 会使我们难以清楚的知道我们使用了哪些外部依赖,以及他们是在哪个库里的。因此,glob 运算符通常用于测试,将所有需要测试的内容引入作用域(我们将后面在 “如何编写测试” 部分讲解);glob 操作符也用于 prelude 模式,这个可以参考官方《the standard library documentation》。