Swift 初体验之访问控制(Access Control)

前言:从MRC时代写过来的OC老炮,刚接触Swift时,对访问控制那一坨关键字是真的头大。OC里不就@public/@private/@protected三兄弟吗?Swift上来给你整五个:open/public/internal/fileprivate/private。尤其是openpublic,我盯着看了三天,查了无数博客,直到自己动手写了两个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%的傻逼错误都拦在编译期了。

这大概就是成长吧。我们从追求"怎么爽怎么写",变成了"怎么稳怎么写"。毕竟,谁也不想半夜起来接报警电话不是?


如果觉得有用,点个赞再走吧。有不同意见的,评论区交流,我一定回。

相关推荐
9765033351 天前
iOS 上架/审核 4.3a Cocos 2026最新方案解读
flutter·ios·swift·cocos2d·ios开发
2501_916007471 天前
IDE 是什么?集成开发环境详解与 iOS 开发选型指南
ide·vscode·ios·objective-c·个人开发·swift·敏捷流程
初级代码游戏2 天前
iOS开发 Swift 速记2:三种集合类型 Array Set Dictionary
开发语言·ios·swift
初级代码游戏3 天前
iOS开发 Swift 速记1:变量和基本数据类型
开发语言·ios·swift
大龄秃头程序员3 天前
Swift 值类型真的每次都会拷贝吗?OC 老兵聊聊 COW
swift
2501_915921433 天前
SwiftUI开发框架入门指南:从基础到实战
ide·vscode·ios·objective-c·个人开发·swift·敏捷流程
东坡肘子3 天前
热茶还是冰咖啡 -- 肘子的 Swift 周报 #147
人工智能·swiftui·swift
大龄秃头程序员4 天前
Swift方法派发
swift
末代iOS程序员华仔4 天前
OPC + Flutter + Swift:跨平台应用上架与专业知识点获客实战指南
开发语言·flutter·swift