Swift 构造器中属性初始化顺序及安全检查详解

Swift初始化 编译的过程的四个安全检查

  1. 在调用父类初始化之前 必须给子类特有的属性设置初始值, 只有在类的所有存储属性状态都明确后, 这个对象才能被初始化
  2. 先调用父类的初始化方法, 再 给从父类那继承来的属性初始化值, 不然这些属性值 会被父类的初始化方法覆盖
  3. convenience 必须先调用 designated 初始化方法, 再 给属性初始值. 不然设置的属性初始值会被 designated 初始化方法覆盖
  4. 在第一阶段完成之前, 不能调用实类方法, 不能读取属性值, 不能引用self

一般情况下, 我们能很好的根据这些原则写好构造器, 但是偶尔还是有些不太理解的事情发生. 以下示例:

class AAA {}

class BBB: AAA {
    var property:String {
        set {}
        get {
            return "tempString"
        }
    }
    
    override init() {
        self.property = "valueString"  // 报错'self' used in property access 'property' before 'super.init' call
        super.init()
    }
}

在上述代码中, self.property = "valueString" 一行报错: 'self' used in property access 'property' before 'super.init' call. 当然可以把这一行放在super.init()之后. 另外一种办法就是将计算属性property改为存储属性. 那么, 为什么存储属性可以计算属性就不行?

下面代码中, 在setter方法中打断点, 查看调用堆栈, 发现其实际调用为BBB.property.setter(newValue="valueString", self=0x00006000002ff370)

class AAA {}

class BBB: AAA {
    var property:String {
        set {
            // 打断点
        }
        get {
            return "tempString"
        }
    }
    
    override init() {
        super.init()
    }
}

let obj = BBB.init()
obj.property = "valueString"

可以观察到, self作为参数使用, 违反了安全检查的第四条, 引用了self

疑问:
既然在第一阶段完成前不能引用self, 那为什么property为存储属性的时候可以在第一阶段使用selfself.property = "valueString"?

Swift的编译过程如下:

swift编译流程.jpg

我们可以将Swift语言转为SIL再进行分析, 为了方便分析构造器中属性初始化和构造器外属性赋值的区别, 将swift代码简化如下:

class AAA {
    init() {}
}

class BBB: AAA {
    
    var property:String

    override init() {
        self.property = "value1"
        super.init()
    }
    
    func setProperty() {
        self.property = "value2"
    }
}

let b = BBB.init()

转换命令如下:

swiftc -emit-sil main.swift

得到的部分SIL如下:

......
// BBB.init()
sil hidden @$s4test3BBBCACycfc : $@convention(method) (@owned BBB) -> @owned BBB {
// %0                                             // users: %9, %13, %2
bb0(%0 : $BBB):
  %1 = alloc_stack $BBB, let, name "self"         // users: %17, %2, %19, %20
  store %0 to %1 : $*BBB                          // id: %2
  %3 = string_literal utf8 "value1"               // user: %8
  %4 = integer_literal $Builtin.Word, 6           // user: %8
  %5 = integer_literal $Builtin.Int1, -1          // user: %8
  %6 = metatype $@thin String.Type                // user: %8
  // function_ref String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:)
  %7 = function_ref @$sSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfC : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %8
  %8 = apply %7(%3, %4, %5, %6) : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %11
  %9 = ref_element_addr %0 : $BBB, #BBB.property  // user: %10
  %10 = begin_access [modify] [dynamic] %9 : $*String // users: %11, %12
  store %8 to %10 : $*String                      // id: %11
  end_access %10 : $*String                       // id: %12
  %13 = upcast %0 : $BBB to $AAA                  // user: %15
  // function_ref AAA.init()
  %14 = function_ref @$s4test3AAACACycfc : $@convention(method) (@owned AAA) -> @owned AAA // user: %15
  %15 = apply %14(%13) : $@convention(method) (@owned AAA) -> @owned AAA // user: %16
  %16 = unchecked_ref_cast %15 : $AAA to $BBB     // users: %18, %21, %17
  store %16 to %1 : $*BBB                         // id: %17
  strong_retain %16 : $BBB                        // id: %18
  destroy_addr %1 : $*BBB                         // id: %19
  dealloc_stack %1 : $*BBB                        // id: %20
  return %16 : $BBB                               // id: %21
} // end sil function '$s4test3BBBCACycfc'

// String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:)
sil [serialized] [always_inline] [readonly] [_semantics "string.makeUTF8"] @$sSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfC : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String

// BBB.setProperty()
sil hidden @$s4test3BBBC11setPropertyyyF : $@convention(method) (@guaranteed BBB) -> () {
// %0                                             // users: %9, %8, %1
bb0(%0 : $BBB):
  debug_value %0 : $BBB, let, name "self", argno 1 // id: %1
  %2 = string_literal utf8 "value2"               // user: %7
  %3 = integer_literal $Builtin.Word, 6           // user: %7
  %4 = integer_literal $Builtin.Int1, -1          // user: %7
  %5 = metatype $@thin String.Type                // user: %7
  // function_ref String.init(_builtinStringLiteral:utf8CodeUnitCount:isASCII:)
  %6 = function_ref @$sSS21_builtinStringLiteral17utf8CodeUnitCount7isASCIISSBp_BwBi1_tcfC : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %7
  %7 = apply %6(%2, %3, %4, %5) : $@convention(method) (Builtin.RawPointer, Builtin.Word, Builtin.Int1, @thin String.Type) -> @owned String // user: %9
  %8 = class_method %0 : $BBB, #BBB.property!setter.1 : (BBB) -> (String) -> (), $@convention(method) (@owned String, @guaranteed BBB) -> () // user: %9
  %9 = apply %8(%7, %0) : $@convention(method) (@owned String, @guaranteed BBB) -> ()
  %10 = tuple ()                                  // user: %11
  return %10 : $()                                // id: %11
} // end sil function '$s4test3BBBC11setPropertyyyF'

......

在代码中有明显的注释

......
// BBB.init()
......
// BBB.setProperty()
......

BBB.init()部分, 对属性property赋值的代码为:

  .....
  %9 = ref_element_addr %0 : $BBB, #BBB.property  // user: %10
  %10 = begin_access [modify] [dynamic] %9 : $*String // users: %11, %12
  store %8 to %10 : $*String                      // id: %11
  end_access %10 : $*String 
  .....

可以看到是通过指针%9将字符串%8保存起来的. 再看BBB.setProperty()部分:

  ......
  %8 = class_method %0 : $BBB, #BBB.property!setter.1 : (BBB) -> (String) -> (), $@convention(method) (@owned String, @guaranteed BBB) -> () // user: %9
  %9 = apply %8(%7, %0) : $@convention(method) (@owned String, @guaranteed BBB) -> ()
  ......

%8为类方法BBB.property!setter, %0self, %7为赋值给属性的字符串.

很明显, 在构造器内对存储属性的初始化代码符合安全检查, 而构造器外对存储属性的赋值与计算属性属性都是通过setter方法. 虽然同样都是self.xxxx = ..... 的调用方式, 但是编译结果却不一样. 有兴趣的同学可以看一看计算属性的SIL, 此文仅抛砖引玉.

SIL官方文档: https://github.com/apple/swift/blob/master/docs/SIL.rst#witness-method
Swift编译流程, 参考: https://www.jianshu.com/p/c27d312bccea

另: 在指定构造器中, 先初始化本类所有的属性, 再调用super.init(). 在便利构造器中, 需要先调用指定构造器, 再修改属性值. 即实例的创建是在指定构造器中一开始完成的. 参考BBB.init()中部分代码:

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