MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Rust模块访问控制策略

2022-05-283.7k 阅读

Rust 模块系统基础

在 Rust 中,模块系统是组织代码的重要方式,它有助于将大型项目分解为更小、更易于管理的部分。模块可以包含函数、结构体、枚举等各种 Rust 语言元素。

定义模块使用 mod 关键字。例如,以下代码定义了一个简单的模块 my_module

mod my_module {
    pub fn my_function() {
        println!("This is my function in my_module");
    }
}

在上述代码中,my_module 模块包含一个名为 my_function 的函数。注意这里函数前面的 pub 关键字,它涉及到访问控制,稍后会详细介绍。

模块可以嵌套。比如我们可以在 my_module 内再定义一个子模块:

mod my_module {
    pub fn my_function() {
        println!("This is my function in my_module");
    }

    mod sub_module {
        pub fn sub_function() {
            println!("This is sub_function in sub_module");
        }
    }
}

要使用模块中的内容,需要通过路径来访问。如果在同一个文件中,可以直接使用相对路径:

mod my_module {
    pub fn my_function() {
        println!("This is my function in my_module");
    }

    mod sub_module {
        pub fn sub_function() {
            println!("This is sub_function in sub_module");
        }
    }
}

fn main() {
    my_module::my_function();
    my_module::sub_module::sub_function();
}

上述 main 函数中,通过 my_module::my_function()my_module::sub_module::sub_function() 分别调用了模块和子模块中的函数。

访问控制关键字 pub

在 Rust 中,默认情况下,模块及其内部的项(函数、结构体、枚举等)都是私有的。这意味着它们只能在定义它们的模块内部被访问。要使模块或项能够从外部访问,需要使用 pub 关键字。

  1. pub 用于函数: 如前面例子中的 my_functionsub_function,加上 pub 关键字后,它们可以在模块外部被调用。如果没有 pub,像下面这样的代码会报错:
mod my_module {
    fn my_function() {
        println!("This is my function in my_module");
    }
}

fn main() {
    my_module::my_function(); // 报错:private function
}

编译器会提示 my_function 是私有的,不能从外部访问。

  1. pub 用于结构体: 当定义结构体时,即使结构体本身是 pub 的,其字段默认也是私有的。例如:
mod my_module {
    pub struct MyStruct {
        data: i32,
    }

    impl MyStruct {
        pub fn new(data: i32) -> MyStruct {
            MyStruct { data }
        }

        pub fn get_data(&self) -> i32 {
            self.data
        }
    }
}

fn main() {
    let my_struct = my_module::MyStruct::new(42);
    let data = my_struct.get_data();
    println!("Data: {}", data);
}

在上述代码中,MyStruct 结构体是 pub 的,这样可以在模块外部创建实例。但它的 data 字段是私有的,所以通过定义 pub 方法 newget_data 来间接访问 data 字段。如果想让 data 字段也能直接从外部访问,可以将其声明为 pub

mod my_module {
    pub struct MyStruct {
        pub data: i32,
    }
}

fn main() {
    let mut my_struct = my_module::MyStruct { data: 42 };
    my_struct.data = 43;
    println!("Data: {}", my_struct.data);
}
  1. pub 用于枚举: 枚举与结构体不同,当枚举被声明为 pub 时,其所有成员默认也是 pub 的。例如:
mod my_module {
    pub enum MyEnum {
        Variant1,
        Variant2,
    }
}

fn main() {
    let my_enum = my_module::MyEnum::Variant1;
}

这里 MyEnum 及其成员 Variant1Variant2 都可以在模块外部使用。

pub(crate)pub(self)

除了普通的 pub,Rust 还提供了 pub(crate)pub(self) 这两个更精细的访问控制关键字。

  1. pub(crate)pub(crate) 表示该项在整个 crate 内是可见的,但在 crate 外部不可见。Crate 是 Rust 中的一个编译单元,可以是一个二进制可执行文件或一个库。

假设我们有一个库项目,结构如下:

src/
├── lib.rs
└── my_module/
    └── mod.rs

lib.rs 中:

pub mod my_module;

fn main() {
    my_module::my_function();
}

my_module/mod.rs 中:

pub(crate) fn my_function() {
    println!("This is my_function visible within the crate");
}

上述代码中,my_function 使用 pub(crate) 声明,它可以在整个 crate 内(lib.rs 中)被调用,但如果这个库被其他项目引用,其他项目无法调用 my_function

  1. pub(self)pub(self) 表示该项在当前模块及其子模块内可见。例如:
mod outer_module {
    pub(self) fn outer_function() {
        println!("This is outer_function");
    }

    mod inner_module {
        fn call_outer() {
            outer_module::outer_function();
        }
    }
}

fn main() {
    // outer_module::outer_function(); // 报错:private function
}

在上述代码中,outer_function 使用 pub(self) 声明,它可以在 outer_module 及其子模块 inner_module 中被调用,但在 main 函数中调用会报错,因为 main 函数不在 outer_module 及其子模块的范围内。

super 关键字与相对路径访问

super 关键字用于从当前模块的父模块开始构建路径。这在处理嵌套模块时非常有用。

例如,我们有如下嵌套模块结构:

mod outer_module {
    mod inner_module {
        fn inner_function() {
            println!("This is inner_function");
        }

        fn call_outer() {
            super::outer_function();
        }
    }

    fn outer_function() {
        println!("This is outer_function");
    }
}

inner_modulecall_outer 函数中,使用 super::outer_function() 来调用父模块 outer_module 中的 outer_function

相对路径也可以使用 self,它表示当前模块。例如,如果在 inner_module 中定义了一个与父模块中同名的函数,想调用当前模块中的函数,可以使用 self::function_name

使用 use 关键字简化路径

随着项目规模的增大,模块路径可能会变得很长,使用 use 关键字可以简化路径。

例如,有如下模块结构:

mod top_level {
    mod middle_level {
        mod bottom_level {
            pub fn bottom_function() {
                println!("This is bottom_function");
            }
        }
    }
}

main 函数中,如果不使用 use,调用 bottom_function 需要完整路径:

fn main() {
    top_level::middle_level::bottom_level::bottom_function();
}

使用 use 关键字后,可以简化路径:

use top_level::middle_level::bottom_level::bottom_function;

fn main() {
    bottom_function();
}

use 还可以用于引入结构体、枚举等。例如:

mod my_module {
    pub struct MyStruct {
        pub data: i32,
    }
}

use my_module::MyStruct;

fn main() {
    let my_struct = MyStruct { data: 42 };
}

此外,use 支持通配符 *,可以引入模块中的所有 pub 项。但在实际使用中,不建议滥用通配符,因为这可能会导致命名冲突,并且不利于代码的可读性。例如:

mod my_module {
    pub fn my_function() {
        println!("This is my_function");
    }

    pub fn another_function() {
        println!("This is another_function");
    }
}

use my_module::*;

fn main() {
    my_function();
    another_function();
}

模块的文件组织与访问控制

在实际项目中,模块通常会分布在多个文件中。Rust 提供了灵活的方式来组织模块文件并保持访问控制。

假设我们有一个项目结构如下:

src/
├── lib.rs
└── utils/
    ├── mod.rs
    └── math.rs

lib.rs 中:

pub mod utils;

fn main() {
    utils::math::add(2, 3);
}

utils/mod.rs 中:

pub mod math;

utils/math.rs 中:

pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

这里通过在 mod.rs 中使用 pub mod math; 来公开 math.rs 中的模块,在 lib.rs 中通过 pub mod utils; 公开 utils 模块,从而使得 main 函数可以访问到 utils::math::add 函数。

如果 math.rs 中的函数不想被外部直接访问,可以去掉 pub,或者使用更精细的访问控制如 pub(crate) 等。例如,如果将 math.rs 中的 add 函数改为 pub(crate)

pub(crate) fn add(a: i32, b: i32) -> i32 {
    a + b
}

那么只有在当前 crate 内可以访问 add 函数,外部项目引用这个库时将无法访问。

访问控制与 trait

在 Rust 中,trait 也受到访问控制的影响。

定义 trait 时,其可见性遵循普通的访问控制规则。例如:

mod my_module {
    pub trait MyTrait {
        fn my_method(&self);
    }

    pub struct MyStruct {
        data: i32,
    }

    impl MyTrait for MyStruct {
        fn my_method(&self) {
            println!("Data: {}", self.data);
        }
    }
}

use my_module::{MyTrait, MyStruct};

fn main() {
    let my_struct = MyStruct { data: 42 };
    let _: &dyn MyTrait = &my_struct;
    my_struct.my_method();
}

在上述代码中,MyTraitMyStruct 都是 pub 的,所以可以在模块外部使用。如果 MyTrait 没有 pub,那么在 main 函数中使用 MyTrait 会报错。

当实现 trait 时,trait 和实现的可见性共同决定了该实现的可见性。例如,如果 MyTraitpub 的,但 MyStruct 是私有的,那么虽然 MyTrait 可以在外部使用,但无法为 MyStruct 创建实例并使用其实现的 MyTrait 方法。

访问控制与泛型

泛型在 Rust 中也与访问控制相互作用。

例如,定义一个泛型结构体和泛型函数:

mod my_module {
    pub struct GenericStruct<T> {
        data: T,
    }

    pub fn generic_function<T>(arg: T) {
        println!("Generic function with arg: {:?}", arg);
    }
}

use my_module::{GenericStruct, generic_function};

fn main() {
    let my_struct = GenericStruct { data: 42 };
    generic_function(43);
}

在上述代码中,GenericStructgeneric_function 都是 pub 的,它们的泛型参数 T 没有额外的访问控制修饰。这意味着只要结构体和函数本身是可见的,就可以使用任意类型作为泛型参数,前提是该类型满足函数或结构体内部的约束(如实现特定的 trait 等)。

如果在泛型参数上添加约束,并且这些约束涉及到访问控制,情况会更复杂。例如:

mod my_module {
    pub trait MyTrait {
        fn my_method(&self);
    }

    pub struct GenericStruct<T: MyTrait> {
        data: T,
    }

    impl<T: MyTrait> GenericStruct<T> {
        pub fn call_method(&self) {
            self.data.my_method();
        }
    }
}

mod other_module {
    use super::my_module::MyTrait;

    struct InnerStruct;

    impl MyTrait for InnerStruct {
        fn my_method(&self) {
            println!("InnerStruct method");
        }
    }

    fn use_generic_struct() {
        let inner = InnerStruct;
        let my_struct = super::my_module::GenericStruct { data: inner };
        my_struct.call_method();
    }
}

在上述代码中,GenericStruct 要求其泛型参数 T 实现 MyTraitother_module 中的 InnerStruct 实现了 MyTrait,并且由于 MyTraitpub 的,所以 InnerStruct 可以作为 GenericStruct 的泛型参数使用。

访问控制在实际项目中的应用场景

  1. 封装与信息隐藏:通过将内部实现细节设置为私有,只暴露必要的接口给外部使用。例如,一个数据库操作库,内部可能有复杂的连接管理、查询构建等逻辑,但只向用户暴露简单的 query 函数,隐藏内部实现,提高代码的安全性和可维护性。
  2. 模块化开发:在大型项目中,不同团队或模块开发者可以专注于自己的模块,通过合理设置访问控制,避免模块间的不必要依赖和干扰。例如,前端和后端模块可以独立开发,通过定义好的 API 进行交互,各自内部的实现细节对对方是隐藏的。
  3. 库开发:当开发一个库时,使用访问控制可以控制哪些功能被库的使用者访问,哪些是库内部使用的。例如,一个图形渲染库可能有一些底层的渲染算法实现是私有的,只向用户暴露高级的绘图接口。

总之,Rust 的模块访问控制策略为开发者提供了强大而灵活的工具,能够有效地组织代码、保护内部实现细节,并确保不同模块之间的正确交互,无论是在小型项目还是大型的企业级应用中都具有重要意义。