由於種種原因,掘金等第三方平臺部落格不再保證能夠同步更新,歡迎移步 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 初始化的「演變」過程
- 根據上面 Demo,延遲儲存屬性的初始化程式碼部分可能有些讓人迷惑,但其實也是初始化的一步步的演變過程。在 @Onetaway 的 【菜雞Playground 1】Swift 中 lazy initialization 中也有描述這個過程,簡單用程式碼表示也如下所示:
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
- 在 @沒故事的卓同學的【菜雞Playground 2】Swift 中 lazy initialization 的使用場景中提到了一個 devxoul/Then 庫,為 Swift 的構造方法加入語法糖。
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(©)
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
等於賦值前變數的值,而變數變為新值。 - 這樣,開發者即可在
willSet
和didSet
中進行相應的操作,如果只是取值和設值而不進行額外操作,那麼直接使用點語法即可。但是有時候一個變數只需要被訪問,而不能在外界賦值,那麼可以使用訪問控制修飾符加上(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 以及屬性觀察器中均沒有程式碼提示,容易造成手誤,程式碼似乎也變得臃腫。但是熟悉之後,這些也都能完成之前的功能,甚至更加細分。保持每一部分可控,也使得整個程式更加嚴謹,更加安全。
參考
- 淺談 Swift 3 中的訪問控制
- Access Control
- Properties
- 【菜雞Playground 1】Swift 中 lazy initialization
- 【菜雞Playground 2】Swift 中 lazy initialization 的使用場景
- devxoul/Then
- lazy 修飾符和 lazy 方法