Swift 構造器中屬性初始化順序及安全檢查詳解

Swift初始化 編譯的過程的四個安全檢查

  1. 在調用父類初始化之前 必須給子類特有的屬性設置初始值, 只有在類的所有存儲屬性狀態(tài)都明確后, 這個對象才能被初始化
  2. 先調用父類的初始化方法, 再 給從父類那繼承來的屬性初始化值, 不然這些屬性值 會被父類的初始化方法覆蓋
  3. convenience 必須先調用 designated 初始化方法, 再 給屬性初始值. 不然設置的屬性初始值會被 designated 初始化方法覆蓋
  4. 在第一階段完成之前, 不能調用實類方法, 不能讀取屬性值, 不能引用self

一般情況下, 我們能很好的根據這些原則寫好構造器, 但是偶爾還是有些不太理解的事情發(fā)生. 以下示例:

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方法中打斷點, 查看調用堆棧, 發(fā)現(xiàn)其實際調用為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作為參數(shù)使用, 違反了安全檢查的第四條, 引用了self

疑問:
既然在第一階段完成前不能引用self, 那為什么property為存儲屬性的時候可以在第一階段使用selfself.property = "valueString"?

Swift的編譯過程如下:

swift編譯流程.jpg

我們可以將Swift語言轉為SIL再進行分析, 為了方便分析構造器中屬性初始化和構造器外屬性賦值的區(qū)別, 將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編譯流程, 參考: http://www.itdecent.cn/p/c27d312bccea

另: 在指定構造器中, 先初始化本類所有的屬性, 再調用super.init(). 在便利構造器中, 需要先調用指定構造器, 再修改屬性值. 即實例的創(chuàng)建是在指定構造器中一開始完成的. 參考BBB.init()中部分代碼:

%1 = alloc_stack $BBB, let, name "self"         // users: %17, %2, %19, %20
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容