屬性
屬性將值與特定的類、結(jié)構(gòu)體或枚舉關(guān)聯(lián)。存儲(chǔ)屬性會(huì)將常量和變量存儲(chǔ)為實(shí)例的一部分,而計(jì)算屬性則是直接計(jì)算(而不是存儲(chǔ))值。計(jì)算屬性可以用于類、結(jié)構(gòu)體和枚舉,而存儲(chǔ)屬性只能用于類和結(jié)構(gòu)體。
存儲(chǔ)屬性和計(jì)算屬性通常與特定類型的實(shí)例關(guān)聯(lián)。但是,屬性也可以直接與類型本身關(guān)聯(lián),這種屬性稱為類型屬性。
另外,還可以定義屬性觀察器來監(jiān)控屬性值的變化,以此來觸發(fā)自定義的操作。屬性觀察器可以添加到類本身定義的存儲(chǔ)屬性上,也可以添加到從父類繼承的屬性上。
你也可以利用屬性包裝器來復(fù)用多個(gè)屬性的 getter 和 setter 中的代碼。
存儲(chǔ)屬性
簡(jiǎn)單來說,一個(gè)存儲(chǔ)屬性就是存儲(chǔ)在特定類或結(jié)構(gòu)體實(shí)例里的一個(gè)常量或變量。存儲(chǔ)屬性可以是變量存儲(chǔ)屬性(用關(guān)鍵字 var 定義),也可以是常量存儲(chǔ)屬性(用關(guān)鍵字 let 定義)。
可以在定義存儲(chǔ)屬性的時(shí)候指定默認(rèn)值,請(qǐng)參考 默認(rèn)構(gòu)造器 一節(jié)。也可以在構(gòu)造過程中設(shè)置或修改存儲(chǔ)屬性的值,甚至修改常量存儲(chǔ)屬性的值,請(qǐng)參考 構(gòu)造過程中常量屬性的修改 一節(jié)。
下面的例子定義了一個(gè)名為 FixedLengthRange 的結(jié)構(gòu)體,該結(jié)構(gòu)體用于描述整數(shù)的區(qū)間,且這個(gè)范圍值在被創(chuàng)建后不能被修改。
struct FixedLengthRange {
var firstValue: Int
let length: Int
}
var rangeOfThreeItems = FixedLengthRange(firstValue: 0, length: 3)
// 該區(qū)間表示整數(shù) 0,1,2
rangeOfThreeItems.firstValue = 6
// 該區(qū)間現(xiàn)在表示整數(shù) 6,7,8
FixedLengthRange 的實(shí)例包含一個(gè)名為 firstValue 的變量存儲(chǔ)屬性和一個(gè)名為 length 的常量存儲(chǔ)屬性。在上面的例子中,length 在創(chuàng)建實(shí)例的時(shí)候被初始化,且之后無法修改它的值,因?yàn)樗且粋€(gè)常量存儲(chǔ)屬性。
常量結(jié)構(gòu)體實(shí)例的存儲(chǔ)屬性
如果創(chuàng)建了一個(gè)結(jié)構(gòu)體實(shí)例并將其賦值給一個(gè)常量,則無法修改該實(shí)例的任何屬性,即使被聲明為可變屬性也不行:
let rangeOfFourItems = FixedLengthRange(firstValue: 0, length: 4)
// 該區(qū)間表示整數(shù) 0,1,2,3
rangeOfFourItems.firstValue = 6
// 盡管 firstValue 是個(gè)可變屬性,但這里還是會(huì)報(bào)錯(cuò)
因?yàn)?rangeOfFourItems 被聲明成了常量(用 let 關(guān)鍵字),所以即使 firstValue 是一個(gè)可變屬性,也無法再修改它了。
這種行為是由于結(jié)構(gòu)體屬于值類型。當(dāng)值類型的實(shí)例被聲明為常量的時(shí)候,它的所有屬性也就成了常量。
屬于引用類型的類則不一樣。把一個(gè)引用類型的實(shí)例賦給一個(gè)常量后,依然可以修改該實(shí)例的可變屬性。
延時(shí)加載存儲(chǔ)屬性
延時(shí)加載存儲(chǔ)屬性是指當(dāng)?shù)谝淮伪徽{(diào)用的時(shí)候才會(huì)計(jì)算其初始值的屬性。在屬性聲明前使用 lazy 來標(biāo)示一個(gè)延時(shí)加載存儲(chǔ)屬性。
注意
必須將延時(shí)加載屬性聲明成變量(使用
var關(guān)鍵字),因?yàn)閷傩缘某跏贾悼赡茉趯?shí)例構(gòu)造完成之后才會(huì)得到。而常量屬性在構(gòu)造過程完成之前必須要有初始值,因此無法聲明成延時(shí)加載。
當(dāng)屬性的值依賴于一些外部因素且這些外部因素只有在構(gòu)造過程結(jié)束之后才會(huì)知道的時(shí)候,延時(shí)加載屬性就會(huì)很有用。或者當(dāng)獲得屬性的值因?yàn)樾枰獜?fù)雜或者大量的計(jì)算,而需要采用需要的時(shí)候再計(jì)算的方式,延時(shí)加載屬性也會(huì)很有用。
下面的例子使用了延時(shí)加載存儲(chǔ)屬性來避免復(fù)雜類中不必要的初始化工作。例子中定義了 DataImporter 和 DataManager 兩個(gè)類,下面是部分代碼:
class DataImporter {
/*
DataImporter 是一個(gè)負(fù)責(zé)將外部文件中的數(shù)據(jù)導(dǎo)入的類。
這個(gè)類的初始化會(huì)消耗不少時(shí)間。
*/
var fileName = "data.txt"
// 這里會(huì)提供數(shù)據(jù)導(dǎo)入功能
}
class DataManager {
lazy var importer = DataImporter()
var data = [String]()
// 這里會(huì)提供數(shù)據(jù)管理功能
}
let manager = DataManager()
manager.data.append("Some data")
manager.data.append("Some more data")
// DataImporter 實(shí)例的 importer 屬性還沒有被創(chuàng)建
DataManager 類包含一個(gè)名為 data 的存儲(chǔ)屬性,初始值是一個(gè)空的字符串?dāng)?shù)組。這里沒有給出全部代碼,只需知道 DataManager 類的目的是管理和提供對(duì)這個(gè)字符串?dāng)?shù)組的訪問即可。
DataManager 的一個(gè)功能是從文件中導(dǎo)入數(shù)據(jù)。這個(gè)功能由 DataImporter 類提供,DataImporter 完成初始化需要消耗不少時(shí)間:因?yàn)樗膶?shí)例在初始化時(shí)可能需要打開文件并讀取文件中的內(nèi)容到內(nèi)存中。
DataManager 管理數(shù)據(jù)時(shí)也可能不從文件中導(dǎo)入數(shù)據(jù)。所以當(dāng) DataManager 的實(shí)例被創(chuàng)建時(shí),沒必要?jiǎng)?chuàng)建一個(gè) DataImporter 的實(shí)例,更明智的做法是第一次用到 DataImporter 的時(shí)候才去創(chuàng)建它。
由于使用了 lazy,DataImporter 的實(shí)例 importer 屬性只有在第一次被訪問的時(shí)候才被創(chuàng)建。比如訪問它的屬性 fileName 時(shí):
print(manager.importer.fileName)
// DataImporter 實(shí)例的 importer 屬性現(xiàn)在被創(chuàng)建了
// 輸出“data.txt”
注意
如果一個(gè)被標(biāo)記為
lazy的屬性在沒有初始化時(shí)就同時(shí)被多個(gè)線程訪問,則無法保證該屬性只會(huì)被初始化一次。
存儲(chǔ)屬性和實(shí)例變量
如果您有過 Objective-C 經(jīng)驗(yàn),應(yīng)該知道 Objective-C 為類實(shí)例存儲(chǔ)值和引用提供兩種方法。除了屬性之外,還可以使用實(shí)例變量作為一個(gè)備份存儲(chǔ)將變量值賦值給屬性。
Swift 編程語言中把這些理論統(tǒng)一用屬性來實(shí)現(xiàn)。Swift 中的屬性沒有對(duì)應(yīng)的實(shí)例變量,屬性的備份存儲(chǔ)也無法直接訪問。這就避免了不同場(chǎng)景下訪問方式的困擾,同時(shí)也將屬性的定義簡(jiǎn)化成一個(gè)語句。屬性的全部信息——包括命名、類型和內(nèi)存管理特征——作為類型定義的一部分,都定義在一個(gè)地方。
計(jì)算屬性
除存儲(chǔ)屬性外,類、結(jié)構(gòu)體和枚舉可以定義計(jì)算屬性。計(jì)算屬性不直接存儲(chǔ)值,而是提供一個(gè) getter 和一個(gè)可選的 setter,來間接獲取和設(shè)置其他屬性或變量的值。
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))")
// 打印“square.origin is now at (10.0, 10.0)”
這個(gè)例子定義了 3 個(gè)結(jié)構(gòu)體來描述幾何形狀:
-
Point封裝了一個(gè)(x, y)的坐標(biāo) -
Size封裝了一個(gè)width和一個(gè)height -
Rect表示一個(gè)有原點(diǎn)和尺寸的矩形
Rect 也提供了一個(gè)名為 center 的計(jì)算屬性。一個(gè) Rect 的中心點(diǎn)可以從 origin(原點(diǎn))和 size(大?。┧愠觯圆恍枰獙⒅行狞c(diǎn)以 Point 類型的值來保存。Rect 的計(jì)算屬性 center 提供了自定義的 getter 和 setter 來獲取和設(shè)置矩形的中心點(diǎn),就像它有一個(gè)存儲(chǔ)屬性一樣。
上述例子中創(chuàng)建了一個(gè)名為 square 的 Rect 實(shí)例,初始值原點(diǎn)是 (0, 0),寬度高度都是 10。如下圖中藍(lán)色正方形所示。
square 的 center 屬性可以通過點(diǎn)運(yùn)算符(square.center)來訪問,這會(huì)調(diào)用該屬性的 getter 來獲取它的值。跟直接返回已經(jīng)存在的值不同,getter 實(shí)際上通過計(jì)算然后返回一個(gè)新的 Point 來表示 square 的中心點(diǎn)。如代碼所示,它正確返回了中心點(diǎn) (5, 5)。
center 屬性之后被設(shè)置了一個(gè)新的值 (15, 15),表示向右上方移動(dòng)正方形到如下圖橙色正方形所示的位置。設(shè)置屬性 center 的值會(huì)調(diào)用它的 setter 來修改屬性 origin 的 x 和 y 的值,從而實(shí)現(xiàn)移動(dòng)正方形到新的位置。

簡(jiǎn)化 Setter 聲明
如果計(jì)算屬性的 setter 沒有定義表示新值的參數(shù)名,則可以使用默認(rèn)名稱 newValue。下面是使用了簡(jiǎn)化 setter 聲明的 Rect 結(jié)構(gòu)體代碼:
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)
}
}
}
簡(jiǎn)化 Getter 聲明
如果整個(gè) getter 是單一表達(dá)式,getter 會(huì)隱式地返回這個(gè)表達(dá)式結(jié)果。下面是另一個(gè)版本的 Rect 結(jié)構(gòu)體,用到了簡(jiǎn)化的 getter 和 setter 聲明:
struct CompactRect {
var origin = Point()
var size = Size()
var center: Point {
get {
Point(x: origin.x + (size.width / 2),
y: origin.y + (size.height / 2))
}
set {
origin.x = newValue.x - (size.width / 2)
origin.y = newValue.y - (size.height / 2)
}
}
}
在 getter 中忽略 return 與在函數(shù)中忽略 return 的規(guī)則相同,請(qǐng)參考 隱式返回的函數(shù)。
只讀計(jì)算屬性
只有 getter 沒有 setter 的計(jì)算屬性叫只讀計(jì)算屬性。只讀計(jì)算屬性總是返回一個(gè)值,可以通過點(diǎn)運(yùn)算符訪問,但不能設(shè)置新的值。
注意
必須使用
var關(guān)鍵字定義計(jì)算屬性,包括只讀計(jì)算屬性,因?yàn)樗鼈兊闹挡皇枪潭ǖ摹?code>let 關(guān)鍵字只用來聲明常量屬性,表示初始化后再也無法修改的值。
只讀計(jì)算屬性的聲明可以去掉 get 關(guān)鍵字和花括號(hào):
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)")
// 打印“the volume of fourByFiveByTwo is 40.0”
這個(gè)例子定義了一個(gè)名為 Cuboid 的結(jié)構(gòu)體,表示三維空間的立方體,包含 width、height 和 depth 屬性。結(jié)構(gòu)體還有一個(gè)名為 volume 的只讀計(jì)算屬性用來返回立方體的體積。為 volume 提供 setter 毫無意義,因?yàn)闊o法確定如何修改 width、height 和 depth 三者的值來匹配新的 volume。然而,Cuboid 提供一個(gè)只讀計(jì)算屬性來讓外部用戶直接獲取體積是很有用的。
屬性觀察器
屬性觀察器監(jiān)控和響應(yīng)屬性值的變化,每次屬性被設(shè)置值的時(shí)候都會(huì)調(diào)用屬性觀察器,即使新值和當(dāng)前值相同的時(shí)候也不例外。
你可以為除了延時(shí)加載存儲(chǔ)屬性之外的其他存儲(chǔ)屬性添加屬性觀察器,你也可以在子類中通過重寫屬性的方式為繼承的屬性(包括存儲(chǔ)屬性和計(jì)算屬性)添加屬性觀察器。你不必為非重寫的計(jì)算屬性添加屬性觀察器,因?yàn)槟憧梢灾苯油ㄟ^它的 setter 監(jiān)控和響應(yīng)值的變化。屬性重寫請(qǐng)參考 重寫。
可以為屬性添加其中一個(gè)或兩個(gè)觀察器:
-
willSet在新的值被設(shè)置之前調(diào)用 -
didSet在新的值被設(shè)置之后調(diào)用
willSet 觀察器會(huì)將新的屬性值作為常量參數(shù)傳入,在 willSet 的實(shí)現(xiàn)代碼中可以為這個(gè)參數(shù)指定一個(gè)名稱,如果不指定則參數(shù)仍然可用,這時(shí)使用默認(rèn)名稱 newValue 表示。
同樣,didSet 觀察器會(huì)將舊的屬性值作為參數(shù)傳入,可以為該參數(shù)指定一個(gè)名稱或者使用默認(rèn)參數(shù)名 oldValue。如果在 didSet 方法中再次對(duì)該屬性賦值,那么新值會(huì)覆蓋舊的值。
注意
在父類初始化方法調(diào)用之后,在子類構(gòu)造器中給父類的屬性賦值時(shí),會(huì)調(diào)用父類屬性的
willSet和didSet觀察器。而在父類初始化方法調(diào)用之前,給子類的屬性賦值時(shí)不會(huì)調(diào)用子類屬性的觀察器。有關(guān)構(gòu)造器代理的更多信息,請(qǐng)參考 值類型的構(gòu)造器代理 和 類的構(gòu)造器代理。
下面是一個(gè) willSet 和 didSet 實(shí)際運(yùn)用的例子,其中定義了一個(gè)名為 StepCounter 的類,用來統(tǒng)計(jì)一個(gè)人步行時(shí)的總步數(shù)。這個(gè)類可以跟計(jì)步器或其他日常鍛煉的統(tǒng)計(jì)裝置的輸入數(shù)據(jù)配合使用。
class StepCounter {
var totalSteps: Int = 0 {
willSet(newTotalSteps) {
print("將 totalSteps 的值設(shè)置為 \(newTotalSteps)")
}
didSet {
if totalSteps > oldValue {
print("增加了 \(totalSteps - oldValue) 步")
}
}
}
}
let stepCounter = StepCounter()
stepCounter.totalSteps = 200
// 將 totalSteps 的值設(shè)置為 200
// 增加了 200 步
stepCounter.totalSteps = 360
// 將 totalSteps 的值設(shè)置為 360
// 增加了 160 步
stepCounter.totalSteps = 896
// 將 totalSteps 的值設(shè)置為 896
// 增加了 536 步
StepCounter 類定義了一個(gè)叫 totalSteps 的 Int 類型的屬性。它是一個(gè)存儲(chǔ)屬性,包含 willSet 和 didSet 觀察器。
當(dāng) totalSteps 被設(shè)置新值的時(shí)候,它的 willSet 和 didSet 觀察器都會(huì)被調(diào)用,即使新值和當(dāng)前值完全相同時(shí)也會(huì)被調(diào)用。
例子中的 willSet 觀察器將表示新值的參數(shù)自定義為 newTotalSteps,這個(gè)觀察器只是簡(jiǎn)單的將新的值輸出。
didSet 觀察器在 totalSteps 的值改變后被調(diào)用,它把新值和舊值進(jìn)行對(duì)比,如果總步數(shù)增加了,就輸出一個(gè)消息表示增加了多少步。didSet 沒有為舊值提供自定義名稱,所以默認(rèn)值 oldValue 表示舊值的參數(shù)名。
注意
如果將帶有觀察器的屬性通過 in-out 方式傳入函數(shù),
willSet和didSet也會(huì)調(diào)用。這是因?yàn)?in-out 參數(shù)采用了拷入拷出內(nèi)存模式:即在函數(shù)內(nèi)部使用的是參數(shù)的 copy,函數(shù)結(jié)束后,又對(duì)參數(shù)重新賦值。關(guān)于 in-out 參數(shù)詳細(xì)的介紹,請(qǐng)參考 輸入輸出參數(shù)。
屬性包裝器
屬性包裝器在管理屬性如何存儲(chǔ)和定義屬性的代碼之間添加了一個(gè)分隔層。舉例來說,如果你的屬性需要線程安全性檢查或者需要在數(shù)據(jù)庫中存儲(chǔ)它們的基本數(shù)據(jù),那么必須給每個(gè)屬性添加同樣的邏輯代碼。當(dāng)使用屬性包裝器時(shí),你只需在定義屬性包裝器時(shí)編寫一次管理代碼,然后應(yīng)用到多個(gè)屬性上來進(jìn)行復(fù)用。
定義一個(gè)屬性包裝器,你需要?jiǎng)?chuàng)建一個(gè)定義 wrappedValue 屬性的結(jié)構(gòu)體、枚舉或者類。在下面的代碼中,TwelveOrLess 結(jié)構(gòu)體確保它包裝的值始終是小于等于 12 的數(shù)字。如果要求它存儲(chǔ)一個(gè)更大的數(shù)字,它則會(huì)存儲(chǔ) 12 這個(gè)數(shù)字。
@propertyWrapper
struct TwelveOrLess {
private var number = 0
var wrappedValue: Int {
get { return number }
set { number = min(newValue, 12) }
}
}
這個(gè) setter 確保新值小于 12,而且返回被存儲(chǔ)的值。
注意
上面例子以
private的方式聲明number變量,這使得number僅在TwelveOrLess的實(shí)現(xiàn)中使用。寫在其他地方的代碼通過使用wrappedValue的 getter 和 setter 來獲取這個(gè)值,但不能直接使用number。有關(guān)private的更多信息,請(qǐng)參考 訪問控制
通過在屬性之前寫上包裝器名稱作為特性的方式,你可以把一個(gè)包裝器應(yīng)用到一個(gè)屬性上去。這里有個(gè)存儲(chǔ)小矩形的結(jié)構(gòu)體。通過 TwelveOrLess 屬性包裝器實(shí)現(xiàn)類似(挺隨意的)對(duì)“小”的定義。
struct SmallRectangle {
@TwelveOrLess var height: Int
@TwelveOrLess var width: Int
}
var rectangle = SmallRectangle()
print(rectangle.height)
// 打印 "0"
rectangle.height = 10
print(rectangle.height)
// 打印 "10"
rectangle.height = 24
print(rectangle.height)
// 打印 "12"
height 和 width 屬性從 TwelveOrLess 的定義中獲取它們的初始值。該定義把 TwelveOrLess.number 設(shè)置為 0。把數(shù)字 10 存進(jìn) rectangle.height 中的操作能成功,是因?yàn)閿?shù)字 10 很小。嘗試存儲(chǔ) 24 的操作實(shí)際上存儲(chǔ)的值為 12,這是因?yàn)閷?duì)于這個(gè)屬性的 setter 的規(guī)則來說,24 太大了。
當(dāng)你把一個(gè)包裝器應(yīng)用到一個(gè)屬性上時(shí),編譯器將合成提供包裝器存儲(chǔ)空間和通過包裝器訪問屬性的代碼。(屬性包裝器只負(fù)責(zé)存儲(chǔ)被包裝值,所以沒有合成這些代碼。)不利用這個(gè)特性語法的情況下,你可以寫出使用屬性包裝器行為的代碼。舉例來說,這是先前代碼清單中的 SmallRectangle 的另一個(gè)版本。這個(gè)版本將其屬性明確地包裝在 TwelveOrLess 結(jié)構(gòu)體中,而不是把 @TwelveOrLess 作為特性寫下來:
struct SmallRectangle {
private var _height = TwelveOrLess()
private var _width = TwelveOrLess()
var height: Int {
get { return _height.wrappedValue }
set { _height.wrappedValue = newValue }
}
var width: Int {
get { return _width.wrappedValue }
set { _width.wrappedValue = newValue }
}
}
_height 和 _width 屬性存著這個(gè)屬性包裝器的一個(gè)實(shí)例,即 TwelveOrLess。height 和 width 的 getter 和 setter 把對(duì) wrappedValue 屬性的訪問包裝起來。
設(shè)置被包裝屬性的初始值
上面例子中的代碼通過在 TwelveOrLess 的定義中賦予 number 一個(gè)初始值來設(shè)置被包裝屬性的初始值。使用這個(gè)屬性包裝器的代碼沒法為被 TwelveOrLess 包裝的屬性指定其他初始值。舉例來說,SmallRectangle 的定義沒法給 height 或者 width 一個(gè)初始值。為了支持設(shè)定一個(gè)初始值或者其他自定義操作,屬性包裝器需要添加一個(gè)構(gòu)造器。這是 TwelveOrLess 的擴(kuò)展版本,稱為 SmallNumber。SmallNumber 定義了能設(shè)置被包裝值和最大值的構(gòu)造器:
@propertyWrapper
struct SmallNumber {
private var maximum: Int
private var number: Int
var wrappedValue: Int {
get { return number }
set { number = min(newValue, maximum) }
}
init() {
maximum = 12
number = 0
}
init(wrappedValue: Int) {
maximum = 12
number = min(wrappedValue, maximum)
}
init(wrappedValue: Int, maximum: Int) {
self.maximum = maximum
number = min(wrappedValue, maximum)
}
}
SmallNumber 的定義包括三個(gè)構(gòu)造器——init()、init(wrappedValue:) 和 init(wrappedValue:maximum:)——下面的示例使用這三個(gè)構(gòu)造器來設(shè)置被包裝值和最大值。有關(guān)構(gòu)造過程和構(gòu)造器語法的更多信息,請(qǐng)參考 構(gòu)造過程。
當(dāng)你把包裝器應(yīng)用于屬性且沒有設(shè)定初始值時(shí),Swift 使用 init() 構(gòu)造器來設(shè)置包裝器。舉個(gè)例子:
struct ZeroRectangle {
@SmallNumber var height: Int
@SmallNumber var width: Int
}
var zeroRectangle = ZeroRectangle()
print(zeroRectangle.height, zeroRectangle.width)
// 打印 "0 0"
調(diào)用 SmallNumber() 來創(chuàng)建包裝 height 和 width 的 SmallNumber 的實(shí)例。構(gòu)造器內(nèi)部的代碼使用默認(rèn)值 0 和 12 設(shè)置初始的被包裝值和初始的最大值。像之前使用在 SmallRectangle 中使用 TwelveOrLess 的例子,這個(gè)屬性包裝器仍然提供所有的初始值。與這個(gè)例子不同的是,SmallNumber 也支持把編寫這些初始值作為聲明屬性的一部分。
當(dāng)你為屬性指定初始值時(shí),Swift 使用 init(wrappedValue:) 構(gòu)造器來設(shè)置包裝器。舉個(gè)例子:
struct UnitRectangle {
@SmallNumber var height: Int = 1
@SmallNumber var width: Int = 1
}
var unitRectangle = UnitRectangle()
print(unitRectangle.height, unitRectangle.width)
// 打印 "1 1"
當(dāng)你對(duì)一個(gè)被包裝的屬性寫下 = 1 時(shí),這被轉(zhuǎn)換為調(diào)用 init(wrappedValue:) 構(gòu)造器。調(diào)用 SmallNumber(wrappedValue: 1)來創(chuàng)建包裝 height 和 width 的 SmallNumber 的實(shí)例。構(gòu)造器使用此處指定的被包裝值,且使用的默認(rèn)最大值為 12。
當(dāng)你在自定義特性后面把實(shí)參寫在括號(hào)里時(shí),Swift 使用接受這些實(shí)參的構(gòu)造器來設(shè)置包裝器。舉例來說,如果你提供初始值和最大值,Swift 使用 init(wrappedValue:maximum:) 構(gòu)造器:
struct NarrowRectangle {
@SmallNumber(wrappedValue: 2, maximum: 5) var height: Int
@SmallNumber(wrappedValue: 3, maximum: 4) var width: Int
}
var narrowRectangle = NarrowRectangle()
print(narrowRectangle.height, narrowRectangle.width)
// 打印 "2 3"
narrowRectangle.height = 100
narrowRectangle.width = 100
print(narrowRectangle.height, narrowRectangle.width)
// 打印 "5 4"
調(diào)用 SmallNumber(wrappedValue: 2, maximum: 5) 來創(chuàng)建包裝 height 的 SmallNumber 的一個(gè)實(shí)例。調(diào)用 SmallNumber(wrappedValue: 3, maximum: 4) 來創(chuàng)建包裝 width 的 SmallNumber 的一個(gè)實(shí)例。
通過將實(shí)參包含到屬性包裝器中,你可以設(shè)置包裝器的初始狀態(tài),或者在創(chuàng)建包裝器時(shí)傳遞其他的選項(xiàng)。這種語法是使用屬性包裝器最通用的方法。你可以為這個(gè)屬性提供任何所需的實(shí)參,且它們將被傳遞給構(gòu)造器。
當(dāng)包含屬性包裝器實(shí)參時(shí),你也可以使用賦值來指定初始值。Swift 將賦值視為 wrappedValue 參數(shù),且使用接受被包含的實(shí)參的構(gòu)造器。舉個(gè)例子:
struct MixedRectangle {
@SmallNumber var height: Int = 1
@SmallNumber(maximum: 9) var width: Int = 2
}
var mixedRectangle = MixedRectangle()
print(mixedRectangle.height)
// 打印 "1"
mixedRectangle.height = 20
print(mixedRectangle.height)
// 打印 "12"
調(diào)用 SmallNumber(wrappedValue: 1) 來創(chuàng)建包裝 height 的 SmallNumber 的一個(gè)實(shí)例,這個(gè)實(shí)例使用默認(rèn)最大值 12。調(diào)用 SmallNumber(wrappedValue: 2, maximum: 9) 來創(chuàng)建包裝 width 的 SmallNumber的一個(gè)實(shí)例。
從屬性包裝器中呈現(xiàn)一個(gè)值
除了被包裝值,屬性包裝器可以通過定義被呈現(xiàn)值暴露出其他功能。舉個(gè)例子,管理對(duì)數(shù)據(jù)庫的訪問的屬性包裝器可以在它的被呈現(xiàn)值上暴露出 flushDatabaseConnection() 方法。除了以貨幣符號(hào)($)開頭,被呈現(xiàn)值的名稱和被包裝值是一樣的。因?yàn)槟愕拇a不能夠定義以 $ 開頭的屬性,所以被呈現(xiàn)值永遠(yuǎn)不會(huì)與你定義的屬性有沖突。
在之前 SmallNumber 的例子中,如果你嘗試把這個(gè)屬性設(shè)置為一個(gè)很大的數(shù)值,屬性包裝器會(huì)在存儲(chǔ)這個(gè)數(shù)值之前調(diào)整這個(gè)數(shù)值。以下的代碼把被呈現(xiàn)值添加到 SmallNumber 結(jié)構(gòu)體中來追蹤在存儲(chǔ)新值之前屬性包裝器是否為這個(gè)屬性調(diào)整了新值。
注意:
wrappedValue和projectedValue為內(nèi)置字段不可以隨意修改為其他自定義字段;另外projectedValue可以自定義數(shù)據(jù)類型,如 private(set) var projectedValue: String
//官方寫法
@propertyWrapper
struct SmallNumber {
private var number = 0
private(set) var projectedValue: Bool
var wrappedValue: Int {
get { return number }
set {
if newValue > 12 {
number = 12
projectedValue = true
} else {
number = newValue
projectedValue = false
}
}
}
init() {
self.number = 0
self.projectedValue = false
}
}
//簡(jiǎn)易寫法
//不寫init方法時(shí),不可以將屬性定義為private,否則報(bào)錯(cuò),如:Private property 'projectedValue' cannot have more restrictive access than its enclosing property wrapper type 'SmallNumber' (which is internal)
//@propertyWrapper
//struct SmallNumber {
// var number = 0
// var projectedValue: Bool = false
// var wrappedValue: Int {
// get { return number }
// set {
// if newValue > 12 {
// number = 12
// projectedValue = true
// } else {
// number = newValue
// projectedValue = false
// }
// }
// }
//}
struct SomeStructure {
@SmallNumber var someNumber: Int
}
var someStructure = SomeStructure()
someStructure.someNumber = 4
print(someStructure.$someNumber)
// 打印 "false"
someStructure.someNumber = 55
print(someStructure.$someNumber)
// 打印 "true"
寫下 s.$someNumber 即可訪問包裝器的被呈現(xiàn)值。在存儲(chǔ)一個(gè)比較小的數(shù)值時(shí),如 4 ,s.$someNumber 的值為 false。但是,在嘗試存儲(chǔ)一個(gè)較大的數(shù)值時(shí),如 55 ,被呈現(xiàn)值變?yōu)?true。
屬性包裝器可以返回任何類型的值作為它的被呈現(xiàn)值。在這個(gè)例子里,屬性包裝器要暴露的信息是:那個(gè)數(shù)值是否被調(diào)整過,所以它暴露出布爾型值來作為它的被呈現(xiàn)值。需要暴露出更多信息的包裝器可以返回其他數(shù)據(jù)類型的實(shí)例,或者可以返回自身來暴露出包裝器的實(shí)例,并把其作為它的被呈現(xiàn)值。
當(dāng)從類型的一部分代碼中訪問被呈現(xiàn)值,例如屬性 getter 或?qū)嵗椒?,你可以在屬性名稱之前省略 self.,就像訪問其他屬性一樣。以下示例中的代碼用 $height 和 $width 引用包裝器 height 和 width 的被呈現(xiàn)值:
enum Size {
case small, large
}
struct SizedRectangle {
@SmallNumber var height: Int
@SmallNumber var width: Int
mutating func resize(to size: Size) -> Bool {
switch size {
case .small:
height = 10
width = 20
case .large:
height = 100
width = 100
}
return $height || $width
}
}
因?yàn)閷傩园b器語法只是具有 getter 和 setter 的屬性的語法糖,所以訪問 height 和 width 的行為與訪問任何其他屬性的行為相同。舉個(gè)例子,resize(to:) 中的代碼使用它們的屬性包裝器來訪問 height 和 width。如果調(diào)用 resize(to: .large),.large 的 switch case 分支語句把矩形的高度和寬度設(shè)置為 100。屬性包裝器防止這些屬性的值大于 12,且把被呈現(xiàn)值設(shè)置成為 true 來記下它調(diào)整過這些值的事實(shí)。在 resize(to:) 的最后,返回語句檢查 $height 和 $width 來確認(rèn)是否屬性包裝器調(diào)整過 height 或 width。
全局變量和局部變量
計(jì)算屬性和觀察屬性所描述的功能也可以用于全局變量和局部變量。全局變量是在函數(shù)、方法、閉包或任何類型之外定義的變量。局部變量是在函數(shù)、方法或閉包內(nèi)部定義的變量。
前面章節(jié)提到的全局或局部變量都屬于存儲(chǔ)型變量,跟存儲(chǔ)屬性類似,它為特定類型的值提供存儲(chǔ)空間,并允許讀取和寫入。
另外,在全局或局部范圍都可以定義計(jì)算型變量和為存儲(chǔ)型變量定義觀察器。計(jì)算型變量跟計(jì)算屬性一樣,返回一個(gè)計(jì)算結(jié)果而不是存儲(chǔ)值,聲明格式也完全一樣。
注意
全局的常量或變量都是延遲計(jì)算的,跟 延時(shí)加載存儲(chǔ)屬性 相似,不同的地方在于,全局的常量或變量不需要標(biāo)記
lazy修飾符。局部范圍的常量和變量從不延遲計(jì)算。
類型屬性
實(shí)例屬性屬于一個(gè)特定類型的實(shí)例,每創(chuàng)建一個(gè)實(shí)例,實(shí)例都擁有屬于自己的一套屬性值,實(shí)例之間的屬性相互獨(dú)立。
你也可以為類型本身定義屬性,無論創(chuàng)建了多少個(gè)該類型的實(shí)例,這些屬性都只有唯一一份。這種屬性就是類型屬性。
類型屬性用于定義某個(gè)類型所有實(shí)例共享的數(shù)據(jù),比如所有實(shí)例都能用的一個(gè)常量(就像 C 語言中的靜態(tài)常量),或者所有實(shí)例都能訪問的一個(gè)變量(就像 C 語言中的靜態(tài)變量)。
存儲(chǔ)型類型屬性可以是變量或常量,計(jì)算型類型屬性跟實(shí)例的計(jì)算型屬性一樣只能定義成變量屬性。
注意
跟實(shí)例的存儲(chǔ)型屬性不同,必須給存儲(chǔ)型類型屬性指定默認(rèn)值,因?yàn)轭愋捅旧頉]有構(gòu)造器,也就無法在初始化過程中使用構(gòu)造器給類型屬性賦值。
存儲(chǔ)型類型屬性是延遲初始化的,它們只有在第一次被訪問的時(shí)候才會(huì)被初始化。即使它們被多個(gè)線程同時(shí)訪問,系統(tǒng)也保證只會(huì)對(duì)其進(jìn)行一次初始化,并且不需要對(duì)其使用
lazy修飾符。
類型屬性語法
在 C 或 Objective-C 中,與某個(gè)類型關(guān)聯(lián)的靜態(tài)常量和靜態(tài)變量,是作為 global(全局)靜態(tài)變量定義的。但是在 Swift 中,類型屬性是作為類型定義的一部分寫在類型最外層的花括號(hào)內(nèi),因此它的作用范圍也就在類型支持的范圍內(nèi)。
使用關(guān)鍵字 static 來定義類型屬性。在為類定義計(jì)算型類型屬性時(shí),可以改用關(guān)鍵字 class 來支持子類對(duì)父類的實(shí)現(xiàn)進(jìn)行重寫。下面的例子演示了存儲(chǔ)型和計(jì)算型類型屬性的語法:
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
}
}
注意
例子中的計(jì)算型類型屬性是只讀的,但也可以定義可讀可寫的計(jì)算型類型屬性,跟計(jì)算型實(shí)例屬性的語法相同。
獲取和設(shè)置類型屬性的值
跟實(shí)例屬性一樣,類型屬性也是通過點(diǎn)運(yùn)算符來訪問。但是,類型屬性是通過類型本身來訪問,而不是通過實(shí)例。比如:
print(SomeStructure.storedTypeProperty)
// 打印“Some value.”
SomeStructure.storedTypeProperty = "Another value."
print(SomeStructure.storedTypeProperty)
// 打印“Another value.”
print(SomeEnumeration.computedTypeProperty)
// 打印“6”
print(SomeClass.computedTypeProperty)
// 打印“27”
下面的例子定義了一個(gè)結(jié)構(gòu)體,使用兩個(gè)存儲(chǔ)型類型屬性來表示兩個(gè)聲道的音量,每個(gè)聲道具有 0 到 10 之間的整數(shù)音量。
下圖展示了如何把兩個(gè)聲道結(jié)合來模擬立體聲的音量。當(dāng)聲道的音量是 0,沒有一個(gè)燈會(huì)亮;當(dāng)聲道的音量是 10,所有燈點(diǎn)亮。本圖中,左聲道的音量是 9,右聲道的音量是 7:

上面所描述的聲道模型使用 AudioChannel 結(jié)構(gòu)體的實(shí)例來表示:
struct AudioChannel {
static let thresholdLevel = 10
static var maxInputLevelForAllChannels = 0
var currentLevel: Int = 0 {
didSet {
if currentLevel > AudioChannel.thresholdLevel {
// 將當(dāng)前音量限制在閾值之內(nèi)
currentLevel = AudioChannel.thresholdLevel
}
if currentLevel > AudioChannel.maxInputLevelForAllChannels {
// 存儲(chǔ)當(dāng)前音量作為新的最大輸入音量
AudioChannel.maxInputLevelForAllChannels = currentLevel
}
}
}
}
AudioChannel 結(jié)構(gòu)定義了 2 個(gè)存儲(chǔ)型類型屬性來實(shí)現(xiàn)上述功能。第一個(gè)是 thresholdLevel,表示音量的最大上限閾值,它是一個(gè)值為 10 的常量,對(duì)所有實(shí)例都可見,如果音量高于 10,則取最大上限值 10(見后面描述)。
第二個(gè)類型屬性是變量存儲(chǔ)型屬性 maxInputLevelForAllChannels,它用來表示所有 AudioChannel 實(shí)例的最大輸入音量,初始值是 0。
AudioChannel 也定義了一個(gè)名為 currentLevel 的存儲(chǔ)型實(shí)例屬性,表示當(dāng)前聲道現(xiàn)在的音量,取值為 0 到 10。
屬性 currentLevel 包含 didSet 屬性觀察器來檢查每次設(shè)置后的屬性值,它做如下兩個(gè)檢查:
- 如果
currentLevel的新值大于允許的閾值thresholdLevel,屬性觀察器將currentLevel的值限定為閾值thresholdLevel。 - 如果修正后的
currentLevel值大于靜態(tài)類型屬性maxInputLevelForAllChannels的值,屬性觀察器就將新值保存在maxInputLevelForAllChannels中。
注意
在第一個(gè)檢查過程中,
didSet屬性觀察器將currentLevel設(shè)置成了不同的值,但這不會(huì)造成屬性觀察器被再次調(diào)用。
可以使用結(jié)構(gòu)體 AudioChannel 創(chuàng)建兩個(gè)聲道 leftChannel 和 rightChannel,用以表示立體聲系統(tǒng)的音量:
var leftChannel = AudioChannel()
var rightChannel = AudioChannel()
如果將左聲道的 currentLevel 設(shè)置成 7,類型屬性 maxInputLevelForAllChannels 也會(huì)更新成 7:
leftChannel.currentLevel = 7
print(leftChannel.currentLevel)
// 輸出“7”
print(AudioChannel.maxInputLevelForAllChannels)
// 輸出“7”
如果試圖將右聲道的 currentLevel 設(shè)置成 11,它會(huì)被修正到最大值 10,同時(shí) maxInputLevelForAllChannels 的值也會(huì)更新到 10:
rightChannel.currentLevel = 11
print(rightChannel.currentLevel)
// 輸出“10”
print(AudioChannel.maxInputLevelForAllChannels)
// 輸出“10”