Rust 入门 模块 (二十二)

模块Module

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

创建嵌套模块

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

rust 复制代码
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() {}
    }
}

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

1.使用mod 关键字来创建新模块,后面紧跟着模块名称

2.模块可以嵌套; 这里的嵌套的原因是招待客人和服务都发生在前厅,因此我们的代码模拟来真实场景

3.模块中可以定义各种Rust 类型, 例如函数,结构体,枚举,特征等

4.所有模块均定义在同一个文件中

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

模块树

在上一节中,我们提到过 src/main.rs 和 src/lib.rs 被成为包根(crate root) , 这个奇葩名称的来源是由于这两个文件的内容形成来一个模块crate ,该模块位于包的树形结构的根部:

crate

└── front_of_house

├── hosting

│ ├── add_to_waitlist

│ └── seat_at_table

└── serving

├── take_order

├── serve_order

└── take_payment

这颗树展示来模块自建彼此的嵌套关系,因此被成为模块树。其中create 包根是src/lib.rs 文件,包根文件中的三个模块分别星辰来模块树的剩余部分。

父子模块

如果模块 A 包含模块 B ,那么 A 是 B 的父模块, B 是 A 的子模块。 在上例中, front_of_house 是hosting 和 serving 的父模块,反之,后两者是前者的子模块,

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

用路径引用模块

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

1.绝对路径,从包根开始,路径名以包名或者crate 作为开头

2.相对路径, 从当前模块开始, 以self ,super 或当前某哎的标识符作为开头

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

rust 复制代码
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_restaurant 和 add_to_waitlist 都定义在一个包中, 因此在绝对路径引用时,可以直接以create 开头,然后逐层引用,每一层自建使用:: 分隔:

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_restaurant 和 front_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

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

代码可见性

运行下面的代码(之前)

rust 复制代码
mod front_of_house {
    mod hosting {
        fn add_to_waiflist() {}
    }
}

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

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

运行 cargo build 编译此库类型的Package ,意料之外的报错, 毕竟看上去确实很简单且没有任何问题:

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 模块标记为对外可见即可

rust 复制代码
mod front_of_house {
    pub mod hosting {
        fn add_to_waitlist(){}
    }
}

但是不幸的是,又报错了

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 :

rust 复制代码
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist(){}
    }
}

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

使用super引用模块

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

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

src/lib.rs

rust 复制代码
fn  serve_order(){}

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

    fn cook_order(){}
}    

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

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

使用self 引用模块

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

rust 复制代码
fn serve_order(){
    self::back_of_house::cook_order()
}

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

    pub fn cook_order() {}
}

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

结构体和枚举的可见性

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

1.将结构体设置为 pub ,但它的所有字段依然是是有的。

2.将枚举设置为 pub ,它的所有字段也将对外可见

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

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

模块与文件分离

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

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

rust 复制代码
pub mod hosting {
    pub fn  add_to_waitlist() {}
}

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

rust 复制代码
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! 其实跟之前在同一个文件中也没有太大的不同, 但是有几点值得注意:

1.mod front_of_house; 告诉Rust 从另一个和模块 front_of_house 同名的文件中加载该模块的内容

2.使用绝对路径的方式来引用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 进行详细讲解

当一个模块有许多子模块时,我们也可以通过文件夹的方式来组织这些子模块。

在上述例子中,我们可以创建一个目录 front_of_house ,然后在文件夹里创建一个 hosting.rs 文件。hosting.rs 文件现在就剩下:

rust 复制代码
pub fn add_to_waitlist() {}

现在,我们尝试编译程序,很遗憾,编译器报错:

error[E0583]: file not found for module `front_of_house`

--> src/lib.rs:3:1

|

1 | mod front_of_house;

| ^^^^^^^^^^^^^^^^^^

|

= help: to create the module `front_of_house`, create file "src/front_of_house.rs" or "src/front_of_house/mod.rs"

是的,如果需要将文件夹作为一个模块,我们需要进行显示指定暴露哪些子模块,按照上述的报错信息,我们有两种方法:

1.在front_of_house 目录里创建一个 mod.rs ,如果你使用的rustc 版本在1.30之前,这是唯一的方法

2.在 front_of_house 同级目录里创建一个与模块(目录)同名的rs 文件, front_of_house.rs ,在新版本里,更建议使用这样的命名方式来避免项目中存在大量同名的mod.rs文件

如果使用第二种方式,文件结构将如下所示

src

├── front_of_house

│ └── hosting.rs

├── front_of_house.rs

└── lib.rs

而无论是上述哪个方式创建的文件,内容都是一样的,你需要在定义你(mod.rs 或 front_of_house.rs) 的子模块(子模块名与文件名相同);

rust 复制代码
pub mod hosting;

使用 use 及受限可见性

如果代码中,通篇都是 crate::front_of_house::hosting::add_to_waitlist , 这样的函数调用形式,我不知道有谁会喜欢,也许靠代码行数赚工资的人会很喜欢,但是强迫症肯定受不了,悲伤的是程序员太多都有强迫症。。

因此我们需要一个办法来简化这种使用方式,在Rust 中,可以使用use 关键字把路径提前引入到当前作用域中,随后的调用就可以省略该路径,极大地简化来代码。

基本引入方式

在Rust中,引入模块中的项有两种方式,:绝对路径和相对路径,这两者在前面章节都有讲过,就不再赘述,先来看看使用绝对路径的引入方式

绝对路径引入模块

rust 复制代码
mod front_of_house {
    pub mod hosting{
        pub fn add_to_waitlist(){}
    }
}

usee crate::front_of_house::hosting;

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

这里,我们使用 use 和绝对路径的方式,将hosting 模块引入到当前作用域中,然后只需通过hosting::add_to_waitlist 的方式,即可调用目标模块中的函数,相比,crate::front_of_house::hosting::add_to_waitlist() 的方式要简单的多,那么还能更简单吗?

相对路径引入模块中的函数

在下面的代码中,我们不仅要使用相对路径进行引入,而且与上面引入hosting模块不同,直接引入该模块中的 add_to_waitlist函数:

rust 复制代码
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

use front_of_house::hosting::add_to_waitlist;

pub fn set_at_restaurant() {
    add_to_waitlist();
}

很明显,调用路径更短了。

引入模块还是函数

从使用简洁性来说,引入函数自然是更甚一筹,但是在某些时候,引入模块会更好:

  1. 需要引入同一个模块的多个函数

2.作用域中存在同名函数

在以上情况中,使用use front_of_house::hosting; 引入模块要比 use front_of_house::hosting::add_to_waitlist; 引入函数更好,

例如,如果想使用HashMap ,那么直接引入该结构体,是比引入模块更好的选择,因为在 collections 模块中,我们只需要使用一个hashmap 结构体:

rust 复制代码
use std::collections::HashMap;

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

其实严格来说。对于引用方式并没有需要遵守的管理,主要赛事取决于你的喜好,不过我们建议,有限使用最细粒度(引入函数、结构体等) 的引用方式,如果引起来某种麻烦(例如前面两种情况),在使用引入模块的方式

避免同名引用

根据上一章节的你日欧那个,我们只要保证同一个模块中不存在同名项就行,模块自建,包之间的同名,谁管得着谁?,一起看下,如果遇到同名的情况该如何处理。

模块::函数

rust 复制代码
use std::fmt;
use std::io;

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

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

上面的例子给出了很好的解决方案,使用模块引入的方式,具体的Rust 通过 模块::Result的方式进行调用

可以看出,避免同名冲突的关键,就是使用父模块的方式来调用,除此值往外,还可以给予引入的项起一个别名

as 别名引用

对于同名冲突玩儿逆天,还可以使用as关键字来决定,它可以赋予引入项一个全新的名称;

rust 复制代码
use std::fmt::Result;
use std::io::Result as  IoResult;

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

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

如上所示,首先通过 use std::io::Result 将Result 引入到作用域,然后使用as 给予它一个全新的名称 IoResult, 这样就不会再产生冲突:

Result 代表 std::fmt::Result;

IoResult 代表std::io::Result

引入项再导出

当外部的模块项A 被引入到当前模块中是,它的可见性自动被设置为是有的,如果你希望允许其它外部代码引用我们的模块项 A ,那么可以对它进行在导出:

rust 复制代码
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();
}

如上,使用 pub use 即可实现,这里 use 代表引入 hosting 模块到当前作用域,pub 表示将该引入的内容再度设置为可见。

当你希望将内部的实现细节隐藏起来,或者按照某个目的组织代码时,可以使用 pub use 在导出,例如统一使用一个模块来提供对外的api , 那该模块就可以引入其它模块中的api ,然后在进行导出,最终对于用户来说,所有的api 都是由一个模块统一提供的。

使用第三方包

之前我们一直在引入标注库模块或者自定义模块,现在来引入下第三方包中的模块,关于如何引入外部以来,我们在Cargo 入门就有将,这里直接给出操作步骤

1.修改 Cargo.toml 文件, 在[dependencies] 区域添加一行: rand = "0.8.3"

  1. 此时,如果你用的是VSCode 和 rust-analyzer 插件,该插件会自动拉取该库,你可能需要等它完成后,在进行下一步(VSCode 左下角有提示)

好了, 此时,rand 包已经被我们添加到依赖中,下一步就是在代码中使用

rust 复制代码
use rand::Rng;

fn main() {
    let secret_number = rand::thread_rng().gen_range(1,101);
}

这里使用 use 引入了第三方包 rand 中的Rng 特征,因为我们需要调用的 gen_range 方法定义在该特征中。

crates.io,lib.rs

Rust社区已经为我们贡献了大量高质量的第三方包,你可以在crates.io 或者 lib.rs 中检索和使用,从目前来说查找包更推荐 lib.rs ,搜索功能更强大,内容展示也更加合理, 但是下载以来包还是得用crates.io.

你可以在网站上搜索 rand 包, 看看它的文档使用方式,是否和我们之前引入方式相一致: 在网上找到想要的包,然后将你想要的包和版本信息写入到cargo.toml 中。

使用 {} 简化引入方式

对于以下一行一行的引入方式:

rust 复制代码
use std::collections::HashMap;
use std::collections::BTreeMap;
use std::collections::HashSet;

use std::cmp::Ordering;
use std::io;

可以使用 {} 来一起引入进来,在大型项目中,使用这种方式来引入,可以减少大量 use 的使用

rust 复制代码
use std::collections::{HashMap,BTreeMap,HashSet};
use std::{cmp::Ordering,io};

对于限免的同时引入模块和模块中的项:

rust 复制代码
use std::io;
use std::io::Write;

可以使用 {} 的方式进行简化

rust 复制代码
use std::io::{self,Write};

self

上面使用到了模块章节提到的self 关键字,用来替代模块自身,结合上一节中的self ,可以得出它在模块中的两个用途:

use self::XXX; 表示加载当前模块中的 xxx, 此时self 可省略

use xxx::{self,yyy} ,表示,加载当前路径下模块 xxx 本身,以及模块 xxx 下的yyy

使用 * 引入模块下的所有项

对于之前一行一行引入 std::collections 的方式,我们还可以使用

rust 复制代码
use std::collections::*;

以上这种方式来引入 std::collections 模块下的所有公共项,这些公共项自然包含了 HashMap ,HashSet 等想手动引入的集合类型。

当使用 * 来引入的时候要格外小型,因为你很男子到到底那些被引入到来当前作用域中,有哪些会和你自己程序中的名称相冲突

rust 复制代码
use std::collections::*;

struct HashMap;

fn main() {
    let mut v = HashMap::new();
    v.insert("a",1);
}

以上代码中, std::collections::HashMap 被 * 引入到当前作用域,但是由于存在另一个同名的结构体,因此HashMap::new 根本不存在, 因为对于编译器来说,本地同名类型的优先级更高。

在实际项目中, 这种引用方式往往用于快速写测试代码,它可以把所有东西依次性引入到tests模块中。

受限的可见性

在上一节中,我们学习来可见性这个概念,这也是模块体系中最为核心的概念,控制来模块中哪些内容可以被外部看见,但是在实际使用时,光被外面看到还不行,我们还想控制哪些人能看,这就是Rust提供的受限可见性。

例如,在Rust中,包是一个模块树,我们可以通过 pub(crate) item; 这种方式来实现: item 虽然是对外可见的,但是只在当前包内可见,外部包无法引用到该 item 。

所以,如果我们想要让某一项可以在整个包中都可以被使用, 那么有两种办法:

在包根中定义一个非 pub 类型的 x ( 父模块的项对子模块都是可见的,因此包根中的项对模块树上的所有模块都可见)

在子模块中定义一个pub 类型 的y ,同时通过use 将其引入到包根

rust 复制代码
mod a {
    pub mod b {
        pub fn c(){
            pringln!("{:?}",crate::X);
        }

        #[derive(Debug)]
        pub  struct Y;
    }
    
}

#[derive(Debug)]
struct X;

use a::b::Y;

fn d() {
    println!("{:?}",Y);
}

以上代码充分说明了之前两种办法的使用方式,但是有时我们会遇到这两种方法都不太好用的时候,例如希望对于某些特定的模块可见,但是对于其它模块又不可见

rust 复制代码
pub mod a {
    pub const I: i32 = 3;

    fn semisecret(x: i32) -> i32 {
        use self::b::c::I;
        x + J
    }

    pub fn bar(z:i32) -> i32 {
        semisecret(I) * z
    }

    pub fn foo(y: i32) -> i32 {
        semisecret(I) + y
    }
    
    mod b {
        mod c {
            const J: i32 =4;
        }
    }    
}  // 以上代码完全看不懂, 一会i 一会 J 一会z 一会y  😫

这段代码会报错,因为与父模块中的项对子模块可见相反,子模块中的项对父模块是不可见的,这里semisecret 方法中, a -> b -> c 形成来父子模块链,那c 中的J 自然对 a 模块不可见。

如果使用之前的可见性方式,那么想保持 J 是有,同时让a 继续使用 semisecret 函数的办法是将该函数移动到 c 模块中,然后使用 pub use 将semisecret 函数进行再导出:

rust 复制代码
pub mod a {
    pub const I: i32 = 3;
    
    use self::b::semisecret;
    
    pub fn bar(z:i32) -> i32 {
        semisecret(I) * z
    }
    
    pub fn foo(y: i32) -> i32 {
        semisecret(I) + y 
    }

    mod b {
        pub use self::c::semisecret;
        mod c {
            const J:i32 =4;
            pub fn semisecret(x:i32) -> i32 {
                x + J
            }
        }
    }
}

这段代码说实话问题不大,但是有些破坏来我么年之前的逻辑,如果项保持代码逻辑,同时又让J在 a 内可见该怎么办?

rust 复制代码
pub mod a {
    pub const I: i32 = 3;
    
    fn semisecret(x:i32) -> i32 {
        use self::b::c::J;
        x + J
    }

    pub fn bar(z:i32) -> i32 {
        semisecret(I) * z 
    }

    pub fn foo(y: i32) -> i32{
        semisecret(I) + y
    }

    mod b {
        pub (in crate::a) mod c {
            pub (in crate::a) const J:i32 = 4; 
        }
    }
}

通过 pub(in crate::a) 的方式,我们指定了模块 c 和常量 J 的可见范围都是a 模块中, a 之外的模块是完全访问不到他们的。

限制可见性语法

pub (crate) 或 pub(in crate::a) 就是限制可见性语法, 前者是限制在整个包内可见, 后者是通过绝对路径,限制在包内的某个模块可见。总结一下。

pub 意味着可见性无任何限制

pub(crate) 表示在当前包可见

pub(self) 在当前模块可见

pub(super) 在父模块可见

pub (in <path>) 表示在某个路径代表的模块中可见,其中path 必须是父模块或者祖先模块

一个综合例子

rust 复制代码
// 一个名为 my_mod 的模块
mod my_mod {
    // 模块中的项 默认具有是有的可见性
    fn  private_function() {
        println!("called my_mod::private_function()");
    }

    // 使用 pub 修饰语 来改变默认可见性
    pub fn function() {
        println!("called my_mod::function()");
    }

    // 在同一模块中,项可以访问其它项,即使它是私有的。
    pub fn indirect_access() {
        print!("called my_mod::indirect_access(), that\n>");

        private_function();
    }

    // 模块可以嵌套
    pub mod nested {
        pub fn function() {
            println!("called my_mod::nested::function()");
        }
        
        #[allow(dead_code)]
        fn private_function() {
            println!("called my_mod::nested::private_function()");
        }

        // 使用 pub(in path) 语法定义的函数只在给定的路径中可见。
        // path 必须是父模块 (parent module) 或 祖先模块 (ancestor module)
        pub(in crate::my_mod) fn public_function_in_my_mod() {
            print!("called my_mod::nested::public_function_in_my_mod(),that \n>");
            public_function_in_nested()
        }

        // 使用 pub(self) 语法定义的函数则只在当前模块中可见。
        pub(self) fn public_function_in_nested(){
            printlin!("called my_mod::nested::public_function_unnested");
        }

        // 使用 pub(super) 语法定义的函数只在父模块中可见。
        pub(super) fn public_function_in_super_mod(){
            println!("called my_mod::nested::public_function_in_super_mod");
        }
    }

    pub fn call_public_function_in_my_mod(){
        print!("called my_mod::call_public_function_in_my_mod(),that \n> ");
        nested::public_function_in_my_mod();
        print!(">");
        nested::public_function_in_super_mod();
    }
    
    // pub(crate) 使得函数只在当前包中可见
    pub(crate) fn public_function_in_crate() {
        println!("called my_mod::public_function_in_crate()");
    }
    
    // 嵌套模块的可见性 遵循相同的规则
    mod private_nested {
        #[allow(dead_code)]
        pub fn function() {
            println!("called my_mod::private_nested::function()");
        }
    }
}

fn function() {
    println!("called function()");
}

fn main() {
        // 模块极致消除来相同名字的项 之间的歧义
    function();
    my_mod::function();

    // 公有项,包括嵌套模块内的,都可以在父模块外部访问。
    my_mod::indirect_access();
    my_mod::nested::function();
    my_mod::call_public_function_in_my_mod();

    // pub(crate) 项 可以在同一个 crate 中的任何地方访问
    my_mod::public_function_in_crate();

    // pub(in path) 项只能在指定的模块中访问
    // 报错! 函数 public_function_in_my_mod 是是有的
    // my_mod::nested::public_function_in_my_mod();
    // 试一试 ^ 取消改行的注解。

    // 模块的私有项 不能直接访问 ,即便它是嵌套在公有模块内部的

    // 报错! private_function 是是有的
    // my_mod::private_functin();
    
    // 报错! private_function 是是有的
    // my_mod::nested::private_function();
    
    // 报错 private_nested 是是有的
    // my_mod::private_nested::function();

}