You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

14 KiB

模块Module

在本章节我们将深入讲讲Rust的代码构成单元模块。使用模块可以将包中的代码按照功能性进行重组最终实现更好的可读性及易用性。同时我们还能非常灵活地去控制代码的可见性进一步强化了Rust的安全性。

创建嵌套模块

小旅馆sorry是小餐馆相信大家都挺熟悉的学校外的估计也没少去那么咱就用小餐馆为例看看Rust的模块该如何使用。

使用cargo new --lib restaurant创建一个小餐馆,注意,这里创建的是一个库类型的package, 然后将以下代码放入src/lib.rs中:

// 餐厅前厅,用于吃饭
mod front_of_house {
    mod hosting {
        fn add_to_waitlist() {}

        fn seat_at_table() {}
    }

    mod serving {
        fn take_order() {}

        fn serve_order() {}

        fn take_payment() {}
    }
}

以上的代码创建了三个模块,有几点需要注意的:

  • 使用mod关键字来创建新模块,后面紧跟着模块名称
  • 模块可以嵌套,这里嵌套的原因是招待客人和服务都发生在前厅,因此我们的代码模拟了真实场景
  • 模块中可以定义各种Rust类型例如函数、结构体、枚举、特征等
  • 所有模块均定义在同一个文件中

类似上述代码中所做的,使用模块,我们就能将功能相关的代码组织到一起,然后通过一个模块名称来说明这些代码为何被组织在一起。这样其它程序员在使用你的模块时,就可以更快的理解和上手。

模块树

上一节中,我们提到过src/main.rssrc/lib.rs被称为包根(crate root),这个奇葩名称的来源(我不想承认是自己翻译水平太烂- , -)是由于这两个文件的内容形成了一个模块crate,该模块位于包的树形结构(由模块组成的树形结构)的根部:

crate
 └── front_of_house
     ├── hosting
     │   ├── add_to_waitlist
     │   └── seat_at_table
     └── serving
         ├── take_order
         ├── serve_order
         └── take_payment

这颗树展示了模块之间彼此的嵌套关系,因此被称为模块树。其中crate包根是src/lib.rs文件,包根文件中的三个模块分别形成了模块树的剩余部分。

父子模块

如果模块A包含模块B,那么AB的父模块,BA的子模块。在上例中,front_of_househostingserving的父模块,反之,后两者是前者的子模块。

聪明的读者应该能联想到模块树跟计算机上文件系统目录树的相似之处。不仅仅是组织结构上的相似就连使用方式都很相似每个文件都有自己的路径用户可以通过这些路径使用它们在Rust中我们也通过路径的方式来引用模块。

用路径引用模块

想要调用一个函数就需要知道它的路径在Rust中这种路径有两种形式

  • 绝对路径, 从包根开始,路径名以包名或者crate作为开头
  • 相对路径, 从当前模块开始,以self,super或当前模块的标识符作为开头

让我们继续经营那个惨淡的小餐馆,这次为它实现一个小功能: 文件名: src/lib.rs

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

pub fn eat_at_restaurant() {
    // 绝对路径
    crate::front_of_house::hosting::add_to_waitlist();

    // 相对路径
    front_of_house::hosting::add_to_waitlist();
}

上面的代码为了简化实现,省去了其余模块和函数,这样可以把关注点放在函数调用上。eat_at_restaurant是一个定义在包根中的函数,在该函数中使用了两种方式对add_to_waitlist进行调用。

绝对路径引用

因为eat_at_restaurantadd_to_waitlist都定义在一个包中,因此在绝对路径引用时,可以直接以crate开头,然后逐层引用,每一层之间使用::分隔:

crate::front_of_house::hosting::add_to_waitlist();

对比下之前的模块树:

crate
 └── eat_at_restaurant
 └── front_of_house
     ├── hosting
     │   ├── add_to_waitlist
     │   └── seat_at_table
     └── serving
         ├── take_order
         ├── serve_order
         └── take_payment

可以看出,绝对路径的调用,完全符合了模块树的层级递进,非常符合直觉,如果类比文件系统,就跟使用绝对路径调用可执行程序差不多:/front_of_house/hosting/add_to_waitlist, 使用crate作为开始就和使用/作为开始一样。

相对路径引用

再回到模块树中,因为eat_at_restaurantfront_of_house都处于包根crate中,因此相对路径可以使用front_of_house作为开头:

front_of_house::hosting::add_to_waitlist();

如果类比文件系统,那么它类似于调用同一个目录下的程序,你可以这么做: front_of_house/hosting/add_to_waitlist, 嗯也很符合直觉。

绝对还是相对?

如果只是为了引用到指定模块中的对象,那么两种都可以,但是在实际使用时,需要遵循一个原则:当代码被挪动位置时,尽量减少引用路径的修改,相信大家都遇到过,修改了某处代码,导致所有路径都要挨个替换,这显然不是好的路径选择。

回到之前的例子, 如果我们把front_of_house模块和eat_at_restaurant移动到一个模块中customer_experience, 那么绝对路径的引用方式就必须进行修改:crate::customer_experience::front_of_house ..., 但是假设我们使用的相对路径,那么该路径就无需修改,因为它们两的相对位置其实没有变:

crate
 └── customer_experience
    └── eat_at_restaurant
    └── front_of_house
        ├── hosting
        │   ├── add_to_waitlist
        │   └── seat_at_table

从新的模块树中可以很清晰的看出这一点。

再比如,其它的都不动,把eat_at_restaurant移动到模块dining中,如果使用相对路径,你需要修改该路径,但如果使用的是绝对路径,就无需修改:

crate
 └── dining
     └── eat_at_restaurant
 └── front_of_house
     ├── hosting
     │   ├── add_to_waitlist

不过,如果不确定哪个好,你可以考虑优先使用绝对路径,因为调用的地方和定义的地方往往是分离的,而定义的地方较少会变动。

代码可见性

让我们运行下面(之前)的代码:

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

pub fn eat_at_restaurant() {
    // 绝对路径
    crate::front_of_house::hosting::add_to_waitlist();

    // 相对路径
    front_of_house::hosting::add_to_waitlist();
}

意料之外的报错了,毕竟看上去确实很简单且没有任何问题:

error[E0603]: module `hosting` is private
 --> src/lib.rs:9:28
  |
9 |     crate::front_of_house::hosting::add_to_waitlist();
  |                            ^^^^^^^ private module

错误信息很清晰:hosting模块是私有的,无法在包根进行访问,那么为何front_of_house模块就可以访问?因为它和eat_at_restaurant同属于一个包根作用域内,同一个模块内的代码自然不存在私有化问题(所以我们之前章节的代码都没有报过这个错误!)。

模块不仅仅对于组织代码很有用,它还能定义代码的私有化边界:在这个边界内,什么内容能让外界看到,什么内容不能,都有很明确的定义。因此,如果希望让函数或者结构体等类型变成私有化的,可以使用模块。

Rust出于安全的考虑默认情况下所有的类型都是私有化的包括函数、方法、结构体、枚举、常量是的就连模块本身也是私有化的。在中国父亲往往不希望孩子拥有小秘密但是在Rust中父模块完全无法访问子模块中的私有项,但是子模块却可以访问父模块、父父..模块的私有项

pub关键字

类似其它语言的public或者Go语言中的首字母大写Rust提供了pub关键字,通过它你可以控制模块和模块中指定项的可见性。

由于之前的解释,我们知道了只需要将hosting模块标记为对外可见即可:

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

/*--- snip ----*/

但是不幸的是,又报错了:

error[E0603]: function `add_to_waitlist` is private
  --> src/lib.rs:12:30
   |
12 |     front_of_house::hosting::add_to_waitlist();
   |                              ^^^^^^^^^^^^^^^ private function

哦? 难道模块可见还不够,还需要将函数add_to_waitlist标记为可见的吗? 是的,没错,模块可见性不代表模块内部项的可见性,模块的可见性仅仅是允许其它模块去引用它,但是想要引用它内部的项,还得继续将对应的项标记为pub

在实际项目中一个模块需要对外暴露的数据和API往往就寥寥数个如果将模块标记为可见代表着内部项也全部对外可见那你是不是还得把那些不可见的一个一个标记为private? 反而更是麻烦的多。

既然知道了如何解决,那么我们为函数也标记上pub:

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

/*--- snip ----*/

Bang, 顺利通过编译,感觉自己又变强了。

使用super引用模块

用路径引用模块中,我们提到了相对路径有三种方式开始:selfsupercrate或者模块名,其中第三种在前面已经讲到过,现在来看看通过super的方式引用模块项。

super代表的是父模块为开始的引用方式,非常类似于文件系统中的..语法: ../a/b 文件名: src/lib.rs

fn serve_order() {}

// 厨房模块
mod back_of_house {
    fn fix_incorrect_order() {
        cook_order();
        super::serve_order();
    }

    fn cook_order() {}
}

嗯,我们的小餐馆又完善了,终于有厨房了! 看来第一个客人也快可以有了。。。在厨房模块中,使用super::serve_order语法,调用了父模块(包根)中的serve_order函数。

那么你可能会问,为何不使用crate::serve_order的方式?额,其实也可以,不过如果你确定未来这种层级关系不会改变,那么super::serve_order的方式会更稳定,未来就算它们都不在包根了,依然无需修改引用路径。所以路径的选用,往往还是取决于场景,以及未来代码的可能走向。

使用self引用模块

self其实就是引用自身模块中的项,也就是说和我们之前章节的代码类似,都调用同一模块中的内容,区别在于之间章节中直接通过名称调用即可,而self,你得多此一举:

fn serve_order() {
    self::back_of_house
}

mod back_of_house {
    fn fix_incorrect_order() {
        cook_order();
        crate::serve_order();
    }

    fn cook_order() {}
}

是的,多此一举,因为完全可以直接调用back_of_house,但是self还有一个大用处,在下一节中我们会讲。

结构体和枚举的可见性

为何要把结构体和枚举的可见性单独拎出来讲呢?因为这两个家伙的成员字段拥有完全不同的可见性:

  • 将结构体设置为pub,但它的所有字段依然是私有的
  • 将枚举设置为pub,它的所有字段也将对外可见

原因在于,枚举和结构体的使用方式不一样。如果枚举的成员对外不可见,那该枚举将一点用都没有,因此枚举成员的可见性自动跟枚举可见性保持一致,这样可以简化用户的使用。

而结构体的应用场景比较复杂其中的字段也往往部分在A处被使用部分在B处被使用因此无法确定成员的可见性那索性就设置为全部不可见将选择权交给程序员。

模块与文件分离

在之前的例子中,我们所有的模块都定义在src/lib.rs中,但是当模块变多或者变大时,需要将模块放入一个单独的文件中,让代码更好维护。

现在,把front_of_house前厅分离出来,放入一个单独的文件中src/front_of_house.rs:

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

然后,将以下代码留在src/lib.rs中:

mod front_of_house;

pub use crate::front_of_house::hosting;

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

so easy! 其实跟之前在同一个文件中也米有太大的不同,但是有几点值得注意:

  • mod front_of_house;告诉Rust从另一个和模块front_of_house同名的文件中加载该模块的内容
  • 使用绝对路径的方式来引用hosting模块: crate::front_of_house::hosting;

需要注意的是,和之前代码中mod front_of_house{..}的完整模块不同,现在的代码中,模块的声明和实现是分离的,实现是在单独的front_of_house.rs文件中,然后通过mod front_of_house;这条声明语句从该文件中把模块内容加载进来。因此我们可以认为,模块front_of_house的定义还是在src/lib.rs中,只不过模块的具体内容被移动到了src/front_of_house.rs文件中。

在这里出现了一个新的关键字use,联想到其它章节我们见过的标准库引入use std::fmt; 可以大致猜测,该关键字用来将外部模块中的项引入到当前作用域中来,这样无需冗长的父模块前缀即可调用: hosting::add_to_waitlist();,在下节中,我们将对use进行详细的讲解。