1-4.【OC】【基础】Objective-C 中,哪些错误能在编译期发现,哪些一定要到运行时才能发现?

2 阅读3分钟

由于 Objective-C 拥有“C 的肉体”和“Smalltalk 的灵魂”,它的错误检查机制也被撕裂成了两个层面:底层的静态约束上层的动态自由


1. 编译期(Compile Time)能发现的错误

编译器(Clang)主要负责检查语法合规性内存布局的确定性

  • 语法错误: 漏写分号、括号不匹配、关键字拼错(如把 @implementation 写成 @implement)。
  • C 语言层面的类型不匹配: 将一个 int 赋值给结构体,或者指针类型转换错误(没有强制转换)。
  • 重复定义: 在同一个作用域定义了两个同名的变量或类。
  • 静态方法签名检查: * 如果你调用 [obj method],而 obj 的类型在 .h 中没有声明该方法,编译器会报 Warning(注意:默认通常是警告,除非开启了“Warning as Error”)。
  • ARC(自动引用计数)内存规范: 在 ARC 下尝试手动调用 retainreleaseautorelease,编译器会直接报错。
  • 协议(Protocol)缺失: 如果一个类声称遵守某个协议,但没实现其中的 @required 方法,编译器会发出警告。

2. 运行时(Runtime)才能发现的错误

这是 Objective-C 最臭名昭著也最灵活的地方——只要逻辑能自圆其说,编译器就放行,真正的考验在程序跑起来那一刻。

  • Unrecognized Selector(最经典的 Crash):

    • 现象: 给对象发送了一个它无法响应的消息。
    • 原因: 编译时对象被声明为 id 类型,或者虽然声明了类型,但在运行过程中该指针指向了另一个完全不同的对象(类型擦除)。
  • KVC (Key-Value Coding) 路径错误:

    • 使用 [obj setValue:val forKey:@"wrongKey"] 时,如果 wrongKey 不存在,编译期完全无法察觉,运行时直接崩溃(valueForUndefinedKey:)。
  • 数组越界与字典塞空值:

    • array[10](如果数组只有 5 个元素)或 dict[@"key"] = nil。这些逻辑错误只能在执行到那一行代码时被拦截。
  • 多线程死锁与竞态条件:

    • 由于线程调度是动态的,这类错误在编译阶段是物理不可查的。
  • 野指针与僵尸对象(Zombie Objects):

    • 虽然 ARC 减少了这类问题,但如果使用了 unsafe_unretained 或在底层 C 代码中手动管理内存,访问已释放的对象会导致运行时崩溃。

3. 核心差异总结表

错误类型发现时间报错形式本质原因
拼写与语法编译期Error违反了 C/Obj-C 的语言文法
头文件引用缺失编译期Error符号表无法链接
方法未实现运行时Crash方法名(Selector)在方法列表中找不到对应的 IMP
动态类型转换错误运行时Crash实际对象的类与代码预期不符

💡 一个有趣的“中间地带”

为了平衡 Smalltalk 的动态性带来的风险,Objective-C 引入了一些编译器指令来把运行时错误提前到编译期:

  • instancetype 告诉编译器返回的是“当前类的实例”,从而让编译器能辅助检查返回对象的方法。
  • 泛型(Generics):NSArray<NSString *> *,这纯粹是给编译器看的,用来在编译阶段拦截往数组里乱塞 NSNumber 的行为。