rust学习笔记之基础:crate和模块

crate 和 package

crate

crate 是 Rust 的编译单元。当调用 rustc some_file.rs 时,some_file.rs 被当作 crate 文件。如果 some_file.rs 里面含有 mod 声明,那么模块文件的内容将在编译之前被插入 crate 文件的相应声明处。换句话说,模块不会单独被编译,只有 crate 才会被编译。

crate 可以编译成二进制可执行文件(binary)或库文件(library)。默认情况 下,rustc 将从 crate 产生二进制可执行文件。这种行为可以通过 rustc 的选项 --crate-type 重载。

pub fn public_function() {
    println!("called rary's `public_function()`");
}

fn private_function() {
    println!("called rary's `private_function()`");
}

pub fn indirect_access() {
    print!("called rary's `indirect_access()`, that\n> ");

    private_function();
}

编译,默认情况下,库会使用 crate 文件的名字,前面加上 “lib” 前缀,但这个默认名称可以 使用 crate_name 属性 覆盖。

$ rustc --crate-type=lib rary.rs
$ ls lib*
library.rlib

要将一个 crate 链接到新建的库,可以使用 rustc 的 --extern 选项。然后将所有的物件导入到与库名相同的模块下。此模块的操作通常与任何其他模块相同。

fn main() {
    rary::public_function();

    // 报错! `private_function` 是私有的
    //rary::private_function();

    rary::indirect_access();
}

编译

# library.rlib 是已编译好的库的路径,这里假设它在同一目录下:
$ rustc executable.rs --extern rary=library.rlib --edition=2018 && ./executable
called rary's `public_function()`
called rary's `indirect_access()`, that
> called rary's `private_function()`

package

包(package) 是提供一系列功能的一个或者多个 crate,由 Cargo 创建,包含有一个 Cargo.toml 文件,阐述元信息和依赖关系。

包中所包含的内容:

  • 一个包中至多只能包含一个库 crate(library crate):src/lib.rs
  • 包中可以包含任意多个二进制 crate(binary crate):src/main.rs(编译为与 Package 同名的可执行文件)、src/bin/*.rs
  • 包中至少包含一个 crate,无论是库的还是二进制的。

Cargo 约定:

  • src/main.rs 就是一个与包同名的二进制 crate 的 crate 根。
  • 如果包目录中包含 src/lib.rs,则包带有与其同名的库 crate,且 src/lib.rs 是 crate 根。
  • crate 根文件将由 Cargo 传递给 rustc 来实际构建库或者二进制项目。
  • 如果一个包同时含有 src/main.rs 和 src/lib.rs,则它有两个 crate:一个库和一个二进制项,且名字都与包相同。
  • 通过将文件放在 src/bin 目录下,一个包可以拥有多个二进制 crate,每个 src/bin 下的文件都会被编译成一个独立的二进制 crate。

一个 crate 会将一个作用域内的相关功能分组到一起,使得该功能可以很方便地在多个项目之间共享。

模块系统和作用域

Rust 提供了一套强大的模块(module)系统,可以将代码按层次分成多个逻辑单元(模块),并管理这些模块之间的可见性(公有(public)或私有(private))。

模块是项(item)的集合,项可以是:函数,结构体,trait,impl 块,甚至其它模块。

模块

模块让我们可以将一个 crate 中的代码进行分组,以提高可读性与重用性。模块还可以控制项的私有性,即项是可以被外部代码使用的(public),还是作为一个内部实现的内容,不能被外部代码使用(private)。

通过执行 cargo new --lib restaurant,来创建一个新的名为 restaurant 的库。
src/lib.rs 的内容:

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

        fn seat_at_table() {}
    }

    mod serving {
        fn take_order() {}

        fn server_order() {}

        fn take_payment() {}
    }
}

我们用关键字 mod 定义一个模块,指定模块的名字,并用大括号包围模块的主体。我们可以在模块中包含其他模块,就像本示例中的 hosting 和 serving 模块。模块中也可以包含其他项,比如结构体、枚举、常量、trait、函数。

通过使用模块,我们可以把相关的定义组织起来,并通过模块命名来解释为什么它们之间有相关性。使用这部分代码的开发者可以更方便的循着这种分组找到自己需要的定义,而不需要通览所有。编写这部分代码的开发者通过分组知道该把新功能放在哪里以便继续让程序保持组织性。

之前我们提到,src/main.rs 和 src/lib.rs 被称为 crate 根。如此称呼的原因是,这两个文件中任意一个的内容会构成名为 crate 的模块,且该模块位于 crate 的被称为模块树的模块结构的根部。

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

可见性

默认情况下,模块中的项拥有私有的可见性(private visibility),不过可以加上 pub 修饰语来使其公有。模块中只有公有的(public)项可以从模块外的作用域访问。

使模块公有并不使其内容也是公有的,模块上的 pub 关键字只允许其父模块引用它。

// 一个名为 `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` 必须是父模块或祖先模块
        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() {
            println!("called `my_mod::nested::public_function_in_nested");
        }

        // 使用 `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_funcion_in_my_mod()`, that\n> ");
        nested::public_function_in_my_mod();
        print!("> ");
        nested::public_function_in_super_mod();
    }

    // `pub(crate)` 使得函数只在当前 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();

    // 模块的私有项不能直接访问,即便它是嵌套在公有模块内部的
    // my_mod::private_function();
    // my_mod::nested::private_function();
    // my_mod::private_nested::function();
}

结构体和枚举的可见性

结构体的字段默认拥有私有的可见性,也可以加上 pub 修饰语来重载该行为。只有从结构体被定义的模块之外访问其字段时,这个可见性才会起作用,其意义是隐藏信息(即封装)。

如果我们在一个结构体定义的前面使用了 pub ,这个结构体会变成公有的,但是这个结构体的字段仍然是私有的。我们可以根据情况决定每个字段是否公有。

mod back_of_house {
    pub struct Breakfast {
        pub toast: String,
        seasonal_fruit: String,
    }

    impl Breakfast {
        pub fn summer(toast: &str) -> Breakfast {
            Breakfast {
                toast: String::from(toast),
                seasonal_fruit: String::from("peaches"),
            }
        }
    }
}

pub fn eat_at_restaurant() {
    let mut meal = back_of_house::Breakfast::summer("Rye");
    meal.toast = String::from("Wheat");
    println!("I'd like {} toast please", meal.toast);
    // meal.seasonal_fruit = String::from("blueberries"); // 编译报错
}

与之相反,如果我们将枚举设为公有,则它的所有成员都将变为公有。

mod back_of_house {
    pub enum Appetizer {
        Soup,
        Salad,
    }
}

pub fn eat_at_restaurant() {
    let order1 = back_of_house::Appetizer::Soup;
    let order2 = back_of_house::Appetizer::Salad;
}
路径

路径有两种形式:

  • 绝对路径(absolute path)从 crate 根部开始,以 crate 名或者字面量 crate 开头。
  • 相对路径(relative path)从当前模块开始,以 self、super 或当前模块的标识符开头。

绝对路径和相对路径都后跟一个或多个由双冒号(::)分割的标识符。

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

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

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

选择使用相对路径还是绝对路径,还是要取决于你的项目。取决于你是更倾向于将项的定义代码与使用该项的代码分开来移动,还是一起移动。我们更倾向于使用绝对路径,因为把代码定义和项调用各自独立地移动是更常见的。

使用 super 起始的相对路径

fn serve_order() {}

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

    fn cook_order() {}
}

use 关键字

到目前为止,似乎我们编写的用于调用函数的路径都很冗长且重复,并不方便。幸运的是,我们可以使用 use 关键字将路径一次性引入作用域,然后调用该路径中的项,就如同它们是本地项一样。

use crate::front_of_house::hosting;

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

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

在作用域中增加 use 和路径类似于在文件系统中创建软连接。通过在 crate 根增加 use crate::front_of_house::hosting,现在 hosting 在作用域中就是有效的名称了,如同 hosting 模块被定义于 crate 根一样。通过 use 引入作用域的路径也会检查私有性,同其它路径一样。

习惯用法:

  • 使用 use 将函数的父模块引入作用域,这样可以清晰地表明函数不是在本地定义的,同时使完整路径的重复度最小化。不要直接使用 use 将函数本身引入作用域。
  • 使用 use 引入结构体、枚举和其他项时,习惯是指定它们的完整路径。
  • 两个项具有相同名称是可将父模块引入作用域以区分,或者使用 as 关键字指定别名。
使用 as 关键字提供新的名称
use std::fmt::Result;
use std::io::Result as IoResult;

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

fn function2() -> IoResult<()> {
    // --snip--
    Ok(())
}
使用 pub use 重导出名称

当使用 use 关键字将名称导入作用域时,在新作用域中可用的名称是私有的。如果为了让调用你编写的代码的代码能够像在自己的作用域内引用这些类型,可以结合 pub 和 use。这个技术被称为 “重导出”,因为这样做将项引入作用域并同时使其可供其他代码引入自己的作用域。

pub use crate::front_of_house::hosting;

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

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

通过 pub use,现在可以通过新路径 hosting::add_to_waitlist 来调用 add_to_waitlist 函数。如果没有指定 pub use,eat_at_restaurant 函数可以在其作用域中调用 hosting::add_to_waitlist,但外部代码则不允许使用这个新路径。

当你的代码的内部结构与调用你的代码的开发者的思考领域不同时,重导出会很有用。使用 pub use,我们可以使用一种结构编写代码,却将不同的结构形式暴露出来。这样做使我们的库井井有条,方便开发这个库的开发者和调用这个库的开发者之间组织起来。

嵌套路径来消除大量的 use 行
use std::{cmp::Ordering, io};
// 等同于
use std::cmp::Ordering;
use std::io;

use std::io::{self, Write};
// 等同于
use std::io;
use std::io::Write;
通过 glob 运算符将所有的公有定义引入作用域
use std::collections::*;

文件分层

当模块变得更大时,你可能想要将它们的定义移动到单独的文件中,从而使代码更容易阅读。

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 模块也提取到其自己的文件中,src/front_of_house.rs 仅包含 hosting 模块的声明:
src/front_of_house.rs

pub mod hosting;

接着我们创建一个 src/front_of_house 目录和一个包含 hosting 模块定义的 src/front_of_house/hosting.rs 文件:
src/front_of_house/hosting.rs

pub fn add_to_waitlist() {}

模块树依然保持相同,eat_at_restaurant 中的函数调用也无需修改继续保持有效,即便其定义存在于不同的文件中。这个技巧让你可以在模块代码增长时,将它们移动到新文件中。

posted @ 2025-07-21 10:35  carol2014  阅读(49)  评论(0)    收藏  举报