屬性
屬性將值跟特定的類、結構或枚舉關聯。
存儲屬性
簡單來說,一個存儲屬性就是存儲在特定類或結構體實例里的一個常量或變量。存儲屬性可以是變量存儲屬性(用關鍵字 var
定義),也可以是常量存儲屬性(用關鍵字 let
定義)。
可以在定義存儲屬性的時候指定默認值。也可以在構造過程中設置或修改存儲屬性的值。
struct FixedLengthRange {
var firstValue: Int
let length: Int
}
var rangeOfThreeItems = FixedLengthRange(firstValue: 0, length: 3)
// 該區間表示整數0,1,2
rangeOfThreeItems.firstValue = 6
// 該區間現在表示整數6,7,8
FixedLengthRange
的實例包含一個名為 firstValue
的變量存儲屬性和一個名為 length
的常量存儲屬性。在上面的例子中,length
在創建實例的時候被初始化,因為它是一個常量存儲屬性,所以之后無法修改它的值。
常量結構體的存儲屬性
如果創建了一個結構體的實例并將其賦值給一個常量,則無法修改該實例的任何屬性,即使有屬性被聲明為變量也不行:
let rangeOfFourItems = FixedLengthRange(firstValue: 0, length: 4)
// 該區間表示整數0,1,2,3
rangeOfFourItems.firstValue = 6
// 盡管 firstValue 是個變量屬性,這里還是會報錯
因為 rangeOfFourItems
被聲明成了常量(用 let
關鍵字),即使 firstValue
是一個變量屬性,也無法再修改它了。
這種行為是由于結構體(struct)屬于值類型。當值類型的實例被聲明為常量的時候,它的所有屬性也就成了常量。
屬于引用類型的類(class)則不一樣。把一個引用類型的實例賦給一個常量后,仍然可以修改該實例的變量屬性。
延遲存儲屬性(懶加載)
延遲存儲屬性是指當第一次被調用的時候才會計算其初始值的屬性。在屬性聲明前使用 lazy
來標示一個延遲存儲屬性。
注意
必須將延遲存儲屬性聲明成變量(使用var
關鍵字),因為屬性的初始值可能在實例構造完成之后才會得到。而常量屬性在構造過程完成之前必須要有初始值,因此無法聲明成延遲屬性。
延遲屬性很有用,當屬性的值依賴于在實例的構造過程結束后才會知道影響值的外部因素時,或者當獲得屬性的初始值需要復雜或大量計算時,可以只在需要的時候計算它。
class DataImporter {
var fileName:String? = "data.txt"
init() {
print("創建DataImporter")
}
}
class DataManager {
lazy var importer = DataImporter()
var data = [String]()
}
let manager = DataManager()
manager.importer.fileName = "fileName" // print: 創建DataImporter
在OC中懶加載是判斷是否為nil, 若為nil則創建, 而在Swift中 用lazy標記的屬性只有在第一次被訪問的時候才被創建。
注意
如果一個被標記為lazy
的屬性在沒有初始化時就同時被多個線程訪問,則無法保證該屬性只會被初始化一次。
計算屬性
除存儲屬性外,類、結構體和枚舉可以定義計算屬性。計算屬性不直接存儲值,而是提供一個 getter 和一個可選的 setter,來間接獲取和設置其他屬性或變量的值。
struct Point { // 坐標
var x = 0.0, y = 0.0
}
struct Size { // 大小
var width = 0.0, height = 0.0
}
struct Rect { // 矩形
var origin = Point()
var size = Size()
var center: Point {
get {
let centerX = origin.x + (size.width / 2)
let centerY = origin.y + (size.height / 2)
return Point(x: centerX, y: centerY)
}
set(newCenter) {
origin.x = newCenter.x - (size.width / 2)
origin.y = newCenter.y - (size.height / 2)
}
}
}
var square = Rect(origin: Point(x: 0.0, y: 0.0),
size: Size(width: 10.0, height: 10.0))
let initialSquareCenter = square.center
square.center = Point(x: 15.0, y: 15.0)
這個例子定義了 3 個結構體來描述幾何形狀:
-
Point
封裝了一個(x, y)
的坐標 -
Size
封裝了一個width
和一個height
-
Rect
表示一個有原點和尺寸的矩形
Rect
也提供了一個名為center
的計算屬性。一個矩形的中心點可以從原點(origin
)和大小(size
)算出,所以不需要將它以顯式聲明的 Point
來保存。Rect
的計算屬性 center
提供了自定義的 getter 和 setter 來獲取和設置矩形的中心點,就像它有一個存儲屬性一樣。
便捷 setter 聲明
如果計算屬性的 setter 沒有定義表示新值的參數名,則可以使用默認名稱 newValue
。下面是使用了便捷 setter 聲明的 Rect
結構體代碼:
struct AlternativeRect {
var origin = Point()
var size = Size()
var center: Point {
get {
let centerX = origin.x + (size.width / 2)
let centerY = origin.y + (size.height / 2)
return Point(x: centerX, y: centerY)
}
set {
origin.x = newValue.x - (size.width / 2)
origin.y = newValue.y - (size.height / 2)
}
}
}
只讀計算屬性
只有 getter 沒有 setter 的計算屬性就是只讀計算屬性。只讀計算屬性總是返回一個值,可以通過點運算符訪問,但不能設置新的值。
注意
必須使用var
關鍵字定義計算屬性,包括只讀計算屬性,因為它們的值不是固定的。let
關鍵字只用來聲明常量屬性,表示初始化后再也無法修改的值。
只讀計算屬性的聲明可以去掉 get
關鍵字和花括號:
struct Cuboid {
var width = 0.0, height = 0.0, depth = 0.0
var volume: Double {
// get {
return width * height * depth
// }
}
}
let fourByFiveByTwo = Cuboid(width: 4.0, height: 5.0, depth: 2.0)
print("the volume of fourByFiveByTwo is \(fourByFiveByTwo.volume)")
// 輸出 "the volume of fourByFiveByTwo is 40.0"
這個例子定義了一個名為 Cuboid
的結構體,表示三維空間的立方體,包含 width
、height
和 depth
屬性。結構體還有一個名為 volume
的只讀計算屬性用來返回立方體的體積。為 volume
提供 setter 毫無意義,因為無法確定如何修改 width
、height
和 depth
三者的值來匹配新的 volume
。然而,Cuboid
提供一個只讀計算屬性來讓外部用戶直接獲取體積是很有用的。
屬性觀察器
屬性觀察器監控和響應屬性值的變化,每次屬性被設置值的時候都會調用屬性觀察器,即使新值和當前值相同的時候也不例外。
可以為除了延遲存儲屬性之外的其他存儲屬性添加屬性觀察器,也可以通過重寫屬性的方式為繼承的屬性(包括存儲屬性和計算屬性)添加屬性觀察器。你不必為非重寫的計算屬性添加屬性觀察器,因為可以通過它的 setter 直接監控和響應值的變化。
可以為屬性添加如下的一個或全部觀察器:
-
willSet
在新的值被設置之前調用 -
didSet
在新的值被設置之后立即調用
willSet
觀察器會將新的屬性值作為常量參數傳入,在 willSet
的實現代碼中可以為這個參數指定一個名稱,如果不指定則參數仍然可用,這時使用默認名稱 newValue
表示。
同樣,didSet
觀察器會將舊的屬性值作為參數傳入,可以為該參數命名或者使用默認參數名 oldValue
。如果在 didSet
方法中再次對該屬性賦值,那么新值會覆蓋舊的值。
注意
父類的屬性在子類的構造器中被賦值時,它在父類中的willSet
和didSet
觀察器會被調用,隨后才會調用子類的觀察器。在父類初始化方法調用之前,子類給屬性賦值時,觀察器不會被調用。
下面是一個 willSet
和 didSet
實際運用的例子,其中定義了一個名為 StepCounter
的類,用來統計一個人步行時的總步數。這個類可以跟計步器或其他日常鍛煉的統計裝置的輸入數據配合使用。
class StepCounter {
var totalSteps: Int = 0 {
willSet(newTotalSteps) {
print("totalSteps 設置為 \(newTotalSteps)")
}
didSet {
if totalSteps > oldValue {
print("增加了 \(totalSteps - oldValue) 步")
}
}
}
}
let stepCounter = StepCounter()
stepCounter.totalSteps = 200
// print: totalSteps 設置為 200
// print: 增加了 200 步
stepCounter.totalSteps = 360
// print: totalSteps 設置為 360
// print: 增加了 160 步
stepCounter.totalSteps = 896
// print: totalSteps 設置為 896
// print: 增加了 536 步
StepCounter
類定義了一個 Int
類型的屬性 totalSteps
,它是一個存儲屬性,包含 willSet
和 didSet
觀察器。
當 totalSteps
被設置新值的時候,它的 willSet
和 didSet
觀察器都會被調用,即使新值和當前值完全相同時也會被調用。
例子中的 willSet
觀察器將表示新值的參數自定義為 newTotalSteps
,這個觀察器只是簡單的將新的值輸出。
didSet
觀察器在 totalSteps
的值改變后被調用,它把新值和舊值進行對比,如果總步數增加了,就輸出一個消息表示增加了多少步。didSet
沒有為舊值提供自定義名稱,所以默認值 oldValue
表示舊值的參數名。
注意
如果將屬性通過 in-out 方式傳入函數,
willSet
和didSet
也會調用。這是因為 in-out 參數采用了拷入拷出模式:即在函數內部使用的是參數的 copy,函數結束后,又對參數重新賦值。
func change( a: inout Int) {
a += 1
}
change(a: &stepCounter.totalSteps)
// totalSteps 設置為 897
// 增加了 1 步