Swift 中的屬性

SwiftGG翻譯組發表於2018-09-18

作者:Thomas Hanning,原文連結,原文日期:2018-03-15 譯者:Sunnyyoung;校對:小鐵匠Linusnumbbbbb;定稿:Forelax

Swift 中有兩種型別的屬性:儲存屬性與計算屬性。儲存屬性將值(常量或者變數)儲存為例項或型別的一部分,而計算屬性沒有儲存值。

提示:這篇文章已經更新至 Swift 4。

儲存屬性

讓我們從儲存屬性開始看起。想象一下你有一個名為 Circle 的類:

class Circle {

    var radius: Double = 0

}

let circle = Circle()
circle.radius = 10

print("radius: \(circle.radius)") //radius: 10.0
複製程式碼

Circle 擁有名為 radius 的例項變數,預設值為 0。在 Swift 中,每個例項變數都為一個屬性。因此你可以新增所謂的屬性觀察者。在 Swift 中有兩種型別的屬性觀察者:一種在賦值之前呼叫,另一種在賦值之後呼叫。

在賦值後呼叫的屬性觀察者採用 didSet 關鍵字標記。在我們的示例中,你可以使用它來監測新設定的值:

class Circle {
    
    var radius: Double = 0 {
        didSet {
            if radius < 0 {
                radius = oldValue
            }
        }
    }
    
}
 
let circle = Circle()
 
circle.radius = -10
print("radius: \(circle.radius)") //radius: 0.0
 
circle.radius = 10
print("radius: \(circle.radius)") //radius: 10.0
複製程式碼

在屬性觀察者中你可以通過變數 oldValue 來訪問屬性的舊值。

你還可以使用 willSet 屬性觀察者,它在賦值之前會被呼叫:

class Circle {
    
    var radius: Double = 0 {
        willSet {
            print("About to assign the new value \(newValue)")
        }
        didSet {
            if radius < 0 {
                radius = oldValue
            }
        }
    }
    
}
 
let circle = Circle()
 
circle.radius = 10 //設定新值 10.0
複製程式碼

willSet 中,你可以通過變數 newValue 來訪問屬性的新值。

計算屬性

與儲存屬性不同的是,計算屬性並不會儲存屬性的值。因此在每次呼叫計算屬性時,都要計算該值。在 Circle 類中,你可以將屬性 area 定義為計算屬性:

class Circle {
    
    var radius: Double = 0 {
        didSet {
            if radius < 0 {
                radius = oldValue
            }
        }
    }
    
    var area: Double {
        get {
            return radius * radius * Double.pi
        }
    }

}

let circle = Circle()
circle.radius = 5

print("area: \(circle.area)") //area: 78.5398163397448
複製程式碼

計算屬性總是需要一個 getter。如果缺少 setter,則該屬性被稱為只讀屬性。下面這個例子很好地說明了 setter 的作用:

import Foundation

class Circle {
    
    var radius: Double = 0 {
        didSet {
            if radius < 0 {
                radius = oldValue
            }
        }
    }
    
    var area: Double {
        get {
            return radius * radius * Double.pi
        }
        set(newArea) {
            radius = sqrt(newArea / Double.pi)
        }
    }
    
}

let circle = Circle()

circle.area = 25

print("radius: \(circle.radius)") //radius: 2.82094791773878
複製程式碼

至此,每次對 area 設定了新的值之後,radius 都會被重新計算。

儲存屬性的初始化

每個儲存屬性在它的物件例項化之後都必須有值。屬性初始化有兩種方法:

  • init 方法中初始化值
  • 給屬性設定預設的值

下面的例子同時使用了這兩種方法:

class Circle {
    
    var radius: Double
    var identifier: Int = 0
    
    init(radius: Double) {
        self.radius = radius
    }
    
}

var circle = Circle(radius: 5)
複製程式碼

如果儲存屬性在物件例項化之後沒有值,程式碼無法通過編譯。

懶載入屬性

如果具有預設值的儲存屬性使用了關鍵字 lazy 標記,則其預設值不會立即初始化,而是在第一次訪問該屬性時初始化。

因此,如果該屬性從未被訪問,它將永遠不會被初始化。你可以將這種特性應用於一些特別耗費 CPU 或記憶體的初始化上。

class TestClass {
    
    lazy var testString: String = "TestString"
    
}
 
let testClass = TestClass()
print(testClass.testString) //TestString
複製程式碼

該屬性在被訪問之前不會進行初始化。在這個例子中並不容易看出來。但由於初始化也可以在 block 裡面實現,我們可以使它更明顯一些:

class TestClass {
    
    lazy var testString: String = {
        print("about to initialize the property")
        return "TestString"
    }()
    
}

let testClass = TestClass()
print("before first call")
print(testClass.testString)
print(testClass.testString)
複製程式碼

這個例子的輸出:

before first call
about to initialize the property
TestString
TestString
複製程式碼

這意味著該 block 僅被呼叫一次 - 第一次訪問該屬性的時候。由於儲存屬性是可變的,因此可以更改初始值。

型別屬性

型別屬性是類的一部分,但不是例項的一部分,型別屬性也被稱為靜態屬性。儲存屬性和計算屬性都可以是型別屬性。型別屬性的關鍵字是 static

class TestClass {
    
    static var testString: String = "TestString"
    
}
 
print("\(TestClass.testString)") //TestString
複製程式碼

如你所見,它們使用類名而不是例項物件來訪問它們。此外,由於型別屬性沒有初始化方法,它總是需要一個預設值。

擁有私有 Setter 的公共屬性

正如我在 另一篇文章 中介紹的那樣,這是一種常見的情況,你不想提供一個公共的 setter,而是提供一個私有的 setter。這是封裝的基本原則。這樣只有類本身可以操作該屬性,但仍可從類外部訪問讀取它。

來看下面的例子:

public class Circle {
    
    public private(set) var area: Double = 0
    public private(set) var diameter: Double = 0
    
    public var radius: Double {
        didSet {
            calculateFigures()
        }
    }
    
    public init(radius:Double) {
        self.radius = radius
        calculateFigures()
    }
    
    private func calculateFigures() {
        area = Double.pi * radius * radius
        diameter = 2 * Double.pi * radius
    }
}

let circle = Circle(radius: 5)

print("area: \(circle.area)") //area: 78.5398163397448
print("diameter: \(circle.diameter)") //diameter: 31.4159265358979

circle.area = 10 //編譯錯誤:無法對 'area' 屬性進行賦值,因為 setter 方法不可訪問
複製程式碼

這裡的屬性 areadiameter 可以從類的外部訪問,但只能在類內部賦值。為此你必須使用 public private(set) 的組合。根據本人的經驗,這個特性在 iOS 開發中很少使用,但它對寫出更少 bug 的程式碼很有幫助。

本文由 SwiftGG 翻譯組翻譯,已經獲得作者翻譯授權,最新文章請訪問 swift.gg

相關文章