任何一門語言都有屬性的概念。Swift中的屬性是怎麼的呢?

一、屬性

Swift中跟例項相關的屬性可以分為2大類:儲存屬性和計算屬性。

1.1. 儲存屬性(Stored Property

特點:

  • 類似於成員變數的概念;
  • 儲存在例項的記憶體中;
  • 結構體、類可以定義儲存屬性;
  • 列舉不可以定義儲存屬性。

示例程式碼:

  1. struct Circle {
  2. var radius: Double
  3. }
  4. class classCircle {
  5. var radius: Double
  6. }

關於儲存屬性,Swift有個明確的規定:

在建立類或結構體的例項時,必須為所有的儲存屬性設定一個合適的初始值。

  • 可以在初始化器裡為儲存屬性設定一個初始值;
  • 可以分配一個預設的屬性值作為屬性定義的一部分。

1.2. 計算屬性(Computed Property

特點:

  • 本質就是方法(函式);
  • 不佔用例項的記憶體;
  • 列舉、結構體、類都可以定義計算屬性。

示例程式碼:

  1. struct Circle {
  2. // 儲存屬性
  3. var radius: Double
  4. // 計算屬性
  5. var diameter: Double {
  6. set {
  7. print("set")
  8. radius = newValue / 2
  9. }
  10. get {
  11. print("get")
  12. return radius * 2
  13. }
  14. }
  15. }
  16. var c = Circle(radius: 10)
  17. c.radius = 11
  18. print("--1--")
  19. c.diameter = 40
  20. print("--2--")
  21. print(c.diameter)
  22. /*
  23. 輸出:
  24. set
  25. get
  26. 20.0
  27. */

輸出分析: 上面程式碼如果執行c.diameter = 40radius的值就會變為20。因為這樣會執行diameterset方法(40作為引數),上面的案例看到變數newValue,但是程式碼中沒有定義這個變數,其實newValueset方法提供的形參,只不過省略沒有寫而已,完整的set方法程式碼應該是set(newValue) {...}newValue是預設值,可以按照自己的規範修改(建議使用預設的形參命名)。c.diameter呼叫的是diameterget方法。

記憶體分析:

上面示例程式碼中結構體Circle佔用多少記憶體呢?

  1. print(MemoryLayout<circle>.stride)
  2. // 輸出:8

結果顯示佔用8個位元組。因為計算屬性的本質是方法。

補充說明:

  1. set傳入的新值預設叫做newValue,也可以自定義。

    1. struct Circle {
    2. var radius: Double
    3. var diameter: Double {
    4. set(newDiameter) {
    5. radius = newDiameter / 2
    6. }
    7. get {
    8. return radius * 2
    9. }
    10. }
    11. }
  2. 只讀計算屬性:只有get,沒有set

    如果是隻讀屬性,get可以省略不寫:

    1. struct Circle {
    2. var radius: Double
    3. var diameter: Double { radius * 2 }
    4. }
  3. 定義計算屬性只能用var,不能用let

  4. set就必須有get

擴充套件: 列舉rawValue的本質就是隻讀的計算屬性。

1.3. 屬性觀察器(Property Observer)

通過名字就可以聯想到OC中的KVO,是的,兩者確實有相似之處。在Swift中可以為非lazyvar儲存屬性 設定屬性觀察器。

示例程式碼:

  1. struct Circle {
  2. var radius: Double {
  3. willSet {
  4. print("willSet", newValue)
  5. }
  6. didSet {
  7. print("didSet", oldValue, radius)
  8. }
  9. }
  10. init() {
  11. self.radius = 2.0
  12. print("Circle Init")
  13. }
  14. }
  15. var c = Circle()
  16. // 輸出:Circle Init
  17. c.radius = 3.0
  18. /*
  19. 輸出:
  20. willSet 3.0
  21. didSet 2.0 3.0
  22. */

分析:

  • willSet會傳遞新值,預設叫做newValue
  • didSet會傳遞舊值,預設叫做oldValue
  • 在初始化器中設定屬性值不會觸發willSetdidSet。同樣在屬性定義時設定初始值也不會觸發。

二、延遲儲存屬性(Lazy Stored Property)

使用lazy可以定義一個延遲儲存屬性,在第一次用到屬性的時候才會進行初始化。

特點:

  • lazy屬性必須是var,不能是let(let必須在例項的初始化方法完成之前就擁有值);
  • 如果多條執行緒同時第一次訪問lazy屬性,無法保證屬性只被初始化1次(非執行緒安全)。

示例程式碼:

  1. class Car {
  2. init() {
  3. print("Car init")
  4. }
  5. func run() {
  6. print("car run")
  7. }
  8. }
  9. class Person {
  10. lazy var car = Car()
  11. init() {
  12. print("Person init")
  13. }
  14. func goOut() {
  15. print("Person goOut")
  16. car.run()
  17. }
  18. }
  19. var p = Person()
  20. // 輸出:Person init
  21. p.goOut()
  22. /*
  23. 輸出:
  24. Person goOut
  25. Car init
  26. car run
  27. */

分析: 如果Person中的儲存屬性car沒有lazy修飾,在建立Person物件p的時候就會呼叫儲存屬性car的初始化方法。新增lazy修飾後,只會在第一次使用car屬性(物件)時進行初始化。

注意點: 當結構體包含一個延遲儲存屬性時,只有var才能訪問延遲儲存屬性。因為延遲屬性初始化時需要改變結構體的記憶體,而結構體如果使用let修飾後就不能修改所在記憶體。

三、型別屬性(Type Property)

嚴格來說,屬性可以分為:

  • 例項屬性(Instance Property):只能通過例項去訪問

    • 儲存例項屬性(Stored Instance Property):儲存在例項的記憶體中,每個例項都有1份;
    • 計算例項屬性(Computed Instance Property)
  • 型別屬性(Type Property):只能通過型別去訪問

    • 儲存型別屬性(Stored Type Property):整個程式執行過程中,就只有1份記憶體(類似於全域性變數)
    • 計算例項屬性(Computed Type Property)

可以通過static定義型別屬性。如果是類,也可以用關鍵字class

示例程式碼:

  1. struct Shape {
  2. var width: Int
  3. static var count: Int = 30
  4. }
  5. var s = Shape(width: 10)
  6. s.width = 20
  7. print("before count:\(Shape.count)") // 輸出:before count:30
  8. Shape.count = 40
  9. print("after count:\(Shape.count)") // 輸出:after count:40

3.1. 型別屬性細節

  1. 不同於儲存例項屬性,儲存型別屬性必須進行初始化,否則報錯(因為型別沒有像例項那樣的init初始化器來初始化儲存屬性):

  2. 儲存型別屬性預設就是lazy,會在第一次使用的時候才初始化,就算被多個執行緒同時訪問,保證只會初始化一次(執行緒安全)。

  3. 儲存型別屬性可以是let

  4. 列舉型別也可以定義型別屬性(儲存型別屬性,計算型別屬性)。

3.2. 單例模式

使用型別屬性可以建立單例模式。

示例程式碼:

  1. class FileManager {
  2. public static let shared = FileHandle()
  3. private init() {}
  4. }
  5. var f1 = FileManager.shared;

把初始化器設為private,這樣就無法讓外界使用init建立例項。把型別屬性設為public,在其他檔案中也可以訪問,儲存型別屬性再用let修飾,這樣就能保證例項只能指向一塊固定記憶體。

3.2. 型別儲存屬性的本質

第一步:示例程式碼

第二步:檢視全域性變數記憶體地址

分析:

num1記憶體地址:0x1000013f1 + 0x5df7 = 0x1000071E8

num2記憶體地址:0x1000013fc + 0x5df4 = 0x1000071F0

num3記憶體地址:0x100001407 + 0x5df1 = 0x1000071F8

結論:

num1,num2,num3三個變數的記憶體地址是連續的。

第三步:檢視型別儲存屬性地址

分析:

num1記憶體地址:0x100001013 + 0x631d = 0x100007330

Car.count記憶體地址:0x100007338

num3記憶體地址:0x10000105c + 0x62e4 = 0x100007340

結論:

num1,Car.count,num3三個變數的記憶體地址是連續的。

從內寸角度看,型別儲存屬性寫在外面和裡面沒有什麼區別,寫在類裡面只是代表該屬性有一定訪問許可權。

型別儲存屬性預設是lazy,所以在第一次訪問的時候做了很多操作。而且只被初始化一次。

通過彙編檢視型別儲存屬性初始化:

發現,型別屬性初始化最終呼叫的是GCD中的dispatch_once,這樣就保證了屬性只被初始化一次。