访问控制
本教程共 93 篇 · 第 73 篇 · 更新于 2026-08-08 · 约 9 分钟阅读
本节目标:理解 Swift 的访问级别,学会用
private/fileprivate/internal/public/open/package控制代码在模块、源文件、包与声明之间的可见范围。
写一个小程序时,所有代码随你取用,几乎不用操心谁能看见谁。可一旦代码变多、要拆成框架给别人用,你就得想清楚:哪些是对外公开的能力,哪些是内部实现、不该泄露的细节。访问控制(access control)就是 Swift 用来划定”可见范围”的机制——它把实现细节藏起来,只暴露你愿意给的接口。
1-1 三个基本概念
Swift 的访问控制建立在三个概念上。
模块(module)是代码分发的基本单元:一个框架或 App,作为整体被构建、被别的模块用 import 引入。Xcode 里每个构建目标(target)就是一个独立模块。源文件(source file)是模块里单个的 .swift 文件。包(package)则是用 Swift Package Manager 等工具组织起来的一组模块。
访问级别就是相对”定义该代码的源文件""它所属的模块""它所属的包”来衡量的。理解了这三者,下面的级别才好懂。
1-2 六个访问级别
Swift 提供从宽松到严格的六个访问级别。从高(最开放)到低(最封闭)依次是:
open与public:允许在定义模块的任何源文件使用,也允许别的模块引入后使用。通常用来标一个框架的对外接口。open只用于类和类成员,比public更开放——它允许别的模块继承、重写。package:允许在定义包内的任何源文件使用,但包外不行。适合把一个 App 或框架拆成多个模块、又只想在包内共享时。internal:允许在定义模块的任何源文件使用,模块外不行。这是绝大多数代码的默认级别。fileprivate:只允许在定义它的那个源文件里使用。用来把”只在这一文件内有用”的实现细节藏起来。private:只允许在”包裹它的声明”及同文件的该声明扩展里使用,范围比fileprivate更小。
Note
open和public的区别只在类和类成员上:public类不能被别的模块继承或重写,而open可以。给框架设计公共类时,若你考虑过”允许外部当父类”,才标open。
1-3 默认访问级别
如果你不写任何访问修饰符,大多数代码默认是 internal。也就是说,写个简单单目标的 App,你几乎从不需要显式标访问级别——默认的 internal 已经满足”模块内随便用”。只有想对外公开(标 public/open)或想隐藏细节(标 fileprivate/private)时,才需要动手。
一个总体原则贯穿所有规则:不能让一个实体建立在”访问级别更低”的另一个实体之上。比如一个 public 变量的类型不能是 internal 或 private 的类型——否则在变量能被用到的地方,它的类型却可能不可见。函数也不能比它的参数类型和返回类型更开放。编译器会替你把这道关。
1-4 自定义类型与它的成员
给类型标访问级别时,写在该类型定义处。类型的访问级别会”传染”给它的成员(属性、方法、初始化器、下标)的默认级别。
关键一点:public 类型的成员默认是 internal,不是 public。也就是说,你标了一个类是 public,但它的方法、属性并不会自动对外公开,还得逐个显式标 public。这条规矩防止你”不小心把内部实现暴露成公共 API”。
public class SomePublicClass {
public var somePublicProperty = 0 // 显式 public
var someInternalProperty = 0 // 隐式 internal
fileprivate func someFilePrivateMethod() {}
private func somePrivateMethod() {}
}
反过来,如果类型是 private 或 fileprivate,它的成员默认也跟着是 private / fileprivate;类型是 internal(或默认),成员默认 internal。
1-5 子类与重写
对子类有个限制:子类的访问级别不能高于父类。你不能写一个 public 子类去继承一个 internal 父类——因为父类的细节在外部可能根本不可见,继承就无意义。
但重写可以把一个成员的可见度”调高”。比如父类有个 fileprivate 方法,子类(同模块、甚至同文件)可以 override 成 internal:
public class A {
fileprivate func someMethod() {}
}
internal class B: A {
override internal func someMethod() {}
}
只要重写发生在允许访问原成员的上下文里(同文件或同模块),就能把可见度提上来。这一条让”父类低调、子类在合适范围内放开”成为可能。
1-6 读写分离:只暴露 getter
常量、变量、属性、下标的 getter 和 setter,默认跟它们所属实体的访问级别一致。但你有权把 setter 设得比 getter 更严格,从而做出”外部只读、内部可写”的属性。
写法是在 var 前加 private(set)、internal(set) 或 fileprivate(set):
struct TrackedString {
private(set) var numberOfEdits = 0
var value: String = "" {
didSet {
numberOfEdits += 1
}
}
}
numberOfEdits 的 getter 是默认的 internal,但 setter 被限成 private——只有 TrackedString 内部能改它,外部只能读。于是别的文件能查”被改了几次”,却改不了这个计数,实现细节被稳稳护住。
如果是 public 结构体想对外只读,就写 public private(set):getter 是 public,setter 仍是 private。
public struct TrackedString {
public private(set) var numberOfEdits = 0
public var value: String = "" {
didSet { numberOfEdits += 1 }
}
public init() {}
}
Tip“只读对外、只内部改”是访问控制里极常用的模式,用
private(set)一行就能搞定,不必写两个属性去手动同步。
1-7 扩展里的访问控制
扩展可以在被扩展类型”可见”的任意访问上下文里写。扩展里新增的成员,默认访问级别和原类型成员一致:扩展一个 public 或 internal 类型,新成员默认 internal;扩展 fileprivate 类型,新成员默认 fileprivate。
你也可以给整个扩展标一个访问级别,作为里面所有成员的”新默认”,个别成员还能再覆盖。但有一种例外:当扩展是用来”添加协议遵循”时,不能给扩展标访问级别——这时协议自己的级别决定了每个协议要求实现的默认级别。
还有一个贴心规则:和原类型写在同一个文件里的扩展,视作原类型声明的一部分。所以你可以在原声明里写 private 成员,在同文件的扩展里访问它;反过来在扩展里写 private 成员,原声明里也能访问。这让”用扩展来整理代码”无论是私有还是公有成员都顺手。
1-8 小结
访问控制用 open/public/package/internal/fileprivate/private 把代码可见范围收进模块、包、源文件、声明之内。默认是 internal,够用就不必写;只有想对外公开才标 public/open,想藏细节才标 fileprivate/private。public 类型的成员默认仍是 internal,要公开得逐个显式标。子类不能高于父类,但重写可提升可见度。用 private(set) 能做”对外只读、内部可写”。扩展的成员默认跟随原类型,同文件扩展视作一体。下章我们看类型嵌套,把相关的类型收进同一作用域。