知识门户

Back

深入理解 Rust 的可见性:为什么 pub 结构体不能被直接构造?#

在阅读关于 Rust 类型状态编程(Typestate Programming)的文章时,很多人会看到一个经典模式:通过 Builder 来构造对象。文章常常会强调:“不能直接生成一个 Foo 对象,必须通过 FooBuilder。” 这时,一个常见的疑问就冒了出来:

Foo 不是被标记为 pub 的吗?既然是公开的,为什么不能直接构造它?难道不应该把 Foo 设为 private 才对吗?

这个疑问非常精准,它触及了 Rust 模块系统中一个极为核心且容易被混淆的概念:类型的可见性字段的可见性 是两件不同的事。

本文将围绕这个问题,抽丝剥茧,彻底搞懂 Rust 的可见性规则,并揭示这种设计模式背后的深层逻辑。

一、pub 到底公开了什么?#

很多初学者会认为,pub struct Foo 意味着 Foo 这个结构体“完全公开”了,外部可以随意使用。但实际上,Rust 的可见性规则是精细化的:

  1. 结构体是 pub:这意味着 类型本身的名字 对外可见。外部模块可以引用这个类型,比如在函数签名中使用它。
  2. 字段不是 pub(默认私有):这意味着 字段的名字和值 对外不可见。

这两者叠加,会产生一个看似矛盾但实际非常符合逻辑的结果:外部代码知道 Foo 这个类型存在,但不知道如何把它“组装”出来

我们来看下面这段典型的代码示例:

二、直接构造失败的原因#

在 Rust 中,构造一个结构体实例有两种常见方式:

  • 使用结构体字面量Foo { inner: 5 }
  • 调用一个返回 Self 的关联函数Foo::new()

在类型状态编程的例子中,Foo 并没有提供 new 方法,而其字段 inner 又是私有的。因此,外部代码试图通过 Foo { inner: 5 } 构造时,编译器会直接报错:

// 外部代码,比如在 src/main.rs 或另一个模块中

fn main() {
    // ❌ 编译报错:error[E0456]: field `inner` is private
    // 即使 Foo 是 pub 的,也无法直接使用字段字面量构造
    let f = Foo { inner: 42 };

    // 这是唯一正确的做法:通过 Builder
    let f = FooBuilder::new()
        .set_value(42)
        .into_foo();
}
rust

即使 Foopub 的,也没法绕过这个规则。因为 pub struct 允许你“看见”类型,但私有字段阻止你“触碰”内部结构

三、如果 FooprivateFooBuilder 还能访问吗?#

这取决于它们是否在同一个模块内。这是理解可见性的另一个关键点:模块是可见性的边界

情况一:在同一个模块内(可以)#

如果 FooFooBuilder 定义在同一个文件中(属于同一个 mod),那么即使 Foo 没有 pub 修饰,FooBuilder 也能自由访问它。

// src/foo.rs (同一个模块)
struct Foo { inner: u32 } // 没有 pub,是私有的

pub struct FooBuilder;

impl FooBuilder {
    pub fn into_foo(self) -> Foo {
        Foo { inner: 42 } // ✅ 编译通过
    }
}
rust

原因:私有性规则规定,一个模块内的项对其子模块是可见的,但在同一模块内更是畅通无阻。对于 FooBuilder 来说,Foo 就是“自己家里”的东西,不需要公开。

情况二:在不同的模块内(不可以)#

如果将它们放在不同的文件或 mod 中,那么 Foo 必须是 pub 的,FooBuilder 才能导入并操作它。

// src/foo.rs
struct Foo { inner: u32 } // 私有

// src/builder.rs
use crate::foo::Foo; // ❌ 编译报错:struct `Foo` is private
rust

四、为什么文章依然把 Foo 标记为 pub#

既然 FooBuilder 在同一个模块内无所谓,那为什么大多数示例代码还是要写成 pub struct Foo 呢?

原因是为了对外暴露 API

因为外部调用者(比如 main 函数,或者依赖该库的其他 crate)最终需要拿到 Foo 这个类型。如果 Foo 是私有的,外部代码根本无法在变量类型、函数返回值或参数上使用它:

// 如果 Foo 是私有的,外部代码根本无法引用它
fn main() {
    // ❌ 编译报错:找不到类型 Foo
    let f: Foo = get_foo();
}

// 即使要封装成这样,返回值类型 Foo 也必须是公开的
// 否则外部代码无法声明这个变量
fn get_foo() -> Foo {
    // ...
}
rust

为了让外部世界知道“我们返回了一个 Foo 类型的数据”,Foo 必须pub 的。而为了让外部无法随意伪造一个 Foo,我们把它的字段设为私有

这样既保证了类型的公开可见,又保证了构造过程的绝对受控。

五、类型状态编程:更进一步的实战#

上面讲的是 Builder 模式的基础,而“类型状态编程”更进一步:它利用类型系统来编码对象的状态,让不合法的操作在编译期就被阻止。

如果我们把刚才的 FooBuilder 改造成带状态约束的类型,就能更清晰地看到为什么 Foo 必须是公开的——因为外部代码需要感知到不同的“状态类型”。

在这个类型状态的版本中,into_foo 方法只存在于 FooBuilder<ValueSet> 上。如果用户忘记调用 set_value,编译器会直接报错,因为 FooBuilder<MissingValue> 压根没有 into_foo 这个方法。

这时你会发现,如果 Foo 不是 pub 的,外部 main 函数连 let foo: Foo = ... 这种赋值都写不了。所以 pub struct Foo 是必需的。

六、可见性与设计模式的结合#

这种“公开类型,私有字段”的策略,正是 Builder 模式类型状态编程 能够成立的基础。

  • 强制流程:由于没有公开构造方法,外部代码只能通过 FooBuilder 来生成 Foo 对象。
  • 保障安全FooBuilder 内部可以包含复杂的校验逻辑、状态转换,确保生成的 Foo 永远处于合法状态。
  • 封装细节Foo 内部的字段可能随时调整,只要 into_foo 方法的签名不变,外部代码就不会被破坏。

总结:可见性的三层逻辑链#

为了方便记忆,我们可以把这套逻辑总结为三条规则:

  1. 类型公开(pub struct:为了允许外部代码在接口中使用这个类型。
  2. 字段私有(不写 pub:为了禁止外部代码直接实例化或修改内部状态。
  3. Builder 同模块:因为同一个模块内无视可见性,所以 FooBuilder 可以自由操作 Foo 的私有字段。

七、结语#

当你下次再看到类似的示例代码时,请记住:pub 是用来控制“能不能提这个名字”的,而字段可见性才是控制“能不能碰这个东西”的关键。

Rust 的可见性系统不是一把简单的“公开/私有”开关,而是一套精准的访问控制工具。理解这一点,你不仅能看懂 Builder 模式,还能更深刻地理解 Rust 如何在不依赖运行时检查的情况下,通过类型系统构建出安全、健壮的 API。

希望这篇文章能帮你解开这个困惑。毕竟,在编程的世界里,能看见不代表能触摸,能公开类型不代表能随意构造 —— 这正是 Rust 强大表达能力的一个缩影。

深入理解 Rust 的可见性
https://knowlage-gallary.vercel.app/blog/%E5%B5%8C%E5%85%A5%E5%BC%8Frust/%E5%8F%AF%E8%A7%81%E6%80%A7
Author 甘霖飞
Published at 2026年7月27日