首页 / Swift 编程语言教程 / 可失败初始化器

Swift 编程语言教程

可失败初始化器

本教程共 93 篇 · 第 65 篇 · 更新于 2026-08-08 · 约 8 分钟阅读

SwiftSwift 编程语言教程可失败初始化器init?return nil可选实例初始化失败

本节目标:学完你能用 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? 最安全;如果失败意味着程序状态不对,那也许该让它在开发期就崩出来,方便及早发现。