前言:从MRC时代写过来的OC老炮,刚接触Swift时,对访问控制那一坨关键字是真的头大。OC里不就
@public/@private/@protected三兄弟吗?Swift上来给你整五个:open/public/internal/fileprivate/private。尤其是open和public,我盯着看了三天,查了无数博客,直到自己动手写了两个SDK+拆了一个30+Pod的组件化工程,才算是真正悟了。这篇文章,写给所有和我一样从OC转Swift的老哥们。
一、先说说OC里我们是怎么"装"私有
写OC的时候,大家都懂的,所谓的访问控制其实就是个"君子协定"。
objc
// ViewController.h ------ 对外暴露的"脸面"
@interface ViewController : UIViewController
- (void)publicMethod; // 大家好,这是我公开的方法
@end
// ViewController.m ------ 背地里啥都干
@interface ViewController ()
@property (nonatomic, strong) NSMutableArray *secretData; // 说是私有,其实呵呵
- (void)doSomethingSecret;
@end
@implementation ViewController
// 想调用?简单:
// [vc performSelector:@selector(doSomethingSecret)];
// 或者用 class_copyMethodList 遍历一下,啥私有不私有的
@end
说白了,OC的@private更多是编译层面的提示,运行时该访问还是能访问。KVC/Runtime一上,底裤都给你扒了。
所以当我第一次看到Swift的访问控制报错:
'XXX' is inaccessible due to 'private' protection level
我的第一反应是:嗯?编译器来真的?
二、先建立宏观认知:Swift的访问控制是"模块级"的
这是OC和Swift最本质的区别。我用一张表对比:
| 维度 | Objective-C | Swift |
|---|---|---|
| 控制粒度 | .h/.m 文件级(声明可见性) | 模块+文件+作用域 三级 |
| 运行时突破 | 轻松(Runtime/KVC) | 基本不可能(编译期锁定) |
| 默认可见性 | .h里public,.m里private | internal(模块内可见) |
| 继承/重写控制 | 无(任何子类都能重写) | 严格控制(open才允许外部重写) |
| 核心哲学 | 动态、灵活、约定大于限制 | 静态、安全、限制大于约定 |
OC转Swift第一个思维转变 :不要去想"这个变量在哪个头文件里",而要想"这个API需要暴露给哪个模块看"。
三、五个关键字,一张图彻底搞定
说实话我之前看了很多博客,都是罗列定义,看完还是不会用。直到我自己在RN混合容器(就是我项目里那个IOSRNContainer)里拆Pods的时候,踩了一堆坑,才总结出这张"实战决策图":
就这么简单。什么,你要官方定义?谷歌一下有的是,我就不抄文档了。
四、重点中的重点:open vs public ------ 我踩过的坑
我写CommonUI Pod的时候踩得最疼的一次。
4.1 反面教材:全用public,然后我傻眼了
最开始我写BaseView.swift,想省事儿全写了public:
swift
// CommonUI/Views/BaseView.swift
public class BaseView: UIView { // ❌ 应该是 open
public func setupUI() { // ❌ 子类想重写,报错
// 搭建基础UI
}
}
然后在FeedModule里继承:
swift
// FeedModule/FeedCardView.swift
import CommonUI
class FeedCardView: BaseView { // ❌ 编译错误!
// 'BaseView' has been marked as public and cannot be subclassed from outside its module
override func setupUI() { // 就算上面能过,这里也会报错
super.setupUI()
// 加自己的UI
}
}
我当时还骂了Swift一句:这破语言怎么这么矫情?OC里我导入头文件想怎么继承就怎么继承。
后来才明白:这就是Swift要保护你的地方。
4.2 正确姿势:该open的open,该final的final
swift
// ✅ 这才是BaseView的正确打开方式
open class BaseView: UIView {
// 设计为钩子方法,子类必须/可以重写 → open
open func setupUI() {
// 默认实现,子类可重写
}
// 这是主流程,不允许子类破坏 → public final
public final func render() {
setupUI()
bindViewModel()
layoutIfNeeded()
}
// 内部方法,子类也不许看 → private
private func bindViewModel() { ... }
}
一句话总结:
open= 我设计这个类/方法,就是为了让你继承重写的(比如UIKit的UIView)public= 你可以用,但别想改我怎么实现的(这其实是大多数情况)
4.3 大厂SDK的设计哲学:能不open就不open
我后来翻了Alamofire、Kingfisher这些主流SDK的源码,发现了一个有趣的现象:90%的public类都加了final。
swift
// Alamofire 里你会看到大量这种写法
public final class Session { ... }
public final class DataRequest: Request { ... }
open class Request { ... } // 基类才是open,子类一律final
为什么?final = 少一个支持的场景 = 少一个背锅的可能。
SDK设计者最怕啥?最怕外部用户继承你的类,改得乱七八糟,然后出了问题找你说"你这SDK有bug"。final就是SDKer的免责声明。
五、App开发和SDK开发,策略完全不一样
我同时写过公司内部的IM SDK(就是那个有25MB包体限制的),也写过30+Pod的主工程。这两个场景下,访问控制的思路是反过来的:
5.1 SDK开发:默认全private,想公开先审批
SDK的核心原则:API面越小越好。

5.2 App开发:默认internal,真的需要再降级
写App我现在的原则是:除了IBOutlet和状态变量,其他啥关键字都别写。
swift
// ✅ 我现在写ViewController的标准模板
final class FeedViewController: UIViewController {
// IBOutlet:一律 private
@IBOutlet private weak var tableView: UITableView!
// 状态:一律 private,改状态走方法
private var items: [FeedItem] = []
// 依赖注入:internal就行(默认不写)
var viewModel: FeedViewModel!
// 给路由调用的方法:internal
func configure(with data: FeedConfig) {
viewModel.config = data
}
}
为啥不都写成private?因为App不是SDK,写得太死,后续加需求改到你怀疑人生。App追求的是可迭代速度 ,SDK追求的是稳定性和兼容性。
六、OC转Swift的几个"哦原来是这样"的瞬间
6.1 为什么public class的init要手动写?
OC里[[Person alloc] init]从来没出过问题,到Swift里:
swift
// MyPod/Public/Person.swift
public class Person {
public var name: String
// ❌ 不写这行,外部模块根本init不了!
// public init(name: String) { self.name = name }
}
// 主工程里:
let p = Person(name: "张三") // 报错:'Person' initializer is inaccessible
我当时查了半小时,后来才知道Swift的规则:
默认构造器的访问级别永远是
internal,哪怕你的class是public。
想让外部能构造?老老实实写public init()。这其实也是Swift的保护机制:你不明确说可以构造,我就默认不让你构造,防止SDK的内部约定被破坏。
6.2 @IBOutlets为什么一定要private?
OC里我们都是这么写的:
objc
@property (nonatomic, weak) IBOutlet UIButton *confirmBtn;
没人加@private对吧?因为觉得反正外部也不会去改。到Swift里我看所有规范都写要加private:
swift
@IBOutlet private weak var confirmBtn: UIButton!
一开始我觉得多此一举,直到有一天,我在FeedRouter里写了这么一行:
swift
feedVC.confirmBtn.isHidden = true // 绕过viewModel直接改UI状态
好了,状态乱了。按钮的显隐同时被viewModel和路由层两边控制,排查了一下午bug。
加private不是为了防别人,是为了防三个月后的自己。
6.3 为什么SwiftLint不让你写private OC方法?
用SwiftLint的同学一定遇到过private_action这个规则:
❌ Private IBAction methods should be marked as private.
在OC里,- (IBAction)clickConfirm:(id)sender默认就是对外暴露的,没人觉得有问题。但Swift强制你标@IBAction private。
这就是两种语言的哲学差异:
- OC:一切默认开放,你自己注意点
- Swift:一切默认封闭,想开你自己说
七、最后说两句心里话
其实从OC转到Swift,语法层面的东西都好学,难的是思维模式的切换。
OC像一个老朋友,你什么德行他都知道,你想玩点花的(isa swizzling、method exchange)他都陪你玩。 Swift像一个新同事,讲规矩,讲流程,你要干什么都得先打申请(声明访问级别),一开始你觉得别扭,时间长了你发现,出错的概率真的低了很多。
写OC的时候,线上出了个野指针,查三天。 写Swift的时候,编译器把90%的傻逼错误都拦在编译期了。
这大概就是成长吧。我们从追求"怎么爽怎么写",变成了"怎么稳怎么写"。毕竟,谁也不想半夜起来接报警电话不是?
如果觉得有用,点个赞再走吧。有不同意见的,评论区交流,我一定回。