可失败初始化器
本教程共 93 篇 · 第 65 篇 · 更新于 2026-08-08 · 约 8 分钟阅读
本节目标:学完你能用 init? 定义可能失败的初始化器,在参数非法或资源缺失时返回 nil,并正确处理返回的可选实例。
有些类型的初始化不一定总能成功。比如用户传了空字符串当动物种类、用字符去匹配不存在的温度单位、或者打开的文件其实不存在。这种”可能建不出实例”的情况,Swift 用可失败初始化器(failable initializer)来表达。它在 init 后面加个问号变成 init?,失败时写 return nil。这一章把它讲清楚。
1-1 基本写法 init?
普通初始化器保证一定能造出实例。可失败初始化器则可能在中途宣告失败:它创建的是该类型的可选值,失败时返回 nil。在初始化器体内,用 return nil 触发失败。
struct Animal {
let species: String
init?(species: String) {
if species.isEmpty { return nil }
self.species = species
}
}
Animal 要求 species 不能是空字符串。如果传了空串,初始化器直接 return nil,实例没建出来。调用它得到的是可选类型 Animal?,得解包才能用:
let giraffe = Animal(species: "长颈鹿")
if let g = giraffe {
print("创建了动物:\(g.species)")
}
let anonymous = Animal(species: "")
if anonymous == nil {
print("空字符串无法初始化")
}
注意一个容易绕的点:return nil 表达”初始化失败”,但你不能用 return 去表达”成功”——初始化器本来就不返回值,return nil 只是给失败一个出口。严格说,初始化器做的是”保证 self 被正确初始化”,return nil 是中断这条保证的信号。
1-2 不能同名同参数并存的坑
同一类型里,你不能有一个可失败初始化器和一个不可失败初始化器,二者参数名和类型完全一样。Swift 不知道该调哪个。所以设计初始化器时,要么可失败要么不可失败,别让它们撞签名。这和函数重载不是一回事:重载靠参数不同来区分,而这里参数一模一样,仅仅是”能不能失败”不同。
1-3 枚举里的可失败初始化器
枚举常配合可失败初始化器,根据参数挑一个合适的 case,挑不到就失败。
enum TemperatureUnit {
case kelvin, celsius, fahrenheit
init?(symbol: Character) {
switch symbol {
case "K": self = .kelvin
case "C": self = .celsius
case "F": self = .fahrenheit
default: return nil
}
}
}
传 “K""C""F” 能选中对应 case;传别的字符就 return nil。这比在外面写一堆 if 判断优雅得多:用初始化器的失败机制天然表达”不合法输入”。
带原始值的枚举更省事——Swift 自动送你一个 init?(rawValue:),无需自己写:
enum TemperatureUnit: Character {
case kelvin = "K", celsius = "C", fahrenheit = "F"
}
let f = TemperatureUnit(rawValue: "F") // 成功
let x = TemperatureUnit(rawValue: "X") // nil
rawValue: 就是系统合成的可失败初始化器,找得到匹配的原始值就成功,否则返回 nil。
1-4 失败的传递
可失败初始化器能委托给同类型的另一个可失败初始化器,子类也能委托给父类的可失败初始化器。一旦委托到的那个失败了,整个初始化立刻失败,后面的代码不再执行。
class Product {
let name: String
init?(name: String) {
if name.isEmpty { return nil }
self.name = name
}
}
class CartItem: Product {
let quantity: Int
init?(name: String, quantity: Int) {
if quantity < 1 { return nil }
self.quantity = quantity
super.init(name: name)
}
}
CartItem 先查 quantity 是否小于 1,不合法就直接 return nil,连父类都不用调;父类 Product 再查 name 是否为空。只要任一环失败,整条初始化链即刻终止。你也可以让可失败初始化器去委托一个不可失败初始化器——当你要在原本不会失败的过程里额外加一道失败条件时会用到。
1-5 用非失败初始化器重写失败的
子类可以重写父类的可失败初始化器,甚至把它重写成不可失败的版本。比如父类允许 name 失败(空串返回 nil),子类却规定”空串就给个默认名”,于是初始化永远不会失败:
class Document {
var name: String?
init() {}
init?(name: String) {
if name.isEmpty { return nil }
self.name = name
}
}
class AutomaticallyNamedDocument: Document {
override init(name: String) {
super.init()
if name.isEmpty {
self.name = "[未命名]"
} else {
self.name = name
}
}
}
AutomaticallyNamedDocument 把父类的 init?(name:) 重写成了不可失败的 init(name:),因为空串在它这里被兜底成”[未命名]“,不再需要失败。注意方向:可以用非失败重写失败,但不能反过来用失败重写字非失败。若子类非失败初始化器要调父类的失败初始化器,得强制解包:super.init(name: "[未命名]")!。
1-6 init! 隐式解包可选
除了 init? 返回普通可选,你还能写 init! 返回隐式解包可选(implicitly unwrapped optional)。两者可以互相委托、互相重写。当你非常确定初始化在调用处不会失败、又想省去反复解包时可用 init!,但它和普通的隐式解包可选一样有风险:一旦真失败,强制解包会崩。除非有充分把握,否则优先用 init? 更安全。
1-7 小结
可失败初始化器用 init? 声明,用 return nil 表达失败,调用得到的是可选实例,需要解包。它适用于参数非法、资源缺失等”建不出对象”的场景;枚举用它按参数选 case,带原始值的枚举还白送 init?(rawValue:)。失败会在委托链上立即传播;子类还能把父类失败初始化器重写为不可失败版本。把 init? 当成”可能返回 nil 的构造”,你就能优雅处理各种不合法的创建请求。
1-8 可失败初始化器用在哪
什么时候该用 init??一个简单判据:如果创建实例的前提条件可能在运行时不成立,就用可失败初始化器。比如用字符串解析成某种格式、用文件路径打开资源、用外部数据构造模型。这些条件不在编译期可控,只能运行时判断,失败就用 return nil 表达。反之,如果参数永远合法、或非法时你希望直接崩溃以暴露 bug,那就用普通初始化器。把”可能失败”显式建模成 init?,调用方就会被迫处理 nil,比悄悄用默认值更诚实。
1-9 和可选链、guard 的配合
拿到 init? 返回的可选实例后,常见处理是 if let 解包,或用 guard let 提前退出。它也能直接参与可选链:比如 Animal(species: s)?.species 在初始化失败时整条链返回 nil。配合可选链,可失败初始化器能写出很流畅的”可能失败就短路”的代码。再提醒一次:同一签名下 init? 和 init 不能共存,设计初始化器时要先定好”这个构造到底会不会失败”。如果你不确定该不该失败,先问:失败的代价是什么?如果失败只是”少建一个对象”,用 init? 最安全;如果失败意味着程序状态不对,那也许该让它在开发期就崩出来,方便及早发现。