为什么需要"访问控制"
- 隐藏实现细节,只暴露必要接口
- 防止外部误用,减少后续兼容压力
- 支持模块化开发(App、Framework、Swift Package 多目标混合)
一句话:接口公开,实现隐藏;该见的见,不该见的永远看不见。
Swift 的三层作用域与六级权限
| 级别 | 可见范围 | 可继承/重写 | 典型用途 |
|---|---|---|---|
| open | 模块外可见,可子类化、可 override | ✅ | 框架的"设计出口" |
| public | 模块外可见,不可子类化/override | ❌ | 稳定 API |
| package | 同一 package 内所有模块 | ✅/❌ | Swift Package 跨模块协作 |
| internal | 默认级,仅当前模块 | ✅ | 模块内部实现 |
| fileprivate | 仅当前源文件 | ✅ | 同一文件内多个类型/扩展共享 |
| private | 仅当前声明 + 同一文件内扩展 | ✅ | 最小化封装单元 |
默认策略速记
- 不手写访问修饰符 → internal
- 类型的默认成员 → 跟随类型(类型 public → 成员 internal)
- 嵌套类型在 public 类型里 → internal(需要公开再写 public)
代码实战:六级权限一次看个够
swift
// 文件:FrameworkA.swift (属于 FrameworkA 模块)
// 1. open:允许外部模块子类化
open class OpenClass {
open func overrideMe() {} // 外部可 override
public func notOverrideOutside() {} // 外部只能调,不能 override
}
// 2. public:稳定接口,不可继承
public struct PublicAPI {
public init() {}
internal var innerCounter = 0 // 模块外不可见
}
// 3. package:同一 Package 内共享
package protocol PackageService {
func fetch() -> String
}
// 4. internal:不暴露给外部
internal class InnerHelper {
@MainActor static let shared = InnerHelper()
private init() {}
}
// 5. fileprivate:同一文件内复用
fileprivate extension String {
var trimmed: String { trimmingCharacters(in: .whitespaces) }
}
// 6. private:隐藏到"声明内部"
public struct Trimmer {
private(set) var count: Int = 0 // 只读公开,写私有
public mutating func trim(_ s: inout String) {
s = s.trimmed // 同一文件,可访问 fileprivate
count += 1
}
}
继承与重写中的"升权"
swift
// 同一模块内
public class A {
fileprivate func hidden() {}
}
internal class B: A {
override internal func hidden() {} // 合法:在同一文件,升级访问权
}
规则回顾:
- 子类访问级别 ≤ 父类
- 重写可提高访问权,但不能降低
- 跨模块只能继承/重写
open成员
协议、扩展、泛型、别名的细节
- 协议
- 协议权限 ≥ 其所有要求
- 继承的协议不能比父协议更开放
- conformance 权限 = min(类型权限, 协议权限)
swift
public protocol PublicProtocol {
func foo()
}
internal class InternalImpl: PublicProtocol {
func foo() {} // 自动 internal,满足要求
}
- 扩展
- 扩展可写访问修饰符,为内部成员统一设置默认级
- 用于协议 conformance 的扩展不能写访问修饰符,由协议本身决定
swift
extension Trimmer: CustomStringConvertible {
public var description: String { "trimmed \(count) times" }
}
- 泛型
- 泛型实体权限 = min(自身权限, 所有约束类型权限)
swift
internal protocol CacheKey {}
public struct Cache<T: CacheKey> {} // 实际权限 internal
- 类型别名
- 别名权限 ≤ 原类型权限
- 利用别名可在模块外"隐藏"真实类型
swift
public typealias Token = String // OK
private typealias SecretDict = [String: Any] // 仅当前文件可用
Package 级访问:多模块仓库的"朋友圈"
场景:一个 Swift Package 包含 Network、UI、Core 三个模块,希望 Core 的接口仅被 Network/UI 使用,而不暴露给最终 App。
swift
// Core 模块
package protocol DataLoader {
package func load() -> Data
}
在 Package 外(App)import Core 后,无法看见 DataLoader,真正做到"仓库内共享,仓库外隔离"。
常见踩坑与调试技巧
| 错误提示 | 原因 | 解决 |
|---|---|---|
| Cannot assign to property: 'count' is a get-only property | 用了 private(set) 却在外部赋值 |
移除 setter 限制或提供内部 API |
| Class cannot be declared public because its superclass is internal | 子类比父类"显眼" | 提升父类或降低子类 |
| Function cannot be declared internal because its parameter uses a private type | 函数权限 > 参数权限 | 提升类型权限或降低函数权限 |
总结与工程实践建议
-
写框架先画"可见性矩阵":哪些类需要被继承?哪些 API 未来必须冻结?
- 需要被继承 →
open - 仅调用 →
public - 仓库内复用 →
package - 模块内复用 →
internal - 文件内工具 →
fileprivate - 纯内部辅助 →
private
- 需要被继承 →
-
先写
internal,真正需要暴露时再升级,避免"过度公开" -
对
private(set)"只读公开"模式上瘾,可大幅减少后续 Breaking Change。 -
用
@testable而非"为了测试把 private 改成 public"。 -
大型 Package 采用"Core → Service → UI"三级依赖,配合
package访问级,保证依赖方向无环,又隐藏核心实现。
扩展场景:访问控制 + SwiftUI + 插件化架构
SwiftUI 的 public 初始化器经常需要接收"仅内部使用的配置对象"。此时可用类型擦除 + 协议权限组合:
swift
// 对外只能拿到协议
public protocol ConfigProtocol {}
// 实际配置在模块内
internal struct RealConfig: ConfigProtocol {
var apiKey: String
}
public struct MyView: View {
public init(config: ConfigProtocol) { ... }
}
App 只能持有 ConfigProtocol,无法直接访问 apiKey,实现"接口公开,配置隐藏"。
一句话背下来
"高"不能依赖"低",默认 internal 先写着;需要再升级,绝不一步到位全 public