Swift学习笔记(七)--构造器

构造器(Initializer)

其实我个人并不喜欢把init和deinit叫做构造器和析构器(真正应该对应constructor和destructor), 因为构造和析构做的更多的应该是分配和回收内存, initializer应该只是初始化器或初始化方法, 但是听起来很奇怪而且也很麻烦, 加上Swift并不需要我们自己来手动分配和回收内存, 所以还是以构造器和析构器来称呼这2个概念吧.

前言

这一段与ObjC有很大的区别, 比如之前我们是先调用父类的init再写自己的, 但是到了Swift里面, 我们却先初始化自己, 再初始化父类, 是相反的, 具体为什么, WWDC的视频里面有说, 总结起来有3点:

  1. Swift规定每个存储属性在被访问之前都必须得到初始化(可选属性如果是变量则会自动初始化为nil); // 1.30更新: 之前漏了 如果是变量, 因为常量是不会自动初始化为nil的, 即使是可选属性, 也要自己手动初始化.
  2. Swift中初始化是两段式的, 如果在第二阶段访问了一个方法, 如果这个方法被子类重载了, 而且方法里面访问了一个子类的属性, 就会出现和规则违背的地方;
  3. 那为什么ObjC可以呢? 原因是ObjC会为每一个属性默认设置为0或者nil, 但是Swift不会.
    综合上面3个原因, 导致出现了这个情况, 具体看一个例子吧
class Human {
    var age:Int
    init(){
        age = 0
        desc()  // 如果初始化一个Man 对象, 这边实际上会调用子类的desc方法, 
                //至于为什么还需要解释吗?
    }
    func desc(){
        print("Human")
    }
}

  class Man : Human {
    var prop: String
    override func desc(){
        print("Man \(prop)")
    }
    override init(){
        prop = "123"
        super.init()
    }
}

这就是为什么要先初始化自身, 再初始化父类的原因, 其中还涉及到很多别的细节, 例如继承而来的属性怎么算?还有具体里面的两段式是什么之后再说, 我们先按官方文档的节奏慢慢讲.

为存储属性设置初始值(Setting Initial Values for Stored Properties)

如前面所述类和结构体在创建实例的时候都必须给所有的存储属性设置初始值, 可以在构造器中设置, 也可以在声明属性的时候就给定. 注意: 给定初始值不会触发属性
监听.

构造器(initializers)

构造器会在创建实例的时候自动调用, 一般来说每个类都需要构造器, 无论是自己写的还是编译器为你生成的.
如果你为所有的属性都设置了默认值, 你可以选择不手动写一个构造器. 可以用init关键字来声明一个构造器(之前说过不需要func修饰), 括号里面可以加各种参数. 如:

init(){
}
init(name:String){
    self.name = name
}
构造参数

构造器和函数很相似, 但也意味着构造器和一般函数不同:

  1. 函数的第一个参数如果不手动写标签是会忽略掉的, 而构造器不会
  2. 函数可能有返回值, 但是构造器没有返回值(这就是另一个原因我不喜欢讲initializer为构造器的原因)
    以官方的结构体构造器为例子:
struct Celsius {
    var temperatureInCelsius: Double
    init(fromFahrenheit fahrenheit: Double) {
        temperatureInCelsius = (fahrenheit - 32.0) / 1.8
    }
    init(fromKelvin kelvin: Double) {
        temperatureInCelsius = kelvin - 273.15
    }
}
// 在使用构造器的时候, 即使只有一个参数也会有标签, 原因当然是构造器名字固定, 不能在后面加WithXXX了, 
// 如果不想要可以和函数一样, 用下划线(_)来隐藏
let boilingPointOfWater = Celsius(fromFahrenheit: 212.0)
let freezingPointOfWater = Celsius(fromKelvin: 273.15)

可以看到, Swift里面构造器都叫init, 而不是像ObjC一样, 还有initWithXXX什么的, 所以构造器(函数也是)是由名字和参数类型来统一标识的.

可选类型与常量

可选类型会被默认置为nil, 所以并不强制在初始化时赋值, 而常量则必须要初始化, 或者声明的时候给默认值.

默认构造器

在下面两种情况下Swift会自动生成一个默认构造器

  1. 结构体没有手动声明任何构造器(结构体很有意思, 如果全部的非可选属性都有默认值, 会生成两个默认构造器, 其根本规则还是和之前说的一样, 使用实例之前, 其存储属性必须全部得到初始化)
  2. 类里面每一个非可选属性都有了默认值, 且没有声明任何构造器

值类型的构造代理

所谓构造代理就是构造器可以调用别的构造器来辅助完成构造过程, 主要还是为了代码复用.
构造代理对值类型和引用类型来说不太一样, 值类型因为不支持继承, 所以只会用自己写的构造器来代理, 从而相对更简单. 类则会有从父类继承构造器的情况要考虑, 不过还是那句话, 所有存储属性在构造器中都完成初始化就可以.
值得一提的是, 如之前所说, 如果你自己实现了一个自定义的构造器, 那么默认的构造器将不会被合成, 如果你还是想要默认的构造器, 就用extension来写自定义构造器即可.
直接上例子吧:

struct Size {
    var width = 0.0, height = 0.0 // 全部有默认值, 会生成2个构造器
}
struct Point {
    var x = 0.0, y = 0.0
}
struct Rect {
    var origin = Point()
    var size = Size()
    init() {}
    init(origin: Point, size: Size) {
        self.origin = origin
        self.size = size
    }
    init(center: Point, size: Size) {
        let originX = center.x - (size.width / 2)
        let originY = center.y - (size.height / 2)
        self.init(origin: Point(x: originX, y: originY), size: size) // 构造器代理
    }
}

类继承与初始化

再次强调, 类的所有存储属性包括从父类继承而来的都必须在初始化的时候赋予初值.
Swift中定义了两种类型的构造器来确保所有的存储属性都获得初值, 即指派构造器和便利构造器.

指派构造器和便利构造器

指派构造器是主要的构造手段, 每个类至少要有1个, 意味着可以有多个, 但是多数情况下会是1个, 而便利构造器则没有要求.
对指派构造器的要求则是必须真正完成所有存储属性的初始化, 而便利构造器则要求最终要调用一个指派构造器, 这两者的关系比较像之前提到的构造代理. 在一些细节上会有区分, 先慢慢来看.
从语法上, 如果仅仅是init, 就会是指派构造器, 用convenience来修饰, 就是便利构造器. 如:

init(parameters) {
    statements
}
convenience init(parameters) {
    statements
}
类类型的构造代理

为了描述清楚指派构造器和便利构造器之间的关系, Swift有以下3个法则:
法则1:
一个指派构造器必须调用它的直接父类的指派构造器(至于为什么, 可以下面这个例子看看):

class Root {
    var a : Int
    init(){
        a = 0
    }
    convenience init(i:Int){
        self.init()
        a = i
        print(self)  // 打印两次, 第一次是Root, 第二次是Node
    }
}

class Node : Root {
    var b: Int
    override init(){
        b = 1
        super.init()
    }
}

var root = Root(i: 2)
var node = Node(i: 2)

法则2:
便利构造器必须调用类中其它的构造器

法则3:
便利构造器必须最终调用一个指派构造器(这个是必须的, 毕竟至于指派构造器才要求初始化所有存储属性)

Tip: 其实一开始很奇怪为什么一定要有个便利构造器的概念, 其实是因为Swift对初始化的检查非常的严格(稍候还会讲继承和二段式), 所以便利构造器就是为了不要反复进行检查, 一般普遍的做法是给传一些默认参数, 例如UIView中, 假如init(frame:CGRect)是指派构造器, 那么init()就可以定义为便利构造器, 直接调用self.init(frame:CGRectZero)即可

两段式初始化

Swift中类的初始化分为两段式:

  1. 第一阶段, 每个存储属性都赋予初值
  2. 对实例进行进一步操作(例如加个监听什么的)
    之所以有两段式其实还是为了确保开发者不会在属性未得到初始化就访问属性的情况.
    所以, 可以预见的是, 在一个稍微复杂的类中, 至少会天然出现2段截然不同的代码, 一段是不停地赋值, 一段是做一些其它的操作.
    同时, Swift的编译器会为两段式初始化做一系列的检查, 以确保初始化完成(这段比较重要, 所以直接全部翻译了):
    安全检查1:
    把构造过程交给父类前, 指派构造器必须保证声明的存储属性都已初始化了.
    如上所述, 一旦所有的存储属性的初始状态都已知之后, 这个对象的内存才被当做已初始化. 为了满足这个规则, 一个指派构造函数在移交给初始化链之前, 必须确定自己本类所有的属性都已经初始化

安全检查2:
一个指派构造器必须把继承属性赋值之后才能移交给父类初始化函数. 如果不这么做, 指派构造器的赋的新值就会被父类的初始化函数给覆盖

安全检查3
一个便利初始化函数在给任何属性赋值之前(包括自己类的属性), 必须调用其它的初始化函数. 如果不这么做, 便利初始化函数赋的值将会被指派初始化函数覆盖

安全检查4
初始化函数不能调用其它实例方法, 不能从实例属性中取值, 也不能用self, 直到第一阶段初始化完成(其实这里Swift做的不是特别严谨, 如果一个类没有父类, 那么即使还没有初始化完成也可以用self, 这里只能勉强解释为第一二阶段合二为一了.)

我们再来看看这两段初始化到底做了些什么事情:
阶段1:

  • 类的一个指派或便利初始化函数被调用
  • 类实例的内存被分配出来, 但是这块内存并没有被初始化
  • 一个类的指派初始化函数保证类所有的存储属性都有值. 到这一阶段, 存储属性的内存就已经初始化了
  • 这个指派构造器把初始化进程移交给父类初始化函数来对父类存储属性实现同样的操作
  • 这个过程一直沿着继承链持续下去, 直到达到继承链顶端
  • 到了继承链顶端, 并且最终父类保证所有的存储属性都有值之后, 实例的内存就被当做完全初始化了, 此时阶段1完成

阶段2:

  • 从继承链顶端倒回来(因为函数调用栈啊), 每一个指派初始化函数都可以进一步定制实例, 初始化函数至此可以访问self, 并且可以修改自己的属性, 调用实例方法, 等等
  • 最终, 调用链上的任意便利初始化方法都可以操作实例了, 也可以访问self.

构造器继承和重载

Swift不会像ObjC一样, 自动从父类那里继承父类的构造器, 因为Swift不像ObjC会给属性默认值, 如果继承下来的话, 可能会有属性没有被初始化. 当然只是说不会自动而已, 我们还是可以做一些事情来实现继承在下一节会细讲.
此外, 如果子类的指派构造器和父类相同, 也要用override来修饰. 但是, 如果子类的指派构造器与父类的便利构造器相同, 那么父类的便利构造器永远都不会被子类调用到(具体原因参考构造代理法则1), 所以这种情况是不需要写override的.
注意: 初始化时, 子类只可以修改继承的变量属性, 而不能修改继承的常量

自动构造器继承

如上一节稍微提过的, 虽然默认不能继承, 但是只要满足下面的2种情况就可以:

规则1:
子类没有定义任何的指派构造器, 那么就会自动从父类那里继承所有的指派构造器(例如, 所有的子类属性都有默认值)

规则2:
子类提供了全部的父类指派构造器而不是从情况1获得的, 即使是提供了一部分实现, 那么它将自动继承所有的父类便利构造器. (个人认为, 调用实现的那一个指派构造器的便利构造器都可以, 其余的不行可以更智能, 而且也不会出问题)

注意: 子类可以实现父类的指派构造器为自己的便利构造器, 从而满足规则2来获得父类的构造器. 如:

class Root {
    var a : Int
    var b: Int
    init(){
        self.a = 0
        self.b = 0
    }
// 2. 解开这段注释下面的node声明不满足规则1
//    init(str:String){   
//        self.a = Int(str)!
//        self.b = Int(str)!
//    }
    convenience init(i:Int){
        self.init()
        a = i
        b = i
    }
}

class Node : Root {
    var bb: Int
    override init() {
        bb = 0
        super.init()
    }
// 3. 解开这段注释将满足规则2
//    convenience override init(str:String){  
//        self.init()
//        
//    }
}

var node = Node(i: 3) // 1. 子类实现了父类的全部指派构造器, 满足规则1

指派构造器和便利构造器实例

文档给出了一个例子来进行讲解, 直接先看代码:

class Food {
    var name: String
    init(name: String) {
        self.name = name
    }
    convenience init() { // 便利构造器用: 给出默认参数
        self.init(name: "[Unnamed]")
    }
}
let namedMeat = Food(name: "Bacon")
let mysteryMeat = Food()

class RecipeIngredient: Food {
    var quantity: Int
    init(name: String, quantity: Int) {
        self.quantity = quantity
        super.init(name: name)
    }
    override convenience init(name: String) { // 重载父类的指派构造器, 满足规则2
        self.init(name: name, quantity: 1)
    }
}
let oneMysteryItem = RecipeIngredient() // 继承而来的构造器
let oneBacon = RecipeIngredient(name: "Bacon")
let sixEggs = RecipeIngredient(name: "Eggs", quantity: 6)

class ShoppingListItem: RecipeIngredient {
    var purchased = false
    var description: String {
        var output = "\(quantity) x \(name)"
        output += purchased ? " ✔" : " ✘"
        return output
    }
}
// 因为ShoppingListItem满足规则1, 所以全部继承下来了
var breakfastList = [
    ShoppingListItem(),
    ShoppingListItem(name: "Bacon"),
    ShoppingListItem(name: "Eggs", quantity: 6),
]

构造失败(Failable initializer)

有时候需要返回构造失败, 例如传入了不恰当的参数. 这种情况主要是引起调用方的注意, 在传入关键参数的时候要当心.
可失败的构造器声明也很简单, 用init?就可以的, 但是要注意, 可失败和不可失败的构造器不能有一样的参数类型列表, 否则就有二义性了.
构造失败, 自然就是返回nil了, 所以可失败的构造器返回值是Optional的, 在使用的时候要注意拆包.
需要注意的是, 值类型和引用类型在处理失败构造的时候有些许不一样, 先看值类型的:

struct Animal {
    let species: String
    init?(species: String) {
        if species.isEmpty { return nil }  // return nil 在任意位置都可以
        self.species = species
    }
}

enum TemperatureUnit {
    case Kelvin, Celsius, Fahrenheit
    init?(symbol: Character) {
        switch symbol {
        case "Z":
            return nil    // 写在哪里都是可以的
        case "K":
            self = .Kelvin
        case "C":
            self = .Celsius
        case "F":
            self = .Fahrenheit
        default:
            return nil
        }
    }
}
// 枚举还有另外一种写法, 因为没有可以通过rawValue来创建
enum TemperatureUnit: Character {
    case Kelvin = "K", Celsius = "C", Fahrenheit = "F"
}
TemperatureUnit(rawValue: "X")  // 这里返回nil

类的失败构造

上一节可以看到, 值类型的失败构造可以发生在初始化进程的任何点上. 但是对于类而言, 只有在所有的存储属性都赋予了初值之后才能出发失败构造. 例如:

class Product {
    let name: String!  // 如果不记得这里的感叹号代表什么就去看第一章
    init?(name: String) {
        self.name = name
        if name.isEmpty { return nil }
    }
}

其实个人不是很明白这里为什么要限定这么死, 但是官方文档也没有讲更多的信息了, 只能当做一项规定来看了.

构造失败的传递

类,结构体和枚举的构造失败是会传递到其它的构造器去的, 而且子类构造失败还会传递到父类的可失败构造器中去. 从而整个初始化进程都会直接失败掉, 不会再执行其它的代码了.
注意: 一个可失败的构造器也是可以调用非可失败构造器的. 通过这种方式, 我们可以在现有的构造进程中添加一个潜在失败状态.
以一个例子来讲解:

class CartItem: Product {
    let quantity: Int!
    init?(name: String, quantity: Int) {
        self.quantity = quantity
        super.init(name: name)
        if quantity < 1 { return nil }
    }
}
if let zeroShirts = CartItem(name: "shirt", quantity: 0) {
    print("Item: \(zeroShirts.name), quantity: \(zeroShirts.quantity)")
} else {
    print("Unable to initialize zero shirts")  // 这里被打印
}
// 同样的
if let oneUnnamed = CartItem(name: "", quantity: 1) {
    print("Item: \(oneUnnamed.name), quantity: \(oneUnnamed.quantity)")
} else {
    print("Unable to initialize one unnamed product") // 父类构造失败
}

重载可失败的构造器

和上面提到的其它构造器重载没有别的区别, 只是我们可以用一个不可失败的构造器来重载可失败的构造器, 但是不能反过来. 需要注意的是, 如果你重载了一个父类的可失败构造器, 记得要拆包. 例子:

class Fail{
    var a:Int
    init?(i:Int){
        a = i
        if i < 0 {return nil}
    }
}

class Error : Fail{
    var b: Int
    override init(i:Int){
        b = i
        super.init(i:abs(i))!  // 要加感叹号, 当然要确保父类可以构造成功
    }
}
init!可失败构造器

对于Optional type有两种形式, 一种是?, 一种则是!. 两者的具体区别在第一张的基础篇已经说过. 这里再强调一次吧. 用?的optional type, 在使用的时候需要拆包, 而用!的optional type可以直接使用, 就像ObjC里面的指针一样. 需要注意的是, 不管哪种形式, 都是需要判空的, 否则会出现crash.
所以在init?和init!的区别也在于此, 这两者在初始化的时候是可以互相代理的. 同时, 也可以在init里面把初始化交给init!代理, 只是这么做的话, 如果失败就会触发断言失败.

必需构造器(Required Initializers)

如果init用了required来修饰, 那么意味着子类必需要重载这个构造器.
不过值得一提的是, 如果你满足构造器继承的条件的话, 必需构造器也不是一定要实现的, 例如:

class Fail{
    var a:Int
    required init(){
        a = 0
    }
}

class Error : Fail{
    var b: Int = 1
}
var err = Error()
err.a  // 打印出0

用闭包或者函数来设置默认值

在设置存储属性默认值的时候, 可以用函数或者闭包来实现, 例如官方的两个例子:

// 闭包
class SomeClass {
    let someProperty: SomeType = {
        // 注意: 在这里不能用self, 更不能用其它的属性(即使它有默认值, 因为self还没准备好)或者该类的实例方法
        // 也就是说执行这段代码时, 初始化都还没有进行
        return someValue
    }()   // 最后要加上(), 不然就被当做是个闭包, 而不是这个闭包的结果了
}

// 函数, 这个例子是创建一个国际象棋的棋盘, 黑白间隔开的
struct Checkerboard {
    let boardColors: [Bool] = {
        var temporaryBoard = [Bool]()
        var isBlack = false
        for i in 1...10 {
            for j in 1...10 {
                temporaryBoard.append(isBlack)
                isBlack = !isBlack
            }
            isBlack = !isBlack
        }
        return temporaryBoard
    }()
    func squareIsBlackAtRow(row: Int, column: Int) -> Bool {
        return boardColors[(row * 10) + column]
    }
}

至此, 初始化就结束了, 这一章总结的不好, 基本都是按照原文翻过来的, 因为的确是很多规则, 没法解释, 就是这么规定的. 如果一定要总结的话, 就是之前反复提到的那句话, Swift要求任何实例使用之前都要先初始化完成. 所有的那些规则基本上都是围绕着这句话进行的.
官网上有一些图表, 有兴趣可以去看看官方文档

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容