Rust 的模块是怎么组织的

楔子

本次来聊一聊 Rust 的模块导入,到目前为止,我们只有一个 main.rs 文件,可以看一下当前的工程目录:

我们的项目叫 RustProj,里面的 src 目录负责存放源代码。在当前命令行中输入 cargo run 并回车的时候,就会执行 src/main.rs 里的 main 函数。但随着功能越来越复杂,我们不可能把代码都写在一个文件中,应该按照功能划分成不同的模块。那么下面就来看看 Rust 的模块是怎么组织的。

手动定义一个模块

在 Rust 中,模块可以来自于文件,也可以单独定义,举个例子:

// 通过 mod 关键字即可定义一个模块
// mod 后面跟模块名,以及模块内容
mod a {
    // rust 模块也有一个可见性的问题,所有条目默认都是私有的
    // 像函数、方法、struct、enum、模块、常量等等,都是私有的
    // 想变成公有,需要在开头加上 pub 关键字
    // 只不过结构体特殊,结构体开头加 pub 只表示结构体是公有的
    // 但字段还是私有的,如果希望字段也公有,那么还要在每个字段前面加上 pub
    pub fn mod_a_f1() {
        println!("我是模块 a 下的函数 f1")
    }

    // 在模块内部还可以再定义模块
    pub mod b {
        pub fn mod_b_f2() {
            println!("我是模块 a/b 下的函数 f2")
        }
    }
}

fn main() {
    a::mod_a_f1();
    a::b::mod_b_f2();
    /*
    我是模块 a 下的函数 f1
    我是模块 a/b 下的函数 f2
    */
}

通过模块名可以调用模块内部的条目,访问方式是通过 ::。但是要注意条目的可见性,默认都是私有的,如果想通过模块名访问,那么一定要在条目的前面加上 pub 关键字。比如里面的模块 b,必须在 mod b 的前面加上 pub,a::b 才能够成立。

那么可能有人好奇了,为啥 mod a 的前面没有 pub呢?原因是模块 a 位于最外层,和调用方处于同一级,因此不需要 pub。

另外在 Rust 里面还有一个 crate 的概念,crate 是 Rust 最小的编译单元,在一个范围内将多个文件里面的功能组合在一起,最终通过编译生成一个二进制文件或库文件。所以 crate 是项目中多个文件的组合,整体形成一棵树,并且其中一个是入口文件、也就是它的根,对于当前来说显然是 main.rs。

如果你对 crate 还感到有些困惑,那么这里给出一个虽然不准确、但是方便记忆的方式,你可以把 main.rs 所在的 src 目录理解为 crate。因此我们还可以这么调用:

fn main() {
    crate::a::mod_a_f1();
    crate::a::b::mod_b_f2();
    /*
    我是模块 a 下的函数 f1
    我是模块 a/b 下的函数 f2
    */
}

通过 crate 即可从指定文件里面查找指定条目,并且这种方式相当于使用绝对路径进行查找,因此它永远都是成立的。另外可能有人发现了,这里 crate 后面没有指定文件啊,因为 main.rs 是入口文件,如果不指定文件的话,那么查找的默认是 main.rs 里的条目。

然后 mod 内部的条目之间,也可以相互调用,举个例子:

mod a {
    pub fn mod_a_f1() {
        b::mod_b_f2()
    }

    // 注意:mod b 不是公有的
    mod b {
        pub fn mod_b_f2() {
            println!("我是模块 a/b 下的函数 f2")
        }
    }
}

fn main() {
    a::mod_a_f1();
    /*
    我是模块 a/b 下的函数 f2
    */

    // a::b::mod_b_f2(); // 不合法
}

我们在函数 mod_a_f1 的内部调用模块 b 的一个函数,显然整个过程不需要解释,但模块 b 不是公有的为啥也能访问呢?很简单,因为函数 mod_a_f1 和模块 b 是在同一级,所以可以直接拿到模块 b,因此模块 b 可以不是公有的,但它内部的函数必须公有。而 main 函数和模块 b 显然就不是同一级了,它们之间有一个屏障,也就是模块 a。但模块 b 在模块 a 里面不是公有的,因此在 main 函数里面无法通过 a::b 的方式获取。

然后上面是在父模块内部调用子模块的条目,因此条目必须公有;但如果是子模块调用父模块的条目,那么条目是否公有就都无所谓了。

mod a {
    // 函数是私有的
    fn mod_a_f1() {
        println!("我是模块 a 下的函数 f1")
    }

    pub mod b {
        pub fn mod_b_f2() {
            println!("我是模块 a/b 下的函数 f2");
            // 想在这里调用 mod_a_f1 要怎么做呢?
            // 首先要找到模块 a,但它和 a 不在同一级
            // 因此无法使用 a::mod_a_f1() 的方式
            crate::a::mod_a_f1();
            // 需要使用绝对路径,从 crate 开始定位
        }
        pub mod c {
            pub fn mod_c_f3() {
                println!("我是模块 a/b/c 下的函数 f3");
                // 它是一个嵌套在模块 b 里面的模块
                // 如果也要调用 mod_a_f1,显然方法和上面相同
                // 但除此之外还有一种方式,就是使用 super
                super::super::mod_a_f1();
                // super 表示获取当前所在模块的上一级模块
                // 一个 super 获取到的显然是模块 b
                // 两个 super 获取到的就是模块 a 了
            }
        }
    }
}

fn main() {
    // 首先 main 函数里的 crate::a::mod_a_f1() 是不合法的;
    // 因为 mod_a_f1 在模块 a 的内部是不可见的

    // 但 mod_b_f2 里面也是 crate::a::mod_a_f1() 啊,为啥它就合法呢
    // 因为子 mod 中的条目如果私有,对于父 mod 是不可见的
    // 但父 mod 中的条目无论公有还是私有,子 mod 都是可见的
    // 所以模块 a 的 mod_a_f1,对于在 crate 里面的 main 函数来说不可见
    // 但对于在模块 b 里面的 mod_b_f2 来说可见
    // 因此调用方式是一样的,唯一的区别就是调用位置所导致的可见性问题
    a::b::mod_b_f2();
    /*
    我是模块 a/b 下的函数 f2
    我是模块 a 下的函数 f1
    */

    a::b::c::mod_c_f3();
    /*
    我是模块 a/b/c 下的函数 f3
    我是模块 a 下的函数 f1
    */
}

还是很好理解的,需要注意的是里面的 super。从父模块找子模块的话,直接一级一级往下找即可,但子模块找父模块则需要从 crate 开始,有时会比较麻烦。为此 Rust 提供了 super,用于定位上一级模块。

然后我们再来看一个好玩的:

mod a {
    pub mod b {
        pub mod c {
            pub fn mod_c_f3() {
                println!("我是模块 a/b/c 下的函数 f3");
                // super 表示上一级模块
                // super::super 显然就是上上一级,也就是模块 a
                // 那么 super::super::super 表示啥呢?显然是 crate
                // 而通过 crate 即可找到 main 函数和模块 a
                super::super::super::main();
            }
        }
    }
}

fn main() {
    println!("main 函数被调用");
    a::b::c::mod_c_f3();
}

你觉得这段代码执行的时候会发生什么现象呢?我们试一下。

我们看到因为无限递归导致栈溢出了,相信你应该明白模块之间的关系了,多个 rs 文件整体组成一个 crate,基于 crate 可以获取每一个文件的条目。此方法相当于使用绝对路径定位,因此无论在什么情况下它都是可靠的。但如果模块嵌套的比较深,那么通过 crate 一级一级查找就有点麻烦了,比如我们要获取相邻模块(比如上一级)内部的条目,这种情况下 Rust 推荐使用 super。

另外上面使用三个 super 找到了 crate,如果是四个 super 呢?显然会报错,因为 crate 已经是最顶层了。

然后 Rust 还提供了一个 use 关键字,可以将指定的条目引入当前作用域,用于简化模块查找过程。

mod a {
    pub mod b {
        pub mod c {
            pub fn mod_c_f3() {
                println!("我是模块 a/b/c 下的函数 f3");
            }
        }
    }
}

fn main() {
    // 引入指定模块,这里通过绝对路径
    use crate::a::b;
    // 然后便可以通过 b 来进行查找
    b::c::mod_c_f3();  //我是模块 a/b/c 下的函数 f3

    // 引入模块,通过相对路径
    use a::b::c;
    c::mod_c_f3();  //我是模块 a/b/c 下的函数 f3

    // 还可以导入到某一个具体的函数
    use a::b::c::mod_c_f3;
    mod_c_f3();  //我是模块 a/b/c 下的函数 f3
}

所以当模块层级比较多的时候,我们还可以使用 use 将指定的模块单独导入进来,这样在使用的时候就没必要从最外层开始找了。当然啦,我们在导入的时候还可以起别名。

fn main() {
    use a::b::c as cc;
    cc::mod_c_f3();

    use a::b::c::mod_c_f3 as mod_c_f33333;
    mod_c_f33333();  
}

结果是一样的,总之在导入模块的时候,可以通过 as 起别名。

基于 .rs 文件定义模块

我们上面是通过 mod 关键字手动定义一个模块,但现在 src 目录下又多了一个 girl.rs 文件,里面内容如下:

然后我们在 main.rs 里面要如何导入它呢?首先这段代码是位于 girl.rs 里面,如果想在 main.rs 里面使用的话,那么条目一定要定义为公有,所以 mod a 的前面一定要有 pub。

下面来看看如何在 main.rs 里面导入它。

// 我们之前在 main.rs 里面也是使用 mod 关键字
// 而现在的代码放在了一个单独的文件里面
// 因此 mod xxx; 表示将 xxx.rs 导入进来成为一个模块
// 而 mod xxx {} 则表示自己手动定义一个名为 xxx 的模块
mod girl;

// 模块有了,我们也可以使用 use 关键字,但注意上面的 mod girl 一定不可以省略
// 我们之前可以直接使用 use,是因为模块是通过 mod 手动定义的
// 相当于模块就在 main.rs 里面,而现在的代码是在 girl.rs 里面
// 需要先通过 mod girl 将其变成一个模块,即 mod girl {girl.rs 里的内容}
// 然后才可以使用 use,不然 Rust 根本不知道 girl 是什么
use girl::a::b::c::Girl;
// 然后 a、b、c 也是模块,它们之间一层一层嵌套

fn main() {
    // 可以一层一层导入
    let g1 = girl::a::b::c::Girl{
        name: String::from("古明地觉"),
        age: 16, length: 156
    };

    // 因为模块 girl 和当前的 main 函数在同一级
    // 因此在 main 函数里面可以直接找到模块 girl
    // 当然我们也可以使用 crate,也就是基于绝对路径查找
    let g2 = crate::girl::a::b::c::Girl{
        name: String::from("古明地恋"),
        age: 15, length: 155
    };

    // 上面已经使用 use 直接将 Girl 导入进来了
    // 所以也可以直接用
    let g3 = Girl{
        name: String::from("雾雨魔理沙"),
        age: 19, length: 163
    };

    println!("{:?}", g1);
    println!("{:?}", g2);
    println!("{:?}", g3);
    /*
    Girl { name: "古明地觉", age: 16, length: 156 }
    Girl { name: "古明地恋", age: 15, length: 155 }
    Girl { name: "雾雨魔理沙", age: 19, length: 163 }
    */
}

过程不难理解,核心就是使用 mod 关键字,mod 不仅可以用来定义模块,也可以将一个文件变成模块。

  • mod xxx; 表示导入 xxx.rs,其中模块名为 xxx。
  • mod xxx {} 表示手动定义模块 xxx,至于模块的内容则写在大括号里面。

当然不管哪一种,它们得到的都是模块,然后我们在 src 目录里面再定义一个 boy.rs。

pub struct Boy {
    pub name: String,
    pub age: u8,
}

pub mod a {
    // crate 可以看作是 main.rs 所在的 src 目录
    // 基于 crate 能获取任何一个文件里的条目,类似使用绝对路径查找一样
    // 但注意这里 crate 的后面需要指定文件名
    // 如果是 crate::Boy 则表示导入 main.rs 里的 Boy
    use crate::boy::Boy;
    // impl 前面不需要加 pub,因为外界调用的是里面的方法
    // 所以只需要控制方法是否私有即可
    impl Boy {
        pub fn info(&self) -> String {
            return format!("name = {}, age = {}", self.name, self.age);
        }
    }
    // 我们还可以通过 impl crate::boy::Boy {} 定义方法
    // 或者通过 impl super::Boy {} 也行,只要能找到即可
    // 但很明显这样做代码有些长,特别是当 Boy 要反复被使用时
    // 因此建议先通过 use crate::boy::Boy; 导入,然后直接使用 Boy 即可
}

然后在 main.rs 里面导入它:

mod boy;

fn main() {
    let b1 = boy::Boy {
        name: "sorata".to_string(),
        age: 17,
    };
    // 也可以先通过 use 将 Boy 导入进来,然后直接使用 Boy
    use boy::Boy;
    let b2 = Boy {
        name: "sorata".to_string(),
        age: 17,
    };
    println!("{}", b1.info());
    println!("{}", b2.info());
    /*
    name = sorata, age = 17
    name = sorata, age = 17
    */
}

过程不难理解,但需要注意为 Boy 结构体实例实现的 info 方法,它必须是公有的,否则外界无法调用。因为结构体 Boy 的定义和 info 方法的定义,不在同一个模块中,如果两者在同一个模块,那么是否公有就无所谓了。

包的导入

通过 mod 可以加载一个文件,那如果是目录呢?这里我们在 src 目录中新创建一个 people 目录,里面有两个文件:girl.rs 和 boy.rs。

girl.rs 和 boy.rs 里面的内容分别如下:

// boy.rs
pub mod a {
    pub mod b {
        pub mod c {
            # [derive(Debug)]
            pub struct Boy {
                pub name: String,
                pub age: u8,
            }
        }
    }
}

// girl.rs
pub mod a {
    pub mod b {
        pub mod c {
            # [derive(Debug)]
            pub struct Girl {
                pub name: String,
                pub age: u8,
                pub length: u8,
            }
        }
    }
}

girl.rs 和 boy.rs 里面各自定义了一个结构体,当然为了更好地理解 Rust 的模块,这里故意将代码变得复杂一些,专门搞了几个模块。然后 main.rs 要如何加载呢?

首先,如果一个目录想要成为一个 Rust 可以导入的包,那么里面必须要有一个 mod.rs 文件。这个 mod.rs 里面有什么,通过包名便可以导入什么。所以 Rust 的 mod.rs 就类似于 Python 里的 __init__.py。

Rust 一个包指向了里面的 mod.rs,所以我们在 people 目录再新建一个 mod.rs,内容如下:

// 导入 boy 和 girl,并且要声明为 pub,否则这两个模块无法被外界使用
// 之前之所以不用 pub,是因为模块直接导入到 main.rs 里面了
pub mod boy;
pub mod girl;

// 也可以使用 use 关键字,将某一个具体的条目引入到当前作用域
// 但还是那句话,使用 use 之前,必须先通过 mod 将模块导入进来
pub use boy::a::b::c::Boy;

由于有了 mod.rs,那么它所在的 people 目录就成为了一个 Rust 可以识别并导入的包。然后通过 people 来使用内部的条目,而这些条目都来自于 mod.rs,我们测试一下。

// 导入 people
mod people;

fn main() {
    // 通过 people 一层层往下找
    let b1 = people::boy::a::b::c::Boy{
        name: String::from("sarata"), age: 17};
    // 由于 Boy 在 mod.rs 中已经引入过来了
    // 那么通过 people 也可以直接使用
    let b2 = people::Boy{
        name: String::from("sarata"), age: 17};
    println!("{:?}", b1);
    println!("{:?}", b2);
    /*
    Boy { name: "sarata", age: 17 }
    Boy { name: "sarata", age: 17 }
    */

    // Girl 也是同理
    let g1 = people::girl::a::b::c::Girl{
        name: String::from("satori"), age: 16, length: 156
    };
    println!("{:?}", g1); 
    /*
    Girl { name: "satori", age: 16, length: 156 }
    */
    
    // 但是 Girl 在 mod.rs 没有被引入进来,所以我们不能直接通过 people.Girl 来获取
    // 需要单独 use,然后通过 Girl 来获取,因此这就要求 girl 在 mod.rs 必须通过 pub mod girl 定义模块
    use people::girl::a::b::c::Girl;
}

当然,我们在 mod.rs 里面也可以单独定义一些逻辑,只要是 mod.rs 里面有的,通过包名都可以导入。只不过业务逻辑应该写在其它文件里面,然后在 mod.rs 里面再导过来。

这里再补充一点,因为 mod.rs 和 main.rs 是隔离的,所以 mod.rs 里面导入的条目如果想被 main.rs 导入,那么一定要使用 pub 声明为公有的。比如 pub mod boy,如果没有这个 pub,那么在 main.rs 里面就无法通过 crate::people 导入 boy。但在 mod.rs 里面 pub use 了 boy.rs 里面 Boy 结构体,所以即使 boy 不是公有的,依旧可以通过 crate::people::Boy 来使用 Boy 结构体。

因此如果一个模块里面有很多条目,你不希望全部暴露出去,那么就在 mod.rs 里面以私有的方式导入模块。并同时将那么希望被 main.rs 访问的条目,使用 pub use 单独导出即可。

然后再来看看在包的内部,文件之间要如何互相导入呢。我们在 people 目录下面再定义一个 method.rs,用于为 Boy 和 Girl 实现方法。

use crate::people::boy::a::b::c::Boy;
use crate::people::girl::a::b::c::Girl;

impl Boy {
    pub fn info(&self) -> String {
        format!("boy's name: {}, boy's age: {}", 
                self.name, self.age)
    }
}

impl Girl {
    pub fn info(&self) -> String {
        format!(
            "girl's name: {}, girl's age: {}, girl's.length: {}", 
            self.name, self.age, self.length)
    }
}

光定义这个文件还不够,我们还要在 mod.rs 里面导入进来,否则该文件就白创建了,因为不可能被执行。

pub mod boy;
pub mod girl;

// 通过引入 method,保证 method.rs 会被执行
// 但没必要定义为公有,因为 main.rs 不需要使用它
// 我们只要保证 method.rs 里面内容会被执行即可
mod method;

然后是 main.rs:

mod people;

fn main() {
    let b = people::boy::a::b::c::Boy{
        name: String::from("sarata"), age: 17};
    let g = people::girl::a::b::c::Girl{
        name: String::from("sarata"), age: 17, length: 156};
    
    println!("{}", b.info());
    println!("{}", g.info());
    /*
    boy's name: sarata, boy's age: 17
    girl's name: sarata, girl's age: 17, girl's.length: 156
    */
}

以上我们就为两个结构体实现了方法。

那么问题来了,如果导入的条目比较多,难道每个都要写一行吗?比如:

pub use crate::xxx::a;
pub use crate::xxx::b;
pub use crate::xxx::c;
pub use crate::xxx::d;

这么写无疑会有些麻烦,为此我们可以换一种方式:

// self 指的是 xxx 本身
// 如果除了 a、b、c、d 还希望导入 xxx 本身,那么就用 self
pub use crate::xxx::{self, a, b, c, d}

以上就是 Rust 的模块导入,特别是模块条目的可见性问题,需要我们注意。比如某个结构体的字段你不希望被修改,但其它字段可以,那么你就将不想被修改的字段声明为私有(不使用 pub)即可。如果在不被修改的前提下,希望访问是可以的,那么就再单独定义一个方法,返回该字段的值。

posted @ 2023-10-14 23:00  古明地盆  阅读(369)  评论(0编辑  收藏  举报