从 OC 到 Swift,老 iOS 开发者踩过的 10 个语法大坑
很多老 iOS 工程师写 OC 时靠的是"运行时直觉":nil 能发消息、强转能糊弄、block 和 closure 差不多、NSString 和 String 随便换。
一到 Swift,编译器开始"找茬",运行时反而更乖,但人先崩了。
下面这 10 个坑,基本是每个从 OC 迁移到 Swift 的人都会踩一遍。
1. 以为 nil 还能"发消息不崩"
OC 里:
ini
NSString *name = nil;
NSUInteger len = [name length]; // 返回 0,不崩
Swift 里:
javascript
let name: String? = nil
let len = name.count // 编译都过不了
Swift 的 nil 不是"空对象",而是"可选值里没有值"。
老 OC 人最容易犯的错是:把 Swift 的 nil 当成 OC 的 nil。
正确姿势:
ini
let len = name?.count ?? 0
OC:nil 是朋友,悄悄帮你兜底
Swift:nil 是类型系统里的炸弹,必须先拆
2. 疯狂用 ! 强解包,把 Swift 写成"会崩的 OC"
刚学 Swift 的人写:
ini
let user = getUser()!
let name = user.name!
这和 OC 里:
ini
User *user = [self getUser];
NSString *name = user.name;
看起来一样,但 Swift 里只要有一层是 nil,直接:
lua
Fatal error: Unexpectedly found nil while unwrapping an Optional
老 OC 开发者常误以为"我肯定有值",结果网络数据、字典解析、IBOutlet、混编接口全在坑你。
经验法则:
- 业务数据:用
guard let/if let - 配置项默认值:用
?? !只留给@IBOutlet和你 100% 控制的初始化流程
3. OC 没标 nonnull,Swift 拿到 T! 隐式解包
OC 头文件:
less
@interface UserService : NSObject
- (User *)currentUser;
@end
没包 NS_ASSUME_NONNULL_BEGIN,Swift 看到的是:
go
func currentUser() -> User!
你一写:
scss
let name = UserService().currentUser().name
OC 返回了 nil,Swift 直接崩。
这是混编项目里最阴的坑之一。
解法:
- OC 侧加
NS_ASSUME_NONNULL_BEGIN / END - 返回可能为空的写
nullable - Swift 侧永远当可选值处理
4. 把 NSArray / NSDictionary 的"可变性"带进 Swift
OC 里:
ini
NSArray *arr = @[@"a"];
NSMutableArray *m = (NSMutableArray *)arr; // 编译过,运行炸
Swift 里更隐蔽:
css
let arr = ["a"]
var m = arr
m.append("b")
print(arr) // ["a"]
老 OC 人以为"传数组进去会被改",其实 Swift 的 Array / Dictionary / String 是值类型,赋值会拷贝(COW)。
坑不在"会改坏",而在:
- 你以为共享状态会同步
- 你以为
let只是不能重新赋值(其实元素也不能改) - 你以为
var dict = ocDict as? [String: Any]还是同一个对象
OC:引用语义,处处是共享
Swift:值语义,默认隔离
5. id 思维残留:什么都写 Any / AnyObject
OC 里:
kotlin
- (id)parse:(NSData *)data {
return @{@"code": @0};
}
Swift 里继续写:
swift
func parse(_ data: Data) -> Any {
return ["code": 0]
}
然后满屏:
javascript
if let dict = result as? [String: Any] { ... }
老项目能跑,但类型安全全没了。
Swift 的正确思路:
- 网络模型用
Codable - 配置用具体 struct
Any只出现在桥接边界
6. 以为 BOOL 和 Bool 是一回事
OC:
ini
BOOL flag = 2; // YES
if (flag) { ... }
Swift:
csharp
let flag: Bool = 2 // 编译错误
更坑的是:
arduino
if someInt { ... } // ❌
OC 里"非 0 即真",Swift 里 只有 Bool 能进 if。
老 OC 人写:
arduino
if user.age { } // 想表达 age > 0,结果编译不过
要写:
if user.age > 0 { }
7. 初始化顺序:OC 可以先留空,Swift 不行
OC:
less
@interface VC : UIViewController
@property (nonatomic, strong) NSMutableArray *items;
@end
@implementation VC
- (void)viewDidLoad {
self.items = [NSMutableArray array];
}
@end
Swift 不允许"属性没值还活着":
kotlin
class VC: UIViewController {
var items: [String] // ❌ 编译错误
}
你必须:
less
var items: [String] = []
// 或
var items: [String]?
还有两段式初始化规则:
- 先初始化自己的存储属性
- 再调
super.init
老 OC 人常在 init 里调 self.method,Swift 直接骂你:
python
'let' property is initialized late / self used before all stored properties are initialized
8. Block 和 Closure 捕获 self 的坑
OC:
ini
self.completion = ^{
[self reload];
};
内存泄漏,但"感觉没事"。
Swift:
ini
self.completion = {
self.reload()
}
编译器直接提醒你循环引用。
老 OC 人第一次写 Swift 会这样"修":
objectivec
self.completion = { [weak self] in
self?.reload()
}
然后又踩新坑:
self是Self?- 后面每一句都要解包
- 多次用
self?时,可能中途被释放
更稳的写法:
swift
self.completion = { [weak self] in
guard let self else { return }
self.reload()
}
9. NS_ENUM / NS_OPTIONS 不是同一个东西
OC:
ini
typedef NS_OPTIONS(NSUInteger, ViewState) {
ViewStateIdle = 1 << 0,
ViewStateLoading = 1 << 1,
};
Swift 里:
swift
struct ViewState : OptionSet {
let rawValue: UInt
static let idle = ViewState(rawValue: 1 << 0)
static let loading = ViewState(rawValue: 1 << 1)
}
老 OC 人写:
arduino
if state == .idle { } // ✅
if state & .loading { } // ❌ Swift 里不能这么写
Swift 里要:
javascript
if state.contains(.loading) { }
而普通 NS_ENUM 是真正的 enum,不能按位与。
OC:枚举和位掩码语法很接近
Swift:枚举是枚举,OptionSet 是 OptionSet
10. 以为 Swift 能像 OC 一样"动态调用"
OC:
ini
SEL sel = NSSelectorFromString(@"doSomething:");
[self performSelector:sel];
Swift 默认不支持这种玩法。
你写:
scss
let sel = Selector("doSomething")
perform(sel) // 很多时候根本不可用
更别说:
method_exchangeImplementationsNSClassFromString后直接调方法- KVC 随便写
"userName"拼字段名
Swift 的世界观是:
编译期能定的,就别拖到运行时
所以老 OC 人最容易幻灭的地方不是语法,而是:
原来我以前写的"黑魔法",在 Swift 里既不推荐,也不好使。
一句话总结
| OC 习惯 | Swift 现实 |
|---|---|
| nil 发消息不崩 | 可选值必须解包 |
| id 通吃 | 类型必须明确 |
| 引用共享 | 值类型拷贝 |
| 非 0 即真 | 只能是 Bool |
| 先声明后赋值 | 初始化必须完整 |
| block 强持 self | closure 要 capture list |
| 运行时动态调用 | 编译期静态检查 |
| 强转 NSMutableArray | Swift 没有"可变强转" |
| 枚举当位运算 | OptionSet 才是对的 |
| 后期再修 bug | 编译器现在就骂你 |