淺談 Swift 中的屬性(Property)

萌面大道發表於2017-04-29

由於種種原因,掘金等第三方平臺部落格不再保證能夠同步更新,歡迎移步 GitHub:github.com/kingcos/Per…。謝謝!

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 中的這兩種屬性,以及屬性觀察器。

延遲儲存屬性

  • 儲存屬性使用廣泛,即是類或結構體中的變數或常量,可以直接賦初始值,也可以修改其初始值(僅指變數)。
  • 延遲儲存屬性是指第一次使用到該變數再進行運算(這裡的運算不能依賴其他成員屬性,但可以使用靜態/類屬性)。
  • 延遲儲存屬性必須宣告為 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 初始化的「演變」過程

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 方法

  • @Onevcat 的 Swifter Tips 中也提到,在 Swift 標準庫中,也有一些 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)
}
// OUTPUT:
// Begin:
// Handling...
// 0
// Handling...
// 2
// Handling...
// 4
// Handling...
// 6
複製程式碼

devxoul/Then

Demo

ViewController.swift

import UIKit
import Then

class 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 = 100
rect.height = 50
print(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 = 10
print(cat.privateSetProp)

cat.hungryValue += 10
print(cat.hungryValue)

// 0
// willSet 0 newValue: 10
// didSet 10 oldValue: 0
// 10
複製程式碼

總結

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

參考

也歡迎您關注我的微博 @萌面大道V & 簡書

相關文章