Property屬性將值與特定類型的類、結構體、枚舉關聯。
分類一(根據是否存儲):
-
Stored properties 存儲屬性:
將常量和變量值存儲為實例的一部分,適用于 類、結構、 枚舉。 -
Computed properties 計算屬性 :
計算值而不是存儲值,適用于類、結構。
分類二 (根據調用者,是實例,還是類型)
- Type properties 可以是 類,結構體
存儲和計算的屬性通常與特定類型的實例關聯。然而,屬性也可以與類型本身相關聯。這些屬性稱為類型屬性。
此外,您還可以定義屬性觀察者來監視屬性值中的更改,您可以使用自定義操作來響應這些更改。屬性觀察者可以添加到您自己定義的存儲屬性中,也可以添加到子類從超類繼承的屬性中。
Stored Properties 存儲屬性
在其最簡單的形式中,存儲屬性是作為特定類或結構的實例的一部分存儲的常量或變量。存儲屬性可以是變量存儲屬性(由var關鍵字引入),也可以是常量存儲屬性(由let關鍵字引入)。
您可以為存儲的屬性提供默認值作為其定義的一部分。還可以在初始化期間設置和修改存儲屬性的初始值。即使對于常量存儲的屬性也是如此。
下面的例子定義了一個名為FixedLengthRange的結構,它描述了一個整數的范圍,其范圍長度在創建后不能改變:
struct FixedLengthRange {
var firstValue: Int
let length: Int
}
var rangeOfThreeItems = FixedLengthRange(firstValue: 0, length: 3)
print(rangeOfThreeItems.firstValue)
print(rangeOfThreeItems.length)
rangeOfThreeItems.firstValue = 1
print(rangeOfThreeItems.firstValue)
print(rangeOfThreeItems.length)
Int 類型的length 使用let 聲明為常量,所以無法修改。
int 類型的firstValue 使用 var 聲明為變量,所以可以修改
Lazy Stored Properties 懶惰存儲屬性
懶惰存儲屬性是直到第一次使用它時才計算其初始值的屬性,該過程又被稱為懶加載。通過在聲明前編寫 lazy修飾符,可以指示延遲存儲屬性。
注意:必須始終將惰性屬性聲明為變量(使用var關鍵字),因為在實例初始化完成之前,可能無法檢索它的初始值。常量屬性在初始化完成之前必須始終有一個值,因此不能聲明為惰性屬性。
當屬性的初始值依賴于外部因素時,延遲屬性非常有用,這些外部因素的值在實例初始化完成后才知道。當屬性的初始值需要復雜的或計算開銷較大的設置時,惰性屬性也很有用,除非或者直到需要時才應該執行這些設置。
下面的示例使用惰性存儲屬性來避免對復雜類進行不必要的初始化。這個例子定義了兩個類DataImporter和DataManager,這兩個類都沒有完整顯示:
class DataImporter {
/*
DataImporter is a class to import data from an external file.
The class is assumed to take a nontrivial amount of time to initialize.
*/
var filename = "data.txt"
// the DataImporter class would provide data importing functionality here
}
class DataManager {
lazy var importer = DataImporter()
var data = [String]()
// the DataManager class would provide data management functionality here
}
let manager = DataManager()
manager.data.append("Some data")
manager.data.append("Some more data")
// the DataImporter instance for the importer property has not yet been created
DataManager類有一個名為data的存儲屬性,它是用一個新的空字符串值數組初始化的。雖然沒有顯示它的其他功能,但是這個DataManager類的目的是管理和提供對這個字符串數據數組的訪問。
DataManager類的部分功能是能夠從文件導入數據。這個功能是由DataImporter類提供的,它的初始化花費了大量的時間。這可能是因為DataImporter實例需要打開一個文件,并在初始化DataImporter實例時將其內容讀入內存。
DataManager實例可以在不從文件導入數據的情況下管理其數據,因此在創建DataManager本身時不需要創建新的DataImporter實例。相反,在第一次使用DataImporter實例時創建它更有意義。
因為它是用惰性修飾符標記的,所以只有在第一次訪問導入器屬性時,例如查詢其filename屬性時,才會創建導入器屬性的DataImporter實例:
print(manager.importer.filename)
// the DataImporter instance for the importer property has now been created
// Prints "data.txt"
注意: 如果標記為惰性修飾符的屬性被多個線程同時訪問,且該屬性尚未初始化,則不能保證該屬性只初始化一次。
Stored Properties and Instance Variables 存儲屬性和實例變量
如果您有使用Objective-C的經驗,您可能知道它提供了兩種方法來將值和引用存儲為類實例的一部分。除了屬性之外,還可以使用實例變量作為存儲在屬性中的值的后備存儲。
Swift將這些概念統一為一個屬性聲明。Swift屬性沒有對應的實例變量,并且不能直接訪問屬性的備份存儲。這種方法避免了在不同上下文中如何訪問值的混淆,并將屬性的聲明簡化為一個確定的語句。關于屬性的所有信息(包括它的名稱、類型和內存管理特性)都在一個位置中定義為類型定義的一部分。
Computed Properties 計算屬性
除了存儲屬性之外,類、結構和枚舉還可以定義計算屬性,而這些計算屬性實際上并不存儲值。相反,它們提供一個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)
print("square.origin is now at (\(square.origin.x), \(square.origin.y))")
// Prints "square.origin is now at (10.0, 10.0)"
這個例子定義了三種處理幾何形狀的結構:
- Point 封裝點的x坐標和y坐標。
- Size 封裝了寬度和高度。
- Rect 通過點和大小定義矩形。
Rect結構還提供了一個名為center的計算屬性。Rect的當前中心位置始終可以從它的原點和大小確定,因此不需要將中心點存儲為顯式的點值。相反,Rect為計算變量center定義了自定義getter和setter,使您能夠像處理實際存儲的屬性一樣處理矩形的center。
上面的示例創建了一個名為square的新Rect變量。square變量初始化的原點為(0,0),寬度和高度為10。這個正方形由下圖中的藍色正方形表示。
然后通過點語法(square.center)訪問square變量的center屬性,從而調用center的getter來檢索當前屬性值。getter實際計算并返回一個表示正方形中心的新點,而不是返回一個現有值。如上所示,getter正確地返回一個中心點(5,5)。
然后將center屬性設置為一個新值(15,15),該值將正方形向右上方移動,移動到下圖中橙色正方形所示的新位置。設置center屬性將調用center的setter,該setter修改存儲的origin屬性的x和y值,并將正方形移動到它的新位置。
Shorthand Setter Declaration 簡寫setter方法聲明
如果計算屬性的setter沒有為要設置的新值定義名稱,則使用newValue的默認名稱。這是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)
}
}
}
Read-Only Computed Properties 只讀的計算屬性
帶有getter但沒有setter的計算屬性稱為只讀計算屬性。
只讀計算屬性總是返回一個值,可以通過點語法訪問,但不能設置為不同的值。
注意:必須使用var關鍵字將計算屬性(包括只讀計算屬性)聲明為變量屬性,因為它們的值不是固定的。let關鍵字僅用于常量屬性,以指示一旦將它們設置為實例初始化的一部分,就不能更改它們的值。
您可以通過刪除get關鍵字及其括號來簡化只讀計算屬性的聲明:
struct Cuboid {
var width = 0.0, height = 0.0, depth = 0.0
var volume: Double {
return width * height * depth
}
}
let fourByFiveByTwo = Cuboid(width: 4.0, height: 5.0, depth: 2.0)
print("the volume of fourByFiveByTwo is \(fourByFiveByTwo.volume)")
上面volume是只讀的計算屬性
Property Observers 屬性觀察者
屬性觀察者觀察并響應屬性值的變化。
每次設置屬性值時都會調用屬性觀察者,即使新值與屬性的當前值相同。
可以將屬性觀察者添加到定義的任何存儲屬性中,lazy存儲屬性除外。還可以通過覆蓋子類中的屬性將屬性觀察者添加到任何繼承的屬性(無論是存儲的還是計算的)。您不需要為未覆蓋的計算屬性定義屬性觀察員,因為您可以在計算屬性的setter中觀察并響應對其值的更改。屬性重寫在重寫中描述。
你可以選擇在一個屬性上定義其中一個或兩個觀察者:
- willSet 將在存儲值之前調用willSet。
- didSet 在存儲新值之后立即調用didSet。
如果您實現一個willSet觀察者,它會將新的屬性值作為常量參數傳遞。您可以為該參數指定一個名稱,作為willSet實現的一部分。如果沒有在實現中編寫參數名和括號,則可以使用默認參數名newValue來提供參數。
類似地,如果實現didSet觀察者,它會傳遞一個包含舊屬性值的常量參數。可以為參數命名,也可以使用oldValue的默認參數名。如果在屬性自身的didSet觀察者中為屬性賦值,則賦值的新值將替換剛剛設置的值。
注意:調用超類初始化器后,在子類初始化器中設置屬性時調用超類屬性的willSet和didSet觀察者。在調用超類初始化器之前,類在設置自己的屬性時不會調用它們。
有關初始化器委托的更多信息,請參考 Initializer Delegation for Value Types 和 Initializer Delegation for Class Types。
這里有一個willSet和didSet的例子。下面的示例定義了一個名為StepCounter的新類,它跟蹤一個人走路時的總步數。這個類可以與計步器或其他計步器的輸入數據一起使用,以跟蹤一個人在日常活動中的鍛煉情況。
class StepCounter {
var totalSteps: Int = 0 {
willSet(newTotalSteps) {
print("About to set totalSteps to \(newTotalSteps)")
}
didSet {
if totalSteps > oldValue {
print("Added \(totalSteps - oldValue) steps")
}
}
}
}
let stepCounter = StepCounter()
stepCounter.totalSteps = 200
stepCounter.totalSteps = 360
stepCounter.totalSteps = 896
StepCounter類聲明一個Int類型的totalSteps屬性。這是一個帶有willSet和didSet觀察者的存儲屬性。
每當為屬性賦值時,都會調用totalSteps的willSet和didSet觀察者。即使新值與當前值相同,也是如此。
本例的willSet觀察者為即將到來的新值使用了newTotalSteps的自定義參數名。在本例中,它只是打印將要設置的值。
更新totalSteps的值之后,將調用didSet觀察者。它將totalSteps的新值與舊值進行比較。如果步驟總數增加了,就會打印一條消息來指示已經采取了多少新步驟。didSet觀察者不為舊值提供自定義參數名,而是使用默認的oldValue名稱。
注意:如果將具有觀察者的屬性作為in-out參數傳遞給函數,則始終調用willSet和didSet觀察者。這是因為in-out參數的copy-in - copy-out內存模型:值總是在函數結束時寫回屬性。有關in-out參數行為的詳細討論,請參見 In-Out Parameters。
Global and Local Variables 全局變量和局部變量
上面描述的計算和觀察屬性的功能也適用于全局變量和局部變量。全局變量是定義在任何函數、方法、閉包或類型上下文之外的變量。局部變量是在函數、方法或閉包上下文中定義的變量。
您在前幾章中遇到的全局變量和局部變量都已存儲為變量。與已存儲的屬性一樣,已存儲的變量為特定類型的值提供存儲,并允許設置和檢索該值。
不過,還可以在全局或局部范圍內定義計算變量和存儲變量的觀察者。計算變量計算它們的值,而不是存儲它,并且它們與計算屬性以相同的方式編寫。
注意:全局常量和變量的計算總是延遲的,這與lazy存儲屬性類似。與惰性存儲屬性不同,全局常量和變量不需要用lazy 修飾符標記。 局部常量和變量的計算從不是延遲的。
Type Properties 類型屬性
實例屬性是屬于特定類型實例的屬性。每次創建該類型的新實例時,它都有自己的一組屬性值,與任何其他實例分開。
您還可以定義屬于類型本身的屬性,而不是屬于該類型的任何一個實例。無論您創建了多少這種類型的實例,這些屬性都只有一個副本。這些類型的屬性稱為類型屬性。
屬性用于定義值類型是普遍的某一特定類型的所有實例,如一個常數所有實例的屬性可以使用(如靜態常數C),或一個變量屬性存儲一個值,該類型的所有實例都是全局的(像一個靜態變量C)。
存儲類型屬性可以是變量或常量。
計算類型屬性始終聲明為變量屬性,方法與實例計算屬性相同。
與實例存儲屬性不同,必須始終為類型存儲屬性提供默認值。這是因為類型本身沒有可以在初始化時為存儲的類型屬性分配值的初始化器。
存儲的類型屬性在第一次訪問時惰性地初始化。它們保證只初始化一次,即使被多個線程同時訪問,也不需要使用lazy修飾符來標記它們。
Type Property Syntax 類型屬性的語法
在C和Objective-C中,將與類型關聯的靜態常量和變量定義為全局靜態變量。然而,在Swift中,類型屬性是作為類型定義的一部分編寫的,在類型的外部花括號中,每個類型屬性都顯式地限定為其支持的類型的范圍。
使用static關鍵字定義類型屬性。對于類類型的計算類型屬性,可以使用class關鍵字來允許子類覆蓋超類的實現。下面的示例顯示了存儲和計算類型屬性的語法:
struct SomeStructure {
static var storedTypeProperty = "Some value."
static var computedTypeProperty: Int {
return 1
}
}
enum SomeEnumeration {
static var storedTypeProperty = "Some value."
static var computedTypeProperty: Int {
return 6
}
}
class SomeClass {
static var storedTypeProperty = "Some value."
static var computedTypeProperty: Int {
return 27
}
class var overrideableComputedTypeProperty: Int {
return 107
}
}
注意:上面的類型計算屬性示例用于只讀計算類型屬性,但是您也可以使用與實例計算屬性相同的語法定義讀寫類型計算屬性。
Querying and Setting Type Properties 查詢和設置類型屬性
類型屬性使用點語法查詢和設置,就像實例屬性一樣。然而,類型屬性是在類型上查詢和設置的,而不是在該類型的實例上。例如:
print(SomeStructure.storedTypeProperty)
// Prints "Some value."
SomeStructure.storedTypeProperty = "Another value."
print(SomeStructure.storedTypeProperty)
// Prints "Another value."
print(SomeEnumeration.computedTypeProperty)
// Prints "6"
print(SomeClass.computedTypeProperty)
// Prints "27"
下面的示例使用兩個類型存儲屬性作為結構的一部分,該結構為多個音頻通道建模音頻電平表。每個通道都有一個整數音頻級別,范圍在0到10之間。
下圖說明了如何將這兩個音頻通道組合在一起來為立體聲音頻電平表建模。當一個通道的音頻水平= 0時,該通道的燈光都不亮。當音頻是10級,所有通道的燈光都亮。在這個圖中,左邊通道的當前水平9日和右通道的當前水平7:
上述音頻通道由音頻通道結構的實例表示:
struct AudioChannel {
static let thresholdLevel = 10
static var maxInputLevelForAllChannels = 0
var currentLevel: Int = 0 {
didSet {
if currentLevel > AudioChannel.thresholdLevel {
// cap the new audio level to the threshold level
currentLevel = AudioChannel.thresholdLevel
}
if currentLevel > AudioChannel.maxInputLevelForAllChannels {
// store this as the new overall maximum input level
AudioChannel.maxInputLevelForAllChannels = currentLevel
}
}
}
}
AudioChannel結構定義了兩個存儲類型屬性來支持其功能。第一個閾值level定義了音頻級別可以接受的最大閾值。對于所有AudioChannel實例,這是一個常量值10。如果音頻信號的值高于10,它將被限制為這個閾值(如下所述)。
第二個類型屬性是一個名為maxInputLevelForAllChannels的變量存儲屬性。這將跟蹤任何AudioChannel實例接收到的最大輸入值。它的初始值為0。
AudioChannel結構還定義了一個名為currentLevel的存儲實例屬性,它表示通道當前的音頻級別,范圍從0到10。
currentLevel屬性有一個didSet屬性觀察者,每當設置currentLevel的值時,這個觀察者都會檢查它的值。
- 如果currentLevel的新值大于允許的閾值級別,則屬性觀察者將currentLevel的上限設置為閾值級別。
- 如果currentLevel的新值(在任何capping之后)高于任何AudioChannel實例之前接收到的任何值,則屬性觀察者將新currentLevel值存儲在maxInputLevelForAllChannels類型屬性中。
在這兩個檢查中的第一個檢查中,didSet觀察者將currentLevel設置為一個不同的值。但是,這不會導致再次調用觀察者。
您可以使用AudioChannel結構創建兩個名為leftChannel和rightChannel的新音頻通道,以表示立體聲系統的音頻級別:
var leftChannel = AudioChannel()
var rightChannel = AudioChannel()
如果將左側通道的currentLevel設置為7,可以看到maxInputLevelForAllChannels類型屬性被更新為等于7:
leftChannel.currentLevel = 7
print(leftChannel.currentLevel)
// Prints "7"
print(AudioChannel.maxInputLevelForAllChannels)
// Prints "7"
如果您嘗試將正確通道的currentLevel設置為11,您可以看到正確通道的currentLevel屬性被限制為最大值10,并且maxInputLevelForAllChannels類型屬性被更新為等于10:
rightChannel.currentLevel = 11
print(rightChannel.currentLevel)
// Prints "10"
print(AudioChannel.maxInputLevelForAllChannels)
// Prints "10"