Swift知识点14 - 构造过程

包括设置实例中每个存储型属性的初始值和执行其他必须的设置或初始化工作。
Swift 的构造器无需返回值,它们的主要任务是保证新实例在第一次使用前完成正确的初始化。

所有存储型属性必须设置默认值。当你为存储型属性设置默认值或者在构造器中为其赋值时,它们的值是被直接设置的,不会触发任何属性观察者。

如果一个属性总是使用相同的初始值,那么为其设置一个默认值比每次都在构造器中赋值要好。两种方法的效果是一样的,只不过使用默认值让属性的初始化和声明结合得更紧密。使用默认值能让你的构造器更简洁、更清晰,且能通过默认值自动推导出属性的类型;同时,它也能让你充分利用默认构造器、构造器继承等特性,后续章节将讲到。

可选属性类型

可选类型的属性会自动初始化为nil

构造过程中的常量属性

你可以在构造过程中的任意时间点给常量属性指定一个值,只要在构造过程结束时是一个确定的值。一旦常量属性被赋值,它将永远不可更改。
常量属性只能在构造过程中更改,子类无法更改。

默认构造器

如果结构体或类的所有属性都有默认值,同时没有自定义的构造器,那么 Swift 会给这些结构体或类提供一个默认构造器(default initializers)。这个默认构造器将简单地创建一个所有属性值都设置为默认值的实例。
而结构体会获得逐一成员构造器。

值类型的构造器代理

构造器可以通过调用其它构造器来完成实例的部分构造过程。这一过程称为构造器代理,它能避免多个构造器间的代码重复。

对于值类型,你可以使用 self.init 在自定义的构造器中引用相同类型中的其它构造器。并且你只能在构造器内部调用 self.init。

值类型,如果自定义了一个构造器,则默认的构造器不能再使用,如果想自定义构造器和默认构造器都能够同时使用的话,在扩展中定义自定义构造器即可。

类的构造过程
  • 指定构造器和便利构造器

  • 指定构造器
    一个指定构造器将初始化类中提供的所有属性,并根据父类链往上调用父类合适的构造器来实现父类的初始化。

每一个类都必须至少拥有一个指定构造器。在某些情况下,许多类通过继承了父类中的指定构造器而满足了这个条件。

  • 便利构造器
    便利构造器是比较次要的辅助型的构造器。可以定义便利构造器,调用指定构造器,来为其参数提供默认值。

使用语法:
指定构造器:

init(parameters) {
    statements
}

便利构造器:

convenience init(parameters) {
    statements
}
类的构造器代理规则
  1. 指定构造器必须调用其直接父类的指定构造器。
  2. 便利构造器必须调用同类中定义的其他构造器。
  3. 便利构造器必须调用指定构造器。
initializerDelegation01_2x.png

指定构造器必须向上代理,便利构造器必须横向代理。

两段式构造过程

第一阶段:类中每一个存储属性赋一个值
第二阶段:给每个类一次机会,进一步的定制存储属性。

安全检查:

Swift 编译器将执行 4 种有效的安全检查,以确保两段式构造过程不出错地完成:

  • 安全检查 1
    指定构造器必须保证他所在类的所有属性被初始化完成。之后才能将其他初始化任务向上代理给父类的构造器。
  • 安全检查2
    指定构造器必须在继承的属性设置之前向上代理调用父类构造器,如果没这么做,继承的属性设置的新值将被父类的构造器所覆盖。
  • 安全检查3
    便利构造器必须为任意属性赋新值之前代理调用同一类中的其他构造器,如果没这么做,便利构造器属性赋值将会被其他构造器所覆盖。
  • 安全检查4
    构造器在第一阶段构造完成之前,不能调用任何实例方法,不能读取任何属性的值,不能引用self作为一个值。

第一阶段完成之后,类实例才完全有效,才能去访问属性和调用方法。

过程:
指定构造器初始化类所有属性 - 向上代理父类的构造器进行初始化 - 继承的属性的初始化 - 便利构造器代理同意类中的其他构造器 - 便利构造器初始化其他属性 == 第一阶段实例构造完成。

阶段1:

  • 某个指定构造器或便利构造器被调用。
  • 完成新实例内存的分配,此时内存还没有被初始化。
  • 指定构造器确定所在类的所有存储属性都已被赋值,存储属性所属内存都已完成初始化。
  • 指定构造器将调用父类的构造器,完成父类属性的初始化。
  • 这个调用父类构造器的过程沿着构造器链一直往上执行,直到到达构造器的最顶部。
  • 当到达了构造器的最顶部,且已确保所有梳理包含的存储属性都已被赋值,这个实例的内存被完全初始化。

阶段2:

  • 从顶部构造器一直往下,每个构造器链中类的指定构造器都会进一步的定制实例。构造器此时可以访问self,访问属性和调用方法。
  • 最终任意构造器链中的便利构造器有机会定制实例和访问self。

总结:
对于指定构造器,先给类的存储属性赋值,再调用父类的指定构造器,再给类的存储属性或者继承的属性再次初始化的机会,然后可以self访问类的属性和调用方法。
对于便利构造器,先调用同类的置顶构造器,然后为类的存储属性定制初始化,然后就可以以self访问类属性和调用类方法。

构造器的继承和重写

子类不会继承父类的构造器,子类可以重写父类的构造器。
子类可以在初始化时,修改父类继承的变量属性,不能修改继承的常量属性。

构造器的自动继承

默认情况下是不会继承父类构造器的,但是如果为类的所有属性提供了默认值,则会自动继承父类的构造器:

  1. 如果子类没有定义指定构造器,则自动继承父类所有的指定构造器。
  2. 如果子类提供了父类所有指定构造器的实现 - 无论是通过规则1继承过来的,还是提供了自定义实现 - 都将自动继承父类所有的便利构造器。
可失败构造器

init?

可失败构造器的参数名和参数类型,不能与其它非可失败构造器的参数名,及其参数类型相同。

可失败构造器会创建一个类型为自身类型的可选类型的对象。你通过 return nil 语句来表明可失败构造器在何种情况下应该 “失败”。

严格来说,构造器都不支持返回值。因为构造器本身的作用,只是为了确保对象能被正确构造。因此你只是用 return nil 表明可失败构造器构造失败,而不要用关键字 return 来表明构造成功。

struct Animal {
    let species: String
    init?(species: String) {
        if species.isEmpty {
            return nil
        }
        self.species = species
    }
}
枚举类型的可失败构造器

你可以通过一个带一个或多个参数的可失败构造器来获取枚举类型中特定的枚举成员。如果提供的参数无法匹配任何枚举成员,则构造失败。

目的:获取枚举成员

enum TempUnit {
    case Kelvin, Celsius, Fahrenheit
    
    init?(symbol: Character) {
        switch symbol {
        case "K":
            self = .Kelvin
        case "C":
            self = .Celsius
        case "F":
            self = .Fahrenheit
        default:
            return nil
        }
    }
}
带原始值的枚举可失败构造器

带原始值的枚举类型会自带一个可失败构造器 init?(rawValue:),该可失败构造器有一个名为 rawValue 的参数,其类型和枚举类型的原始值类型一致,如果该参数的值能够和某个枚举成员的原始值匹配,则该构造器会构造相应的枚举成员,否则构造失败。

构造失败的传递

类,结构体,枚举的可失败构造器可以横向代理到同类型中的其他可失败构造器。类似的,子类的可失败构造器也能向上代理到父类的可失败构造器。

无论是向上代理还是横向代理,如果你代理到的其他可失败构造器触发构造失败,整个构造过程将立即终止,接下来的任何构造代码不会再被执行。

可失败构造器也可以代理到其它的非可失败构造器。通过这种方式,你可以增加一个可能的失败状态到现有的构造过程中。

class Product {
    let name: String
    init?(name: String) {
        if name.isEmpty {
            return nil
        }
        self.name = name
    }
}

class CartItem: Product {
    let quatity: Int
    init?(name: String, quatity: Int) {
        if quatity < 1 {
            return nil
        }
        self.quatity = quatity
        super.init(name: name)
    }
}
重写一个可失败构造器

你可以在子类中重写父类的可失败构造器。或者你也可以用子类的非可失败构造器重写一个父类的可失败构造器。这使你可以定义一个不会构造失败的子类,即使父类的构造器允许构造失败。

注意,当你用子类的非可失败构造器重写父类的可失败构造器时,向上代理到父类的可失败构造器的唯一方式是对父类的可失败构造器的返回值进行强制解包。

你可以用非可失败构造器重写可失败构造器,但反过来却不行。

init!可失败构造器

通常来说我们通过在 init 关键字后添加问号的方式(init?)来定义一个可失败构造器,但你也可以通过在 init 后面添加惊叹号的方式来定义一个可失败构造器(init!),该可失败构造器将会构建一个对应类型的隐式解包可选类型的对象。

你可以在 init? 中代理到 init!,反之亦然。你也可以用 init? 重写 init!,反之亦然。你还可以用 init 代理到 init!,不过,一旦 init! 构造失败,则会触发一个断言。

必要构造器

在类的构造器前添加 required 修饰符表明所有该类的子类都必须实现该构造器:

class SomeClass {
    required init() {
        // 构造器的实现代码
    }
}

在子类重写父类的必要构造器时,必须在子类的构造器前也添加 required 修饰符,表明该构造器要求也应用于继承链后面的子类。在重写父类中必要的指定构造器时,不需要添加 override 修饰符:

class SomeSubclass: SomeClass {
    required init() {
        // 构造器的实现代码
    }
}

如果子类继承的构造器能满足必要构造器的要求,则无须在子类中显式提供必要构造器的实现。

通过闭包或者函数设置属性的默认值

如果某个存储型属性的默认值需要一些定制或者设置,可使用闭包或者全局函数为其设置默认值。
每当所造类型新实例被创建时,对应的闭包或者函数就会被调用,而他们的返回值会当做默认值付给属性。

这种类型的闭包或者函数通常会创建一个临时的跟属性类型一致的变量,然后修改他们的值,然后把这个临时变量,作为属性的默认值。

使用:

class SomeClass {
    let someProperty: SomeType = {
        // 在这个闭包中给 someProperty 创建一个默认值
        // someValue 必须和 SomeType 类型相同
        return someValue
    }()
}

闭包结尾的后面接了一对空括号,用来告诉Swift立即执行这个闭包,然后将闭包返回值作为属性的值,而不是将闭包本身赋值给属性。

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

推荐阅读更多精彩内容