Swift 屬性

Swift 屬性

Swift中屬性主要分為存儲屬性拷泽、計算屬性碘梢、延遲存儲屬性咬摇、類型屬性這四種,并且Swift還提供了屬性觀察者煞躬,以便開發(fā)者能在屬性的改變前后進行觀察肛鹏。下面我就來一一探索逸邦。

1. 存儲屬性

存儲屬性顧名思義,就是需要存儲的一種屬性在扰,既然存儲就要占用內存空間缕减,下面我們通過一個例子來對其進行解釋。

示例代碼:

class Teacher {
    let age: Int = 18
    var age2: Int = 100
}

let t = Teacher()

我們通過我的上一篇文章對Swift的類和對象的探索芒珠,我們可以知道Swift對象的存儲前16個字節(jié)是metadatarefCounts

1.1 通過lldb查看存儲屬性

下面我們就通過lldb調試來查看一下示例代碼中t這個對象的內存結構桥狡。

16086182160555.jpg

通過lldb調試我們可以看到,我們的兩個age的值都存儲在了連續(xù)的對象存儲空間中(注:這里是16進制)

1.2 通過SIL文件查看存儲屬性

我們使用如下命令將我們的swift文件編譯成sil文件皱卓,并打開(注:如果打開失敗就設置一下用VSCode打開sil類型的文件就可以了)

swiftc -emit-sil main.swift >> ./main.sil && open main.sil

1.2.1 類

編譯后的SIL代碼:

class Teacher {
  @_hasStorage @_hasInitialValue final let age: Int { get }
  @_hasStorage @_hasInitialValue var age2: Int { get set }
  @objc deinit
  init()
}

sil代碼中我們可以看到屬性的前面加上了@_hasStorage關鍵字裹芝,這是存儲屬性獨有的,對應的是計算屬性娜汁,就沒有這個關鍵字嫂易。

1.2.2 get&set

get方法及分析:

// Teacher.age.getter
sil hidden [transparent] @main.Teacher.age.getter : Swift.Int : $@convention(method) (@guaranteed Teacher) -> Int {
// %0 "self"                                      // users: %2, %1
bb0(%0 : $Teacher):
  debug_value %0 : $Teacher, let, name "self", argno 1 // id: %1
  // 找出實例內部物理實例變量的地址
  %2 = ref_element_addr %0 : $Teacher, #Teacher.age // user: %3
  // 開始訪問內存
  %3 = begin_access [read] [dynamic] %2 : $*Int   // users: %4, %5
  // 內存中的值加載到%4虛擬寄存器
  %4 = load %3 : $*Int                            // user: %6
  // 結束內存訪問
  end_access %3 : $*Int                           // id: %5
  // 返回獲取到的值
  return %4 : $Int                                // id: %6
} // end sil function 'main.Teacher.age.getter : Swift.Int'

set方法及分析:

// Teacher.age.setter
sil hidden [transparent] @main.Teacher.age.setter : Swift.Int : $@convention(method) (Int, @guaranteed Teacher) -> () {
// %0 "value"                                     // users: %6, %2
// %1 "self"                                      // users: %4, %3
bb0(%0 : $Int, %1 : $Teacher):
  debug_value %0 : $Int, let, name "value", argno 1 // id: %2
  debug_value %1 : $Teacher, let, name "self", argno 2 // id: %3
  // 找出實例內部物理實例變量的地址
  %4 = ref_element_addr %1 : $Teacher, #Teacher.age // user: %5
  // 開始訪問內存
  %5 = begin_access [modify] [dynamic] %4 : $*Int // users: %6, %7
  // 將第一個參數(shù)%0的值存儲到%5
  store %0 to %5 : $*Int                          // id: %6
  // 停止內存訪問
  end_access %5 : $*Int                           // id: %7
  %8 = tuple ()                                   // user: %9
  return %8 : $()                                 // id: %9
} // end sil function 'main.Teacher.age.setter : Swift.Int'

1.3 小結

打印內存占用大小:

16086275671125.jpg

對象的內存結構

16086252349746.jpg

這個32其實就是8+8+8+8得到的掐禁,在Swift中對象的本質是HeapObject內部有兩個屬性metadata占用8字節(jié)怜械,refCounts占用8字節(jié),SwiftInt實際是個結構體傅事,占用8自己缕允,這里兩個Int就是16字節(jié)。

綜上所述:

  1. 存儲屬性是占用對象的分配的內存空間

2. 計算屬性

計算屬性享完,指的就是需要通過計算得到一些想要的值灼芭,那么對于屬性來說計算就是getset有额,下面我們就通過一個舉例來說明一下計算屬性般又。

示例代碼:

class Teacher {
    var age: Int {
        get {
            return 18
        }
        set {
            age = newValue
        }
    }
}
let t = Teacher()
t.age = 10

print("end")

對于計算屬性就是屬性自己實現(xiàn)getset方法,但是示例代碼中這樣寫是不對的巍佑,首先會有警告:

16086261150433.jpg

??警告的意思就是會自己調用自己茴迁,如果運行這段代碼就會造成無限遞歸調用而崩潰:

16086261800055.jpg

2.1 計算屬性的使用

在上面的介紹中,是我們通常對屬性的getset寫法萤衰,既然這樣不對堕义,那么計算屬性該怎么用呢?下面我們在來看一個正方形的示例脆栋。

實例代碼:

class Square{
    var width: Double = 10.0
    var area: Double{
        get{
            //這里的return可以省略倦卖,編譯器會自動推導
            return width * width
        }
        set{
            width = sqrt(newValue)
        }
    }
}

let s = Square()

s.width = 8.0
print(s.area)

s.area = 81.0
print(s.width)


print("end")

打印結果:

16086265899242.jpg

這里我們可以看到可以通過對計算屬性的getset方法內的一些操作,實現(xiàn)對其他屬性的修改椿争。

2.3 通過lldb查看計算屬性的存儲

16086299169390.jpg

這里的0x4024000000000000就是Swift中對Double類型的存儲怕膛,這里我們對width的賦值為10。

2.2 通過SIL文件查看計算屬性

按照上面所說的命令秦踪,我們在編譯一個sil文件褐捻,sil代碼如下:

2.2.1 類

class Square {
  @_hasStorage @_hasInitialValue var width: Double { get set }
  var area: Double { get set }
  @objc deinit
  init()
}

我們看到計算屬性area沒有了@_hasStorage的修飾掸茅。

2.2.2 get&set

get方法及分析:

// Square.area.getter
sil hidden @main.Square.area.getter : Swift.Double : $@convention(method) (@guaranteed Square) -> Double {
// %0 "self"                                      // users: %5, %4, %3, %2, %1
bb0(%0 : $Square):
  debug_value %0 : $Square, let, name "self", argno 1 // id: %1
  // width get 方法的函數(shù)地址
  %2 = class_method %0 : $Square, #Square.width!getter : (Square) -> () -> Double, $@convention(method) (@guaranteed Square) -> Double // user: %3
  // 調用函數(shù),參數(shù)是%0-self柠逞,返回值width
  %3 = apply %2(%0) : $@convention(method) (@guaranteed Square) -> Double // user: %6
  // width get 方法的函數(shù)地址
  %4 = class_method %0 : $Square, #Square.width!getter : (Square) -> () -> Double, $@convention(method) (@guaranteed Square) -> Double // user: %5
  // 調用函數(shù)昧狮,參數(shù)是%0-self,返回值width
  %5 = apply %4(%0) : $@convention(method) (@guaranteed Square) -> Double // user: %7
  // 轉成 結構體
  %6 = struct_extract %3 : $Double, #Double._value // user: %8
  // 轉成 結構體
  %7 = struct_extract %5 : $Double, #Double._value // user: %8
  // 計算板壮,并將返回值存儲到%8
  %8 = builtin "fmul_FPIEEE64"(%6 : $Builtin.FPIEEE64, %7 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: %9
  // 將%8 的值包裝到%9
  %9 = struct $Double (%8 : $Builtin.FPIEEE64)    // user: %10
  // 返回
  return %9 : $Double                             // id: %10
} // end sil function 'main.Square.area.getter : Swift.Double'

set方法及分析:

// Square.area.setter
sil hidden @main.Square.area.setter : Swift.Double : $@convention(method) (Double, @guaranteed Square) -> () {
// %0 "newValue"                                  // users: %5, %2
// %1 "self"                                      // users: %7, %6, %3
bb0(%0 : $Double, %1 : $Square):
  debug_value %0 : $Double, let, name "newValue", argno 1 // id: %2
  debug_value %1 : $Square, let, name "self", argno 2 // id: %3
  // function_ref sqrt
  // 獲取sqrt函數(shù)的地址
  %4 = function_ref @sqrt : $@convention(c) (Double) -> Double // user: %5
  // 調用sqrt函數(shù)
  %5 = apply %4(%0) : $@convention(c) (Double) -> Double // user: %7
  // 獲取width set 方法的函數(shù)地址
  %6 = class_method %1 : $Square, #Square.width!setter : (Square) -> (Double) -> (), $@convention(method) (Double, @guaranteed Square) -> () // user: %7
  // 調用width set 方法逗鸣,傳入?yún)?shù)%5---sqrt方法的返回值,%1---self
  %7 = apply %6(%5, %1) : $@convention(method) (Double, @guaranteed Square) -> ()
  %8 = tuple ()                                   // user: %9
  return %8 : $()                                 // id: %9
} // end sil function 'main.Square.area.setter : Swift.Double'

2.3 小結

打印內存占用大写戮:

16086288542018.jpg

這里的24 就是8+8+8得來的慕购,所以可以得到一個結論就是計算屬性不占用對象分配的內存空間。

綜上所述:

  1. 計算屬性的本質就是getset方法
  2. 計算屬性不占用對象的內存空間

3. 延遲存儲屬性

延遲存儲屬性就是我們常說的懶加載茬底,懶加載就是使用的時候在加載沪悲,那么我們就來看看Swift中這個延遲是什么樣的。舉個例子:

class Teacher {
    lazy var age:Int = 10
}

let t = Teacher()

print(t.age)

print("end")

3.1 通過lldb查看延遲存儲屬性在內存中的存儲

延遲存儲屬性實際上還是存儲屬性阱表,既然是存儲屬性肯定是占用對象分配的內存的殿如,下面我們通過lldb來看看延遲存儲屬性在內存中是怎樣存儲的。

lldb調試:

16086939120494.jpg

在這個截圖中第一段x/8gx打印的是第一個斷點時的數(shù)據(jù)最爬,第二個是打印的第二斷點時的數(shù)據(jù)涉馁。

  • 通過以上的數(shù)據(jù)我們可以看到,延遲存儲屬性在不使用的時候是沒有存儲值的爱致,雖然在代碼的初始化中我們給它賦了個初始值10
  • 當我們使用了這個屬性后烤送,在內存中打印的時候就可以在看到這個值出現(xiàn)在了內存段中,這里的10是就是16進制的0xa

3.2 通過sil代碼進一步探索延遲存儲屬性

修改Swift為如下糠悯,這里我們即使用set也使用get

class Teacher {
    lazy var age:Int = 10
}

let t = Teacher()

t.age = 18
print(t.age)

print("end")

使用如下命令將我們的main.swift文件編譯成sil文件帮坚,這里還刪除了原有的sil文件,并通過xcrun swift-demangle命令還原sil代碼中的混淆互艾。

rm -rf main.sil && swiftc -emit-sil main.swift | xcrun swift-demangle >> ./main.sil && open main.sil

3.2.1 類

class Teacher {
  lazy var age: Int { get set }
  @_hasStorage @_hasInitialValue final var $__lazy_storage_$_age: Int? { get set }
  @objc deinit
  init()
}

我們可以看到在sil代碼中Teacher這個類中的延遲存儲屬性:

  1. 有一個和swift代碼中書寫差不多的屬性试和,帶有getset方法
  2. 在存儲屬性的基礎上對var變量增加了final修飾
  3. 生成了$__lazy_storage_$_age這個可選(optional)變量

3.2.2 main 函數(shù)

16087064255177.jpg

針對main函數(shù)中的主要調用在截圖中加了注釋:

  1. 首先創(chuàng)建變量t的內存地址方到%3
  2. 獲取tmetadata
  3. 獲取__allocating_init的函數(shù)地址,用于對象的初始化
  4. 調用__allocating_init函數(shù)傳入metadata初始化變量t
  5. 將初始化的結果存如%3的地址中
  6. %3這個內存地址中的t對象的數(shù)據(jù)加載到%8
  7. 將要賦值的18這個值放到%9這個虛擬寄存器中
  8. 通過%9中的值初始化一個Int類型的的結構體放到%10
  9. age屬性的set方法放到%11
  10. 調用ageset方法纫普,傳入%10--18%8--self
  11. %8一致阅悍,將%3這個內存地址中的t對象的數(shù)據(jù)加載到%19
  12. ageget方法放到%20
  13. 調用ageget方法,傳入%19--self參數(shù)昨稼,并將返回值存儲到%21

3.2.3 set方法

16087103442712.jpg
// Teacher.age.setter
sil hidden @main.Teacher.age.setter : Swift.Int : $@convention(method) (Int, @guaranteed Teacher) -> () {
// %0 "value"                                     // users: %4, %2
// %1 "self"                                      // users: %5, %3
bb0(%0 : $Int, %1 : $Teacher):
  debug_value %0 : $Int, let, name "value", argno 1 // id: %2
  debug_value %1 : $Teacher, let, name "self", argno 2 // id: %3
  // 將傳入的value值%0存儲到%4中
  %4 = enum $Optional<Int>, #Optional.some!enumelt, %0 : $Int // user: %7
  // 獲取懶加載的$__lazy_storage_$_age的地址存放到%5中
  %5 = ref_element_addr %1 : $Teacher, #Teacher.$__lazy_storage_$_age // user: %6
  // 開始訪問age的內存
  %6 = begin_access [modify] [dynamic] %5 : $*Optional<Int> // users: %7, %8
  // 將%4的值存儲到%6中
  store %4 to %6 : $*Optional<Int>                // id: %7
  // 結束內存的訪問
  end_access %6 : $*Optional<Int>                 // id: %8
  // 返回一個空的元組
  %9 = tuple ()                                   // user: %10
  return %9 : $()                                 // id: %10
} // end sil function 'main.Teacher.age.setter : Swift.Int'

sil中的set方法我們可以知道节视,在延遲存儲屬性中:

  1. 獲取延遲屬性的內存地址
  2. 將傳入的值存儲到內存地址中

3.2.4 get方法

16087116765812.jpg
// Teacher.age.getter
sil hidden [lazy_getter] [noinline] @main.Teacher.age.getter : Swift.Int : $@convention(method) (@guaranteed Teacher) -> Int {
// %0 "self"                                      // users: %14, %2, %1
bb0(%0 : $Teacher):
  debug_value %0 : $Teacher, let, name "self", argno 1 // id: %1
  // 獲取$__lazy_storage_$_age的內存地址存放到%2中
  %2 = ref_element_addr %0 : $Teacher, #Teacher.$__lazy_storage_$_age // user: %3
  // 開始訪問內存
  %3 = begin_access [read] [dynamic] %2 : $*Optional<Int> // users: %4, %5
  // 加載內存中數(shù)據(jù)存放到%4中
  %4 = load %3 : $*Optional<Int>                  // user: %6
  // 結束內存訪問
  end_access %3 : $*Optional<Int>                 // id: %5
  // 如果%4中有值(some)則跳轉bb1,如果沒有值(none)則跳轉到bb2
  switch_enum %4 : $Optional<Int>, case #Optional.some!enumelt: bb1, case #Optional.none!enumelt: bb2 // id: %6

// %7                                             // users: %9, %8
bb1(%7 : $Int):                                   // Preds: bb0
  debug_value %7 : $Int, let, name "tmp1"         // id: %8
  // 跳轉bb3
  br bb3(%7 : $Int)                               // id: %9

bb2:                                              // Preds: bb0
  // 將給age的初始值10取出存放到%10中
  %10 = integer_literal $Builtin.Int64, 10        // user: %11
  // 用一個Int類型的結構體包裝這個10存放到%11
  %11 = struct $Int (%10 : $Builtin.Int64)        // users: %18, %13, %12
  debug_value %11 : $Int, let, name "tmp2"        // id: %12
  // 判斷self是否為空假栓,不空則將%11的值存放到%13中寻行,否則$Int
  %13 = enum $Optional<Int>, #Optional.some!enumelt, %11 : $Int // user: %16
  // 獲取$__lazy_storage_$_age的內存地址存放到%14中
  %14 = ref_element_addr %0 : $Teacher, #Teacher.$__lazy_storage_$_age // user: %15
  // 開始訪問內存
  %15 = begin_access [modify] [dynamic] %14 : $*Optional<Int> // users: %16, %17
  // 將%13中的值存儲到%15
  store %13 to %15 : $*Optional<Int>              // id: %16
  // 結束內存訪問
  end_access %15 : $*Optional<Int>                // id: %17
  // 跳轉bb3
  br bb3(%11 : $Int)                              // id: %18

// %19                                            // user: %20
bb3(%19 : $Int):                                  // Preds: bb2 bb1
  // 返回傳入的值%19,如果%19為空則返回$Int  
  return %19 : $Int                               // id: %20
} // end sil function 'main.Teacher.age.getter : Swift.Int'

sil中的get方法我們可以知道但指,在延遲存儲屬性中:

  1. 獲取延遲屬性的內存地址
  2. 然后取出該地址中的值
  3. 如果獲取的值為空則將默認值取出并返回
  4. 如果不為空則直接返回取的值

通過該sil代碼我們可以看出寡痰,延遲存儲屬性的get是線程不安全的抗楔,為什么不安全呢?

  1. 如果線程1調用get拦坠,執(zhí)行完bb0连躏,判斷age沒有值,將會執(zhí)行bb2
  2. 此時正好cpu時間片分配給了線程2贞滨,線程2也訪問age入热,此時age依然沒有值,同樣會走bb2age賦初始值
  3. 待線程2執(zhí)行完畢后線程1又獲得執(zhí)行權限晓铆,同樣開始執(zhí)行bb2勺良,又會給age賦初值值
  4. 以上并不會影響age的值,如果線程2調用的是set此時修改了age的值骄噪,然后線程1在執(zhí)行bb2的時候尚困,給age賦初始值,就會因線程問題導致我們age的值不準確

3.3 內存的占用

打印非延遲屬性和延遲屬性類的內存的占用大小

16087170949276.jpg

可以看到在使用lazy修飾的延遲存儲屬性在對象的內存占用上會比不使用lazy修飾的多链蕊,那么這是為什么呢事甜?

  • 我們在上面分析sil代碼的時候知道lazy修飾的屬性在底層是可選類型,在這里實際就是Optional<Int>滔韵。
  • 不使用lazy修飾的時候逻谦,對象的內存占用是24,是由metadat+refCounts+Int得來的
  • 使用lazy修飾的時候陪蜻,對象的內存占用是32是由metadat+refCounts+Optional<Int>得來的

這里我們就來看看Optional<Int>的內存占用:

16087176888057.jpg

可以看到Optional<Int>的size是9邦马,其實這里是Int的8,加上Optional的1宴卖,Optional在底層是個枚舉滋将,默認是Int8占用1字節(jié),在后續(xù)會詳細說明嘱腥。由于需要內存對齊耕渴,都是8字節(jié)8字節(jié)的讀取,所以剩下不用的也要補齊齿兔。

3.4 lazy的其他用法

let data = 0...3
let result = data.lazy.map { (i: Int) -> Int in
    print("Handling...")
    return i * 2
}

print("Begin:")
for i in result {
    print(i)
}

在 Swift 標準庫中,也有一些 Lazy 方法础米,就可以在不需要運行時分苇,避免消耗太多的性能。

打印結果如下:

16097387023079.jpg

3.5 小結

經(jīng)過上面的分析現(xiàn)總結如下:

  1. 延遲存儲屬性需要使用lazy修飾
  2. 延遲存儲屬性必須有一個默認的初始值
  3. 延遲存儲屬性在第一次訪問的時候才能被賦值
  4. 延遲存儲屬性并不保證線程安全
  5. 延遲存儲屬性對實例對象的大小有影響

4. 類型屬性

類型屬性跟類方法的命名類似屁桑,是屬于類的一個屬性医寿,不能通過實例對象去訪問,類型屬性需要使用static進行修飾蘑斧,舉個例子:

class Teacher {
    static var age:Int = 18
}

let t = Teacher()

Teacher.age = 20
print(Teacher.age)

print("end")

4.1 通過lldb對類型屬性初步探索

類型屬性也是屬性靖秩,那么類型屬性會存儲在哪里呢须眷?下面我們通過lldb調試,分別從對象沟突、類花颗、類型屬性的內存來對類型屬性進行初步探索。

16087775583517.jpg

我們首先打印了對象t惠拭,查看對象的內存扩劝,并沒有找到對象中對age的存儲。

隨后我們查看對象的metadata职辅,也并沒有相關屬性棒呛。

我們又打印了Teacher.age,得到的直接是age的值

那么類型屬性到底存儲在哪里呢域携?下面我們通過sil代碼來看看簇秒。

4.2 通過sil進一步探索類型屬性

生成sil代碼的方式見3.2

這里為了簡化sil代碼,將Swift代碼修改成如下:

class Teacher {
    static var age:Int = 18
}

Teacher.age = 20

4.2.1 類

class Teacher {
  @_hasStorage @_hasInitialValue static var age: Int { get set }
  @objc deinit
  init()
}

在類中主要的區(qū)別就是:相對于普通的存儲屬性增加了static修飾秀鞭。

在這里我們并沒有得出什么有用的結論宰睡,下面我們繼續(xù)往下看:

16087880666372.jpg

此時我們看到如上圖所示的sil_global,這里我們的Teacher.age已經(jīng)是一個全局變量了气筋。下面我們就去main函數(shù)中看看age的初始化拆内。

4.2.2 main函數(shù)

16087884747572.jpg
// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  %2 = metatype $@thick Teacher.Type
  // function_ref Teacher.age.unsafeMutableAddressor
  %3 = function_ref @main.Teacher.age.unsafeMutableAddressor : Swift.Int : $@convention(thin) () -> Builtin.RawPointer // user: %4
  // 調用%3中存儲的函數(shù),按照命名應該是獲取Teacher.age的內存地址宠默,并將返回結果存儲到%4
  %4 = apply %3() : $@convention(thin) () -> Builtin.RawPointer // user: %5
  // 將%4的地址存放到%5
  %5 = pointer_to_address %4 : $Builtin.RawPointer to [strict] $*Int // user: %8
  // 構建20這個值
  %6 = integer_literal $Builtin.Int64, 20         // user: %7
  // 構建Int結構體
  %7 = struct $Int (%6 : $Builtin.Int64)          // user: %9
  // 開始方法內存麸恍,%5中的,也就是age的地址
  %8 = begin_access [modify] [dynamic] %5 : $*Int // users: %9, %10
  // 將%7存儲到%8中搀矫,也就是將20存儲到age的地址
  store %7 to %8 : $*Int                          // id: %9
  // 結束內存訪問
  end_access %8 : $*Int                           // id: %10
  %11 = integer_literal $Builtin.Int32, 0         // user: %12
  %12 = struct $Int32 (%11 : $Builtin.Int32)      // user: %13
  return %12 : $Int32                             // id: %13
} // end sil function 'main'

經(jīng)過對main函數(shù)的分析抹沪,我們可以知道:

  • 類型屬性的內存地址是通過xxx.unsafeMutableAddressor函數(shù)獲取到的
  • 然后將需要存儲的值存儲到這個內存中
  • 這也就很好的解釋了我們在lldb調試的時候為什么打印出來的是存儲的值。

4.2.3 xxx.unsafeMutableAddressor

16087901415141.jpg
// Teacher.age.unsafeMutableAddressor
sil hidden [global_init] @main.Teacher.age.unsafeMutableAddressor : Swift.Int : $@convention(thin) () -> Builtin.RawPointer {
bb0:
  // 全局地址
  %0 = global_addr @globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_token0 : $*Builtin.Word // user: %1
  // 全局地址存放到%1
  %1 = address_to_pointer %0 : $*Builtin.Word to $Builtin.RawPointer // user: %3
  // function_ref globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_func0
  // 函數(shù)地址存儲到%2
  %2 = function_ref @globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_func0 : $@convention(c) () -> () // user: %3
  // once 保證全局地址只被初始化一次
  %3 = builtin "once"(%1 : $Builtin.RawPointer, %2 : $@convention(c) () -> ()) : $()
  // 初始化的全局地址存放到%4
  %4 = global_addr @static main.Teacher.age : Swift.Int : $*Int // user: %5
  // 地址轉為指針存放到%5
  %5 = address_to_pointer %4 : $*Int to $Builtin.RawPointer // user: %6
  // 返回
  return %5 : $Builtin.RawPointer                 // id: %6
} // end sil function 'main.Teacher.age.unsafeMutableAddressor : Swift.Int'

我們可以在這個函數(shù)中看到:

  • 通過@globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_token0創(chuàng)建一個全局地址瓤球,這個就是類下面那段代碼
  • 這里面還用到了builtin "once"來保證值初始化一次

4.2.4 builtin "once"

那么這個builtin "once"是怎么保證類型屬性只初始化一次的呢融欧?

我們通過添加斷點和匯編調試來看看。

添加斷點:

16087907583429.jpg

開啟匯編調試:

16087907853587.jpg

匯編代碼:

16087908489836.jpg

在匯編代碼中我們看到斷點在c處卦羡,我們按住command點擊鼠標噪馏,跳轉到下一層中:

16087909729953.jpg

我們看到這里調用的是swift_once

我們在swift_once這行添加斷點,過掉上一個斷點绿饵,command點擊鼠標欠肾,跳轉到下一層中:

16087910734447.jpg

此時我們看到了dispatch_once_f,這里基本就能證明了builtin "once"實際上還是使用了GCD中的dispatch_once來保證類型屬性只被初始化一次的拟赊。

其實我們來到Swift源碼中刺桃,搜索一下swift_once

16087913530439.jpg
/// Runs the given function with the given context argument exactly once.
/// The predicate argument must point to a global or static variable of static
/// extent of type swift_once_t.
void swift::swift_once(swift_once_t *predicate, void (*fn)(void *),
                       void *context) {
#if defined(__APPLE__)
  dispatch_once_f(predicate, context, fn);
#elif defined(__CYGWIN__)
  _swift_once_f(predicate, context, fn);
#else
  std::call_once(*predicate, [fn, context]() { fn(context); });
#endif
}

通過對源碼的探索,我們也可以清楚的看到吸祟,swift_onceapple平臺調用的是dispatch_once瑟慈。

4.2.5 get&set

下面我們在來看看類型屬性的getset方法

get方法及分析:

// static Teacher.age.getter
sil hidden [transparent] @static main.Teacher.age.getter : Swift.Int : $@convention(method) (@thick Teacher.Type) -> Int {
// %0 "self"                                      // user: %1
bb0(%0 : $@thick Teacher.Type):
  debug_value %0 : $@thick Teacher.Type, let, name "self", argno 1 // id: %1
  // function_ref Teacher.age.unsafeMutableAddressor
  // 將獲取地址的函數(shù)存儲到%2
  %2 = function_ref @main.Teacher.age.unsafeMutableAddressor : Swift.Int : $@convention(thin) () -> Builtin.RawPointer // user: %3
  // 調用函數(shù)并將返回結果存儲到%3
  %3 = apply %2() : $@convention(thin) () -> Builtin.RawPointer // user: %4
  // 指針轉地址存放到%4
  %4 = pointer_to_address %3 : $Builtin.RawPointer to [strict] $*Int // user: %5
  // 開始內存訪問
  %5 = begin_access [read] [dynamic] %4 : $*Int   // users: %6, %7
  // 加載內存地址中的數(shù)據(jù)存儲到%6
  %6 = load %5 : $*Int                            // user: %8
  // 結束內存訪問
  end_access %5 : $*Int                           // id: %7
  // 返回取到的數(shù)據(jù)
  return %6 : $Int                                // id: %8
} // end sil function 'static main.Teacher.age.getter : Swift.Int'

set方法及分析:

// static Teacher.age.setter
sil hidden [transparent] @static main.Teacher.age.setter : Swift.Int : $@convention(method) (Int, @thick Teacher.Type) -> () {
// %0 "value"                                     // users: %8, %2
// %1 "self"                                      // user: %3
bb0(%0 : $Int, %1 : $@thick Teacher.Type):
  debug_value %0 : $Int, let, name "value", argno 1 // id: %2
  debug_value %1 : $@thick Teacher.Type, let, name "self", argno 2 // id: %3
  // function_ref Teacher.age.unsafeMutableAddressor
  // 獲取age的地址的函數(shù)存放到%4
  %4 = function_ref @main.Teacher.age.unsafeMutableAddressor : Swift.Int : $@convention(thin) () -> Builtin.RawPointer // user: %5
  // 調用函數(shù)并將返回值存放到%5
  %5 = apply %4() : $@convention(thin) () -> Builtin.RawPointer // user: %6
  // 指針轉地址存儲到%6
  %6 = pointer_to_address %5 : $Builtin.RawPointer to [strict] $*Int // user: %7
  // 開始方法指針
  %7 = begin_access [modify] [dynamic] %6 : $*Int // users: %8, %9
  // 將傳入的參數(shù)%0 存儲到%7
  store %0 to %7 : $*Int                          // id: %8
  // 結束內存訪問
  end_access %7 : $*Int                           // id: %9
  %10 = tuple ()                                  // user: %11
  return %10 : $()                                // id: %11
} // end sil function 'static main.Teacher.age.setter : Swift.Int'

4.3 類型屬性的內存占用

通過上面的分析桃移,我們可以知道類型屬性是一個全局變量,那么類型屬性是不是不會占用類的存儲空間呢葛碧?下面我們打印來看看:

16087925470522.jpg

通過打印我們可以知道借杰,類型屬性并不會占用類的空間。

4.4 單例的創(chuàng)建

回想一下吹埠,在Objective-C我們會使用如下的方式創(chuàng)建一個單例:

@implementation SITeacher
+ (instancetype)shareInstance{
    static SITeacher *shareInstance = nil;
    dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        shareInstance = [[SITeacher alloc] init];
    });
    return shareInstance;
}
@end

既然類型存儲屬性是個全局變量第步,那么我們在Swift中創(chuàng)建一個單例就簡單多了,代碼如下:

class Teacher {
    // 聲明一個不可變的 let 類型 static 屬性
    // 為當前類的實例對象
    static let shared = Teacher()
    // 將當前類的 init 方法 添加private訪問權限
    private init(){}
}

其實經(jīng)常寫Swift代碼的人都知道Swift中單例是怎么寫的缘琅,但是其原理是什么并不是很多人都知道的粘都,其實就是類型屬性是個全局變量,而且只初始化一次刷袍,符合單例的條件翩隧。

4.5 小結

通過上面的分析,我們總結如下:

  1. 類型屬性通過static來修飾
  2. 類型屬性需要附一個默認的初始值
  3. 類型屬性只會被初始化一次
  4. 類型屬性是線程安全的
  5. 類型屬性存儲為全局變量
  6. 類型屬性不占用對象的存儲空間
  7. 類型屬性可以用作單例來使用呻纹,前提是需要private類的init方法堆生。

5. 屬性觀察者

介紹了完了Swift中的幾種屬性后,下面我們來看看在屬性中使用頻率最高的屬性觀察者雷酪。屬性觀察者在代碼中實際就是willSetdidSet

class Teacher {
    var age:Int = 18 {
        //賦新值之前調用
        willSet {
            print("willSet newValue --- \(newValue)")
        }
        // 賦新值之后調用
        didSet {
            print("didSet oldValue --- \(oldValue)")
        }
    }
}

let t = Teacher()
t.age = 20

print("end")

打印結果:

16087981771930.jpg

通過打印結果我們可以看到淑仆,在賦新值前我們可以拿到即將要賦予的新值,在賦新值后也能夠拿到賦值前的舊值哥力。

5.2 通過sil探索屬性觀察者

sil編譯命令詳見3.2

5.2.1 類

class Teacher {
  @_hasStorage @_hasInitialValue var age: Int { get set }
  @objc deinit
  init()
}

在類中相比于普通的存儲屬性沒有什么區(qū)別蔗怠。

5.2.2 set 方法

其實屬性觀察主要是在設置新值的時候觸發(fā)的,這里我們直接來看set方法吩跋。

set方法及分析:

// Teacher.age.setter
sil hidden @main.Teacher.age.setter : Swift.Int : $@convention(method) (Int, @guaranteed Teacher) -> () {
// %0 "value"                                     // users: %13, %10, %2
// %1 "self"                                      // users: %16, %11, %10, %4, %3
bb0(%0 : $Int, %1 : $Teacher):
  debug_value %0 : $Int, let, name "value", argno 1 // id: %2
  debug_value %1 : $Teacher, let, name "self", argno 2 // id: %3
  // 找出實例內部物理實例變量的地址
  %4 = ref_element_addr %1 : $Teacher, #Teacher.age // user: %5
  // 開始訪問內存
  %5 = begin_access [read] [dynamic] %4 : $*Int   // users: %6, %7
  // 將內存中的值加載到%6
  %6 = load %5 : $*Int                            // users: %8, %16
  // 結束內存訪問
  end_access %5 : $*Int                           // id: %7
  debug_value %6 : $Int, let, name "tmp"          // id: %8
  // function_ref Teacher.age.willset
  // 獲取 willset 的函數(shù)地址
  %9 = function_ref @main.Teacher.age.willset : Swift.Int : $@convention(method) (Int, @guaranteed Teacher) -> () // user: %10
  // 調用 willset 函數(shù)寞射,傳入%0---新值,%1---self
  %10 = apply %9(%0, %1) : $@convention(method) (Int, @guaranteed Teacher) -> ()
  // 找出實例內部物理實例變量的地址
  %11 = ref_element_addr %1 : $Teacher, #Teacher.age // user: %12
  // 開始訪問內存
  %12 = begin_access [modify] [dynamic] %11 : $*Int // users: %13, %14
  // 存儲新值
  store %0 to %12 : $*Int                         // id: %13
  // 結束內存訪問
  end_access %12 : $*Int                          // id: %14
  // function_ref Teacher.age.didset
  // 獲取 didset 的函數(shù)地址
  %15 = function_ref @main.Teacher.age.didset : Swift.Int : $@convention(method) (Int, @guaranteed Teacher) -> () // user: %16
  // 調用 didset 傳入?yún)?shù) %6---舊值锌钮,%1---self
  %16 = apply %15(%6, %1) : $@convention(method) (Int, @guaranteed Teacher) -> ()
  %17 = tuple ()                                  // user: %18
  return %17 : $()                                // id: %18
} // end sil function 'main.Teacher.age.setter : Swift.Int'

set方法中我們可以看到桥温,使用了屬性觀察者的屬性,在set方法中:

  1. 會將屬性中的舊值取出梁丘,存起來
  2. 在設置新值前會調用willSet函數(shù)侵浸,并將新值作為參數(shù)傳入
  3. 在設置新值后會調用didSet函數(shù),并將舊值作為參數(shù)傳入

5.2.3 willSet&didSet

willSet.jpg

willSet 函數(shù)中我們可以看到兰吟,其內部聲明了newValue變量通惫,所以我們可以直接使用

didSet.jpg

didSet 函數(shù)中我們同樣也看到了oldValue變量的聲明,同樣也為我們直接使用提供了變量混蔼。

5.3 屬性觀察者的使用

5.3.1 init方法中會觸發(fā)屬性觀察者嗎

class Teacher {
    var age: Int = 18 {
        //賦新值之前調用
        willSet {
            print("willSet newValue --- \(newValue)")
        }
        // 賦新值之后調用
        didSet {
            print("didSet oldValue --- \(oldValue)")
        }
    }
    
    init() {
        age = 20
    }
}

print("end")

運行結果:

16088029261282.jpg

我們可以看到在init方法中給屬性賦值,并沒有觸發(fā)屬性觀察者珊燎,那是為什么呢惭嚣?

init 方法會初始化當前變量遵湖,對于存儲屬性,在分配內存后晚吞,會調用memset清理內存延旧,因為可能存在臟數(shù)據(jù),清理完才會賦值槽地。

還有一種原因就是迁沫,我們如果有多個屬性,我們給A屬性賦值捌蚊,并在A屬性的觀察者中訪問了B屬性集畅,此時B屬性可能還沒有被初始化,此時我們就有可能獲取到臟數(shù)據(jù)缅糟,導致結果不準確挺智。

(PS:)仔細想想也不是很嚴謹,所以通過sil代碼來看看窗宦,調用流程如下:

@main.Teacher.__allocating_init()——>@main.Teacher.init()

// Teacher.init()
sil hidden @main.Teacher.init() -> main.Teacher : $@convention(method) (@owned Teacher) -> @owned Teacher {
// %0 "self"                                      // users: %14, %10, %4, %1
bb0(%0 : $Teacher):
  debug_value %0 : $Teacher, let, name "self", argno 1 // id: %1
  %2 = integer_literal $Builtin.Int64, 18         // user: %3
  %3 = struct $Int (%2 : $Builtin.Int64)          // user: %6
  %4 = ref_element_addr %0 : $Teacher, #Teacher.age // user: %5
  %5 = begin_access [modify] [dynamic] %4 : $*Int // users: %6, %7
  store %3 to %5 : $*Int                          // id: %6
  end_access %5 : $*Int                           // id: %7
  %8 = integer_literal $Builtin.Int64, 20         // user: %9
  %9 = struct $Int (%8 : $Builtin.Int64)          // user: %12
  %10 = ref_element_addr %0 : $Teacher, #Teacher.age // user: %11
  %11 = begin_access [modify] [dynamic] %10 : $*Int // users: %12, %13
  store %9 to %11 : $*Int                         // id: %12
  end_access %11 : $*Int                          // id: %13
  return %0 : $Teacher                            // id: %14
} // end sil function 'main.Teacher.init() -> main.Teacher'

init方法中赦颇,我們可以看到,屬性的賦值都是操作的屬性的內存地址赴涵,并沒有調用willSetdidSet相關方法媒怯。但是賦值也是在屬性初始化之后,所以對于memset應該是站不住腳的髓窜,對于屬性觀察者中調用其他存儲屬性按道理也是站不住腳的扇苞。

下面我們換一下測試代碼:

class Teacher {
    
    var age2: Int {
        get{
            return age
        }
        set{
            self.age = newValue
        }
    }
    
    var age: Int = 18 {
        //賦新值之前調用
        willSet {
            print("willSet newValue --- \(newValue)")
        }
        // 賦新值之后調用
        didSet {
            print("didSet oldValue --- \(oldValue)")
        }
    }
    
    init() {
        age2 = 20
    }
}

打印結果:

16088621679034.jpg

此時我們發(fā)現(xiàn)在計算屬性中給存儲屬性賦值就會調用屬性觀察者,下面我們通過sil代碼看看調用纱烘。

init 函數(shù):

16088625229134.jpg

我們可以看到在init函數(shù)中對于計算屬性是直接調用了其setter方法杨拐。并且是在存儲屬性age初始化完畢后才調用的計算屬性的setter

16088626625701.jpg

我們可以看到在計算屬性的的setter方法中給存儲屬性賦值是調用的存儲屬性的setter方法擂啥,在上面的分析中我們已經(jīng)知道了哄陶,在存儲屬性的setter方法中會調用willSetdidSet

綜上所述哺壶,在init方法中給存儲屬性賦值是否觸發(fā)屬性觀察者應該是這樣的:

  • 賦值前后自己是能知道的屋吨,沒必要在調用屬性觀察者
  • 在計算屬性中給存儲屬性賦值,是因為計算屬性的代碼一般是固定的山宾,并且這個值有可能計算后的至扰,所以需要觸發(fā)屬性觀察者,使其做后續(xù)處理

5.3.2 在那些地方可以添加屬性觀察者

  • 屬性觀察者應用在存儲屬性上
  • 計算屬性自己實現(xiàn)了getset资锰,此時要想做些什么事情敢课,可以隨便去做,不需要屬性觀察者
  • 但是繼承的計算屬性也是可以添加屬性觀察者的

應用在存儲屬性上在上面一直在使用,這里就不驗證了

對于計算屬性直秆,驗證結果如下:

16088641842589.jpg

我們可以看到willSetdidSet不能和getter一起提供濒募,那么我們注釋掉getter呢?

16088643020537.jpg

此時我們看到有setter的變量也必須有getter

所以屬性觀察者不能添加到計算屬性中

下面我們來驗證在繼承的計算屬性中的屬性觀察者:

16088646388045.jpg

我們看到對于繼承計算屬性的同樣可以添加屬性觀察者圾结,并在賦值的時候調用瑰剃。

5.3.3 屬性觀察者的調用順序

如果是繼承存儲屬性,并且父類和子類都添加了屬性觀察者筝野,那么這個調用順序是什么呢晌姚?,驗證如下:

16088648987009.jpg

我們看到調用順序是:

  1. 先調用子類的willSet歇竟,再調用父類的willSet
  2. 先調用父類的didSet挥唠,在調用子類的didSet

其實也很好理解,首先通知子類要改變途蒋,畢竟是給子類賦值猛遍,然后在通知父類改變,父類改變完了先在父類做些處理号坡,然后在讓子類改變懊烤,子類在做最后的處理。

5.3.4 在子類的init方法中給屬性賦值

如果在子類的init方法中給屬性賦值會怎樣調用屬性觀察者呢宽堆?測試結果如下:

16088656016892.jpg

我們可以看到在子類的init方法中給屬性賦值與直接給子類對象賦值的調用屬性是一模模一樣樣的腌紧。我的理解是這樣的:

  • 在子類的init方法中賦值,父類是監(jiān)聽不到畜隶,所以需要觸發(fā)父類的屬性觀察者壁肋,使其作出相應處理
  • 當父類做完處理子類也是監(jiān)聽不到的,所以同意需要觸發(fā)子類屬性觀察者通知子類作出相應處理

6. Then

Swift中對于屬性也會經(jīng)常用到一個叫Then的框架:

devxoul/Then

其用法如下:

6.1 Then

import Then

let label = UILabel().then {
  $0.textAlignment = .center
  $0.textColor = .black
  $0.text = "Hello, World!"
}

以上等效于

let label: UILabel = {
  let label = UILabel()
  label.textAlignment = .center
  label.textColor = .black
  label.text = "Hello, World!"
  return label
}()

6.2 with

let newFrame = oldFrame.with {
  $0.size.width = 200
  $0.size.height = 100
}
newFrame.width // 200
newFrame.height // 100

6.3 do

UserDefaults.standard.do {
  $0.set("devxoul", forKey: "username")
  $0.set("devxoul@gmail.com", forKey: "email")
  $0.synchronize()
}

其原理其實也蠻簡單的籽慢,感興趣的可以去看看浸遗。

7. 總結

至此我們對Swift中的屬性基本就分析完畢了先在總結如下:

存儲屬性:

  1. 存儲屬性是占用實例存儲空間的
  2. sil代碼中使用``@_hasStorage關鍵字修飾
  3. 再其getset方法中通過訪問內存獲取和修改屬性值

計算屬性:

  1. 計算屬性的本質就是getset方法
  2. 計算屬性不占用實例對象的內存空間

延遲存儲屬性:

  1. 延遲存儲屬性也是存儲屬性
  2. 延遲存儲屬性需要使用lazy修飾
  3. 延遲存儲屬性必須有一個默認的初始值
  4. 延遲存儲屬性在第一次訪問的時候才能被賦值
  5. 延遲存儲屬性并不保證線程安全
  6. 延遲存儲屬性對實例對象的大小有影響,一般會增加內存占用

類型屬性:

  1. 類型屬性通過static來修飾
  2. 類型屬性需要賦一個默認的初始值
  3. 類型屬性只會被初始化一次
  4. 類型屬性是線程安全的
  5. 類型屬性存儲為全局變量
  6. 類型屬性不占用對象的存儲空間
  7. 類型屬性可以用作單例來使用箱亿,前提是需要private類的init方法跛锌。

屬性觀察者:

  1. 屬性觀察者是觀察屬性改變前后的,本質就是willSetdidSet
  2. 屬性觀察者可以添加在
    1. 存儲屬性
    2. 繼承的存儲屬性
    3. 繼承的計算屬性
  3. 如果并沒有在屬性中添加屬性觀察者届惋,則不會調用屬性觀察者
  4. 在底層代碼中會根據(jù)開發(fā)者對屬性觀察者的依賴調用屬性觀察者
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末髓帽,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子脑豹,更是在濱河造成了極大的恐慌郑藏,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件瘩欺,死亡現(xiàn)場離奇詭異必盖,居然都是意外死亡拌牲,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進店門筑悴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來们拙,“玉大人稍途,你說我怎么就攤上這事阁吝÷蹩撸” “怎么了热芹?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵爹耗,是天一觀的道長擎厢。 經(jīng)常有香客問我铆惑,道長卸察,這世上最難降的妖魔是什么慈迈? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任嗜湃,我火速辦了婚禮迄损,結果婚禮上定躏,老公的妹妹穿的比我還像新娘。我一直安慰自己芹敌,他們只是感情好痊远,可當我...
    茶點故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著氏捞,像睡著了一般碧聪。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上液茎,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天逞姿,我揣著相機與錄音,去河邊找鬼捆等。 笑死滞造,一個胖子當著我的面吹牛,可吹牛的內容都是我干的栋烤。 我是一名探鬼主播谒养,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼班缎!你這毒婦竟也來了蝴光?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤达址,失蹤者是張志新(化名)和其女友劉穎蔑祟,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體沉唠,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡疆虚,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片径簿。...
    茶點故事閱讀 40,102評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡罢屈,死狀恐怖,靈堂內的尸體忽然破棺而出篇亭,到底是詐尸還是另有隱情缠捌,我是刑警寧澤,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布译蒂,位于F島的核電站曼月,受9級特大地震影響,放射性物質發(fā)生泄漏柔昼。R本人自食惡果不足惜哑芹,卻給世界環(huán)境...
    茶點故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望捕透。 院中可真熱鬧聪姿,春花似錦、人聲如沸乙嘀。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽乒躺。三九已至招盲,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間嘉冒,已是汗流浹背曹货。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留讳推,地道東北人顶籽。 一個月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像银觅,于是被迫代替她去往敵國和親礼饱。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,044評論 2 355

推薦閱讀更多精彩內容

  • Swift屬性 存儲屬性(要么是常量(let 修飾)存儲屬性究驴,要么是變量(var 修飾)存儲屬性) 計算屬性(顧名...
    Mjs閱讀 326評論 0 0
  • 原文博客地址: 淺談Swift的屬性(Property) 今年期待已久的Swift5.0穩(wěn)定版就已經(jīng)發(fā)布了, 感興...
    TitanCoder閱讀 1,694評論 0 6
  • 屬性分類在Swift中, 嚴格意義上來講屬性可以分為兩大類: 實例屬性和類型屬性 實例屬性(Instance Pr...
    1980_4b74閱讀 442評論 0 0
  • 存儲屬性 計算屬性 屬性觀察者 靜態(tài)屬性 使用下標 存儲屬性 存儲屬性概念 存儲屬性可以存儲數(shù)據(jù)镊绪,分為常量屬性(用...
    優(yōu)雅的步伐閱讀 265評論 0 1
  • 存儲屬性 - Stored Properties 相當于 OC 的下劃線成員變量 適用于:結構體 蝴韭、 類 類型:常...
    Sunday_David閱讀 265評論 0 0