淺談 Swift 中的屬性(Property

davidtim發表於2021-09-09

Properties in Swift

Date Notes Swift Xcode
2017-04-27 擴充 #延遲儲存屬性# 部分並新增 #devxoul/Then# 3.1 8.3.2
2016-10-26 首次提交 3.0 8.1 Beta 3

前言

Swift 中的屬性分為儲存屬性與計算屬性,儲存屬性即為我們平時常用的屬性,可以直接賦值使用,而計算屬性不直接儲存值,而是根據其他(儲存)屬性計算得來的值。

在其他物件導向的程式語言中,例如 Java 和 Objective-C 中,get 和 set 方法提供了統一、規範的介面,可以使得外部訪問或設定物件的私有屬性,而不破壞封裝性,也可以很好的控制許可權(選擇性實現 get 或 set 方法)。而 Swift 中似乎並沒有見到類似的 get 和 set 方法,而 Swift 使用了一種名為屬性觀察器的概念來解決該問題。

本文簡單介紹下 Swift 中的這兩種屬性,以及屬性觀察器。

延遲儲存屬性

  • 儲存屬性使用廣泛,即是類或結構體中的變數或常量,可以直接賦初始值,也可以修改其初始值(僅指變數)。

  • 延遲儲存屬性是指第一次使用到該變數再進行運算(這裡的運算不能依賴其他成員屬性,經 評論提示,延遲儲存屬性可以使用成員屬性,只是需要在閉包中明確指出其持有物件本身,即加上 self;也可以直接使用靜態/類屬性)。

  • 延遲儲存屬性必須宣告為 var 變數,因為其屬性值在物件例項化前可能無法得到,而常量必須在初始化完成前擁有初始值。

  • 在 Swift 中,可以將消耗效能才能得到的值作為延遲儲存屬性,即懶載入。

  • 全域性的常量和變數也是延遲儲存屬性,但不需要顯式宣告為 lazy(不支援 Playground)。

Demo

  • 這裡假定在 ViewController.swift 有一個屬性,需要從 plist 檔案讀取內容,將其中的字典轉為模型。如果 plist 檔案中內容很多,那麼就十分消耗效能。如果使用者不觸發相應事件,也沒有必要載入這些資料。那麼這裡就很適合使用懶載入,即延遲儲存屬性。

ViewController.swift

class ViewController: UIViewController {    lazy var goods: NSArray? = {        var goodsArray: NSMutableArray = []        if let path = Bundle.main.path(forResource: "Goods", ofType: "plist") {            if let array = NSArray(contentsOfFile: path) {                for goodsDict in array {
                    goodsArray.add(Goods(goodsDict as! NSDictionary))
                }                return goodsArray
            }
        }        return nil
    }()    // 這樣也是允許的,可以把初始化的程式碼直接放在構造方法中
    lazy var testLazy = Person()
}class Person {}

可以在延遲儲存屬性運算的程式碼中加入列印語句,即可驗證其何時初始化。

Lazy 初始化的「演變」過程

  • 根據上面 Demo,延遲儲存屬性的初始化程式碼部分可能有些讓人迷惑,但其實也是初始化的一步步的演變過程。在 的 中也有描述這個過程,簡單用程式碼表示也如下所示:

struct Person {
    var name = ""

    init() {        print(#function)
    }
}

// 直接初始化let p1 = Person()

// 利用閉包let getOnePerson = { () -> Person in
    let p = Person()    return p
}let p2 = getOnePerson()

// 閉包執行let p3 = { () -> Person in
    let p = Person()    return p
}()

// 簡化let p4: Person = {    let p = Person()    return p
}()

Lazy 方法

  • 的 Swifter Tips 中也提到,在 Swift 標準庫中,也有一些 Lazy 方法,就可以在不需要執行時,避免消耗太多的效能。

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

print("Begin:")for i in result {
    print(i)
}// OUTPUT:// Begin:// Handling...// 0// Handling...// 2// Handling...// 4// Handling...// 6

devxoul/Then

  • 在 的中提到了一個 devxoul/Then 庫,為 Swift 的構造方法加入語法糖。

Demo

ViewController.swift

import UIKitimport Thenclass ViewController: UIViewController {
    lazy var myLabel = UILabel().then {
        $0.text = "My Label"
    }

    override func viewDidLoad() {        super.viewDidLoad()

        myLabel.frame = CGRect(x: 0.0, y: 0.0,
                               width: 100.0, height: 100.0)
        view.addSubview(myLabel)
    }
}

Source Code

  • Then 的核心原始碼部分總共只有不到 20 行,非常簡單易懂。

  • Then 庫中定義了一個名為 Then 的空協議,之後透過協議擴充套件(Protocol Extension),來為協議新增預設的方法實現。

then()

  • 因為 then() 內部將 self 返回,即可在初始化完成後,呼叫該方法,並在閉包中設定屬性,而且不需要再將自身返回。

  • 支援 NSObject 子類,也可以將自定義型別(僅支援 AnyObject 型別,即 class)宣告實現該協議即可(協議擴充套件已經擁有預設實現,所以僅宣告實現協議即可)。

extension Then where Self: AnyObject {  /// Makes it available to set properties with closures just after initializing.
  public func then(_ block: (Self) -> Void) -> Self {
    block(self)    return self
  }

}

with()

  • then() 適用於引用型別,而 with() 適用於值型別。

  • 使用了 inout 確保方法內外共用一個值型別變數。

extension Then where Self: Any {  /// Makes it available to set properties with closures just after initializing and copying the value types.
  public func with(_ block: (inout Self) -> Void) -> Self {    var copy = self
    block(&copy)    return copy
  }

}

do()

  • do() 使得可以直接在閉包中簡單地執行一些操作。

extension Then where Self: Any {  /// Makes it available to execute something with closures.
  public func `do`(_ block: (Self) -> Void) {
    block(self)
  }

}

計算屬性

  • 舉個例子,一個矩形結構體(類同理),擁有寬度高度兩個儲存屬性,以及一個只讀面積的計算屬性,因為透過設定矩形的寬度和高度即可計算出矩形的面積,而無需直接設定其值。當寬度或高度改變,面積也應當可以跟隨其變化(反之不能推算,因此為只讀)。為說明 setter 以及便捷 setter 說明,另外新增了原點(矩形左下角)儲存屬性,以及中心計算屬性。

Demo

struct Point {    var x = 0.0
    var y = 0.0}struct Rectangle {    var width = 0.0
    var height = 0.0
    var origin = Point()    // 只讀計算屬性
    var size: Double {        get {            return width * height
        }
    }    // 只讀計算屬性簡寫為//    var size: Double {//        return width * height//    }

    var center: Point {        get {            return Point(x: origin.x + width / 2,
                         y: origin.y + height / 2)
        }        set(newCenter) {
            origin.x = newCenter.x - width / 2
            origin.y = newCenter.y - height / 2
        }        // 便捷 setter 宣告//        set {//            origin.x = newValue.x - width / 2//            origin.y = newValue.y - height / 2//        }
    }

}var rect = Rectangle()
rect.width = 100rect.height = 50print(rect.size)

rect.origin = Point(x: 0, y: 0)print(rect.center)

rect.center = Point(x: 100, y: 100)print(rect.origin)// 5000.0// Point(x: 50.0, y: 25.0)// Point(x: 50.0, y: 75.0)

綜上,getter 可以根據儲存屬性推算計算屬性的值,setter 可以在被賦值時根據新值倒推儲存屬性,但它們與我們在其他語言中的 get/set 方法卻不一樣。

屬性觀察器

  • 屬性觀察器算是 Swift 中的一個 feature,變數在設值會先進入 willSet,這時預設 newValue 等於即將要賦值的值,而變數本身尚未改變。變數在設值會先進入 didSet,這時預設 oldValue 等於賦值前變數的值,而變數變為新值。

  • 這樣,開發者即可在 willSetdidSet 中進行相應的操作,如果只是取值和設值而不進行額外操作,那麼直接使用點語法即可。但是有時候一個變數只需要被訪問,而不能在外界賦值,那麼可以使用加上 (set) 即可私有化 set 方法。例如 fileprivate(set)private(set),以及 internal(set)。值得注意的是,這裡的訪問控制修飾符修飾的是 set 方法,訪問許可權(即 get)是另外設定的。例如 public fileprivate(set) var prop = 0,該變數全域性可以訪問,但只有同檔案內可以使用 set 方法。

Demo

struct Animal {    // internal 為預設許可權,可不加
    internal private(set) var privateSetProp = 0

    var hungryValue = 0 {        // 設定前呼叫
        willSet {            print("willSet (hungryValue) newValue: (newValue)")
        }        // 設定後呼叫
        didSet {            print("didSet (hungryValue) oldValue: (oldValue)")
        }        // 也可以自己命名預設的 newValue/oldValue
        // willSet(new) {}
        // didSet(old) {}
    }
}var cat = Animal()// private(set) 即只讀// cat.privateSetProp = 10print(cat.privateSetProp)

cat.hungryValue += 10print(cat.hungryValue)// 0// willSet 0 newValue: 10// didSet 10 oldValue: 0// 10

總結

Swift 的這幾個 feature 我未曾在其他語言中見過,對於初學者確實容易造成凌亂。特別是 getter/setter 以及屬性觀察器中均沒有程式碼提示,容易造成手誤,程式碼似乎也變得臃腫。但是熟悉之後,這些也都能完成之前的功能,甚至更加細分。保持每一部分可控,也使得整個程式更加嚴謹,更加安全。



作者:萌面大道
連結:


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/3349/viewspace-2820151/,如需轉載,請註明出處,否則將追究法律責任。

相關文章