# 屬性
*屬性*將值與特定的類、結構體或枚舉關聯。存儲屬性會將常量和變量存儲為實例的一部分,而計算屬性則是直接計算(而不是存儲)值。計算屬性可以用于類、結構體和枚舉,而存儲屬性只能用于類和結構體。
存儲屬性和計算屬性通常與特定類型的實例關聯。但是,屬性也可以直接與類型本身關聯,這種屬性稱為類型屬性。
另外,還可以定義屬性觀察器來監控屬性值的變化,以此來觸發自定義的操作。屬性觀察器可以添加到類本身定義的存儲屬性上,也可以添加到從父類繼承的屬性上。
你也可以利用屬性包裝器來復用多個屬性的 getter 和 setter 中的代碼。
## 存儲屬性 {#stored-properties}
簡單來說,一個存儲屬性就是存儲在特定類或結構體實例里的一個常量或變量。存儲屬性可以是*變量存儲屬性*(用關鍵字 `var` 定義),也可以是*常量存儲屬性*(用關鍵字 `let` 定義)。
可以在定義存儲屬性的時候指定默認值,請參考 [默認構造器](./14_Initialization.md#default-initializers) 一節。也可以在構造過程中設置或修改存儲屬性的值,甚至修改常量存儲屬性的值,請參考 [構造過程中常量屬性的修改](./14_Initialization.md#assigning-constant-properties-during-initialization) 一節。
下面的例子定義了一個名為 `FixedLengthRange` 的結構體,該結構體用于描述整數的區間,且這個范圍值在被創建后不能被修改。
```swift
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` 在創建實例的時候被初始化,且之后無法修改它的值,因為它是一個常量存儲屬性。
### 常量結構體實例的存儲屬性 {#stored-properties-of-constant-structure-instances}
如果創建了一個結構體實例并將其賦值給一個常量,則無法修改該實例的任何屬性,即使被聲明為可變屬性也不行:
```swift
let rangeOfFourItems = FixedLengthRange(firstValue: 0, length: 4)
// 該區間表示整數 0,1,2,3
rangeOfFourItems.firstValue = 6
// 盡管 firstValue 是個可變屬性,但這里還是會報錯
```
因為 `rangeOfFourItems` 被聲明成了常量(用 `let` 關鍵字),所以即使 `firstValue` 是一個可變屬性,也無法再修改它了。
這種行為是由于結構體屬于*值類型*。當值類型的實例被聲明為常量的時候,它的所有屬性也就成了常量。
屬于*引用類型*的類則不一樣。把一個引用類型的實例賦給一個常量后,依然可以修改該實例的可變屬性。
### 延時加載存儲屬性 {#lazy-stored-properties}
*延時加載存儲屬性*是指當第一次被調用的時候才會計算其初始值的屬性。在屬性聲明前使用 `lazy` 來標示一個延時加載存儲屬性。
> 注意
>
> 必須將延時加載屬性聲明成變量(使用 `var` 關鍵字),因為屬性的初始值可能在實例構造完成之后才會得到。而常量屬性在構造過程完成之前必須要有初始值,因此無法聲明成延時加載。
當屬性的值依賴于一些外部因素且這些外部因素只有在構造過程結束之后才會知道的時候,延時加載屬性就會很有用。或者當獲得屬性的值因為需要復雜或者大量的計算,而需要采用需要的時候再計算的方式,延時加載屬性也會很有用。
下面的例子使用了延時加載存儲屬性來避免復雜類中不必要的初始化工作。例子中定義了 `DataImporter` 和 `DataManager` 兩個類,下面是部分代碼:
```swift
class DataImporter {
/*
DataImporter 是一個負責將外部文件中的數據導入的類。
這個類的初始化會消耗不少時間。
*/
var fileName = "data.txt"
// 這里會提供數據導入功能
}
class DataManager {
lazy var importer = DataImporter()
var data = [String]()
// 這里會提供數據管理功能
}
let manager = DataManager()
manager.data.append("Some data")
manager.data.append("Some more data")
// DataImporter 實例的 importer 屬性還沒有被創建
```
`DataManager` 類包含一個名為 `data` 的存儲屬性,初始值是一個空的字符串數組。這里沒有給出全部代碼,只需知道 `DataManager` 類的目的是管理和提供對這個字符串數組的訪問即可。
`DataManager` 的一個功能是從文件中導入數據。這個功能由 `DataImporter` 類提供,`DataImporter` 完成初始化需要消耗不少時間:因為它的實例在初始化時可能需要打開文件并讀取文件中的內容到內存中。
`DataManager` 管理數據時也可能不從文件中導入數據。所以當 `DataManager` 的實例被創建時,沒必要創建一個 `DataImporter` 的實例,更明智的做法是第一次用到 `DataImporter` 的時候才去創建它。
由于使用了 `lazy`,`DataImporter` 的實例 `importer` 屬性只有在第一次被訪問的時候才被創建。比如訪問它的屬性 `fileName` 時:
```swift
print(manager.importer.fileName)
// DataImporter 實例的 importer 屬性現在被創建了
// 輸出“data.txt”
```
> 注意
>
> 如果一個被標記為 `lazy` 的屬性在沒有初始化時就同時被多個線程訪問,則無法保證該屬性只會被初始化一次。
### 存儲屬性和實例變量 {#stored-properties-and-instance-variables}
如果你有過 Objective-C 經驗,應該知道 Objective-C 為類實例存儲值和引用提供兩種方法。除了屬性之外,還可以使用實例變量作為一個備份存儲將變量值賦值給屬性。
Swift 編程語言中把這些理論統一用屬性來實現。Swift 中的屬性沒有對應的實例變量,屬性的備份存儲也無法直接訪問。這就避免了不同場景下訪問方式的困擾,同時也將屬性的定義簡化成一個語句。屬性的全部信息——包括命名、類型和內存管理特征——作為類型定義的一部分,都定義在一個地方。
## 計算屬性 {#computed-properties}
除存儲屬性外,類、結構體和枚舉可以定義*計算屬性*。計算屬性不直接存儲值,而是提供一個 getter 和一個可選的 setter,來間接獲取和設置其他屬性或變量的值。
```swift
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)”
```
這個例子定義了 3 個結構體來描述幾何形狀:
- `Point` 封裝了一個 `(x, y)` 的坐標
- `Size` 封裝了一個 `width` 和一個 `height`
- `Rect` 表示一個有原點和尺寸的矩形
`Rect` 也提供了一個名為 `center` 的計算屬性。一個 `Rect` 的中心點可以從 `origin`(原點)和 `size`(大小)算出,所以不需要將中心點以 `Point` 類型的值來保存。`Rect` 的計算屬性 `center` 提供了自定義的 getter 和 setter 來獲取和設置矩形的中心點,就像它有一個存儲屬性一樣。
上述例子中創建了一個名為 `square` 的 `Rect` 實例,初始值原點是 `(0, 0)`,寬度高度都是 `10`。如下圖中藍色正方形所示。
`square` 的 `center` 屬性可以通過點運算符(`square.center`)來訪問,這會調用該屬性的 getter 來獲取它的值。跟直接返回已經存在的值不同,getter 實際上通過計算然后返回一個新的 `Point` 來表示 `square` 的中心點。如代碼所示,它正確返回了中心點 `(5, 5)`。
`center` 屬性之后被設置了一個新的值 `(15, 15)`,表示向右上方移動正方形到如下圖橙色正方形所示的位置。設置屬性 `center` 的值會調用它的 setter 來修改屬性 `origin` 的 `x` 和 `y` 的值,從而實現移動正方形到新的位置。
<img src="https://docs.swift.org/swift-book/_images/computedProperties_2x.png" alt="Computed Properties sample" width="388" height="387" />
### 簡化 Setter 聲明 {#shorthand-setter-declaration}
如果計算屬性的 setter 沒有定義表示新值的參數名,則可以使用默認名稱 `newValue`。下面是使用了簡化 setter 聲明的 `Rect` 結構體代碼:
```swift
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 聲明 {#shorthand-getter-declaration}
如果整個 getter 是單一表達式,getter 會隱式地返回這個表達式結果。下面是另一個版本的 `Rect` 結構體,用到了簡化的 getter 和 setter 聲明:
```swift
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` 與在函數中忽略 `return` 的規則相同,請參考 [隱式返回的函數](./06_Functions.md/#functions-with-an-implicit-return)。
### 只讀計算屬性 {#readonly-computed-properties}
只有 getter 沒有 setter 的計算屬性叫*只讀計算屬性*。只讀計算屬性總是返回一個值,可以通過點運算符訪問,但不能設置新的值。
> 注意
>
> 必須使用 `var` 關鍵字定義計算屬性,包括只讀計算屬性,因為它們的值不是固定的。`let` 關鍵字只用來聲明常量屬性,表示初始化后再也無法修改的值。
只讀計算屬性的聲明可以去掉 `get` 關鍵字和花括號:
```swift
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”
```
這個例子定義了一個名為 `Cuboid` 的結構體,表示三維空間的立方體,包含 `width`、`height` 和 `depth` 屬性。結構體還有一個名為 `volume` 的只讀計算屬性用來返回立方體的體積。為 `volume` 提供 setter 毫無意義,因為無法確定如何修改 `width`、`height` 和 `depth` 三者的值來匹配新的 `volume`。然而,`Cuboid` 提供一個只讀計算屬性來讓外部用戶直接獲取體積是很有用的。
## 屬性觀察器 {#property-observers}
屬性觀察器監控和響應屬性值的變化,每次屬性被設置值的時候都會調用屬性觀察器,即使新值和當前值相同的時候也不例外。
你可以在以下位置添加屬性觀察器:
* 自定義的存儲屬性
* 繼承的存儲屬性
* 繼承的計算屬性
對于繼承的屬性,你可以在子類中通過重寫屬性的方式為它添加屬性觀察器。對于自定義的計算屬性來說,使用它的 setter 監控和響應值的變化,而不是嘗試創建觀察器。屬性重寫請參考 [重寫](./13_Inheritance.md#overriding)。
可以為屬性添加其中一個或兩個觀察器:
- `willSet` 在新的值被設置之前調用
- `didSet` 在新的值被設置之后調用
`willSet` 觀察器會將新的屬性值作為常量參數傳入,在 `willSet` 的實現代碼中可以為這個參數指定一個名稱,如果不指定則參數仍然可用,這時使用默認名稱 `newValue` 表示。
同樣,`didSet` 觀察器會將舊的屬性值作為參數傳入,可以為該參數指定一個名稱或者使用默認參數名 `oldValue`。如果在 `didSet` 方法中再次對該屬性賦值,那么新值會覆蓋舊的值。
> 注意
>
> 在父類初始化方法調用之后,在子類構造器中給父類的屬性賦值時,會調用父類屬性的 `willSet` 和 `didSet` 觀察器。而在父類初始化方法調用之前,給子類的屬性賦值時不會調用子類屬性的觀察器。
>
> 有關構造器代理的更多信息,請參考 [值類型的構造器代理](./14_Initialization.md#initializer-delegation-for-value-types) 和 [類的構造器代理](./14_Initialization.md#initializer-delegation-for-class-types)。
下面是一個 `willSet` 和 `didSet` 實際運用的例子,其中定義了一個名為 `StepCounter` 的類,用來統計一個人步行時的總步數。這個類可以跟計步器或其他日常鍛煉的統計裝置的輸入數據配合使用。
```swift
class StepCounter {
var totalSteps: Int = 0 {
willSet(newTotalSteps) {
print("將 totalSteps 的值設置為 \(newTotalSteps)")
}
didSet {
if totalSteps > oldValue {
print("增加了 \(totalSteps - oldValue) 步")
}
}
}
}
let stepCounter = StepCounter()
stepCounter.totalSteps = 200
// 將 totalSteps 的值設置為 200
// 增加了 200 步
stepCounter.totalSteps = 360
// 將 totalSteps 的值設置為 360
// 增加了 160 步
stepCounter.totalSteps = 896
// 將 totalSteps 的值設置為 896
// 增加了 536 步
```
`StepCounter` 類定義了一個叫 `totalSteps` 的 `Int` 類型的屬性。它是一個存儲屬性,包含 `willSet` 和 `didSet` 觀察器。
當 `totalSteps` 被設置新值的時候,它的 `willSet` 和 `didSet` 觀察器都會被調用,即使新值和當前值完全相同時也會被調用。
例子中的 `willSet` 觀察器將表示新值的參數自定義為 `newTotalSteps`,這個觀察器只是簡單的將新的值輸出。
`didSet` 觀察器在 `totalSteps` 的值改變后被調用,它把新值和舊值進行對比,如果總步數增加了,就輸出一個消息表示增加了多少步。`didSet` 沒有為舊值提供自定義名稱,所以默認值 `oldValue` 表示舊值的參數名。
> 注意
>
> 如果將帶有觀察器的屬性通過 in-out 方式傳入函數,`willSet` 和 `didSet` 也會調用。這是因為 in-out 參數采用了拷入拷出內存模式:即在函數內部使用的是參數的 copy,函數結束后,又對參數重新賦值。關于 in-out 參數詳細的介紹,請參考 [輸入輸出參數](../03_language_reference/06_Declarations.md#in-out-parameters)。
## 屬性包裝器 {#property-wrappers}
屬性包裝器在管理屬性如何存儲和定義屬性的代碼之間添加了一個分隔層。舉例來說,如果你的屬性需要線程安全性檢查或者需要在數據庫中存儲它們的基本數據,那么必須給每個屬性添加同樣的邏輯代碼。當使用屬性包裝器時,你只需在定義屬性包裝器時編寫一次管理代碼,然后應用到多個屬性上來進行復用。
定義一個屬性包裝器,你需要創建一個定義 `wrappedValue` 屬性的結構體、枚舉或者類。在下面的代碼中,`TwelveOrLess` 結構體確保它包裝的值始終是小于等于 12 的數字。如果要求它存儲一個更大的數字,它則會存儲 12 這個數字。
```swift
@propertyWrapper
struct TwelveOrLess {
private var number: Int
init() { self.number = 0 }
var wrappedValue: Int {
get { return number }
set { number = min(newValue, 12) }
}
}
```
這個 setter 確保新值小于 12,而且返回被存儲的值。
> 注意
>
> 上面例子以 `private` 的方式聲明 `number` 變量,這使得 `number` 僅在 `TwelveOrLess` 的實現中使用。寫在其他地方的代碼通過使用 `wrappedValue` 的 getter 和 setter 來獲取這個值,但不能直接使用 `number`。有關 `private` 的更多信息,請參考 [訪問控制](./26_Access_Control.md)
通過在屬性之前寫上包裝器名稱作為特性的方式,你可以把一個包裝器應用到一個屬性上去。這里有個存儲小矩形的結構體。通過 `TwelveOrLess` 屬性包裝器實現類似(挺隨意的)對“小”的定義。
```swift
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` 設置為 0。把數字 10 存進 `rectangle.height` 中的操作能成功,是因為數字 10 很小。嘗試存儲 24 的操作實際上存儲的值為 12,這是因為對于這個屬性的 setter 的規則來說,24 太大了。
當你把一個包裝器應用到一個屬性上時,編譯器將合成提供包裝器存儲空間和通過包裝器訪問屬性的代碼。(屬性包裝器只負責存儲被包裝值,所以沒有合成這些代碼。)不利用這個特性語法的情況下,你可以寫出使用屬性包裝器行為的代碼。舉例來說,這是先前代碼清單中的 `SmallRectangle` 的另一個版本。這個版本將其屬性明確地包裝在 `TwelveOrLess` 結構體中,而不是把 `@TwelveOrLess` 作為特性寫下來:
```swift
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` 屬性存著這個屬性包裝器的一個實例,即 `TwelveOrLess`。`height` 和 `width` 的 getter 和 setter 把對 `wrappedValue` 屬性的訪問包裝起來。
### 設置被包裝屬性的初始值 {#setting-initial-values-for-wrapped-properties}
上面例子中的代碼通過在 `TwelveOrLess` 的定義中賦予 `number` 一個初始值來設置被包裝屬性的初始值。使用這個屬性包裝器的代碼沒法為被 `TwelveOrLess` 包裝的屬性指定其他初始值。舉例來說,`SmallRectangle` 的定義沒法給 `height` 或者 `width` 一個初始值。為了支持設定一個初始值或者其他自定義操作,屬性包裝器需要添加一個構造器。這是 `TwelveOrLess` 的擴展版本,稱為 `SmallNumber`。`SmallNumber` 定義了能設置被包裝值和最大值的構造器:
```swift
@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` 的定義包括三個構造器——`init()`、`init(wrappedValue:)` 和 `init(wrappedValue:maximum:)`——下面的示例使用這三個構造器來設置被包裝值和最大值。有關構造過程和構造器語法的更多信息,請參考 [構造過程](./14_Initialization.md)。
當你把包裝器應用于屬性且沒有設定初始值時,Swift 使用 `init()` 構造器來設置包裝器。舉個例子:
```swift
struct ZeroRectangle {
@SmallNumber var height: Int
@SmallNumber var width: Int
}
var zeroRectangle = ZeroRectangle()
print(zeroRectangle.height, zeroRectangle.width)
// 打印 "0 0"
```
調用 `SmallNumber()` 來創建包裝 `height` 和 `width` 的 `SmallNumber` 的實例。構造器內部的代碼使用默認值 0 和 12 設置初始的被包裝值和初始的最大值。像之前使用在 `SmallRectangle` 中使用 `TwelveOrLess` 的例子,這個屬性包裝器仍然提供所有的初始值。與這個例子不同的是,`SmallNumber` 也支持把編寫這些初始值作為聲明屬性的一部分。
當你為屬性指定初始值時,Swift 使用 `init(wrappedValue:)` 構造器來設置包裝器。舉個例子:
```swift
struct UnitRectangle {
@SmallNumber var height: Int = 1
@SmallNumber var width: Int = 1
}
var unitRectangle = UnitRectangle()
print(unitRectangle.height, unitRectangle.width)
// 打印 "1 1"
```
當你對一個被包裝的屬性寫下 `= 1` 時,這被轉換為調用 `init(wrappedValue:)` 構造器。調用 `SmallNumber(wrappedValue: 1)`來創建包裝 `height` 和 `width` 的 `SmallNumber` 的實例。構造器使用此處指定的被包裝值,且使用的默認最大值為 12。
當你在自定義特性后面把實參寫在括號里時,Swift 使用接受這些實參的構造器來設置包裝器。舉例來說,如果你提供初始值和最大值,Swift 使用 `init(wrappedValue:maximum:)` 構造器:
```swift
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"
```
調用 `SmallNumber(wrappedValue: 2, maximum: 5)` 來創建包裝 `height` 的 `SmallNumber` 的一個實例。調用 `SmallNumber(wrappedValue: 3, maximum: 4)` 來創建包裝 `width` 的 `SmallNumber` 的一個實例。
通過將實參包含到屬性包裝器中,你可以設置包裝器的初始狀態,或者在創建包裝器時傳遞其他的選項。這種語法是使用屬性包裝器最通用的方法。你可以為這個屬性提供任何所需的實參,且它們將被傳遞給構造器。
當包含屬性包裝器實參時,你也可以使用賦值來指定初始值。Swift 將賦值視為 `wrappedValue` 參數,且使用接受被包含的實參的構造器。舉個例子:
```swift
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"
```
調用 `SmallNumber(wrappedValue: 1)` 來創建包裝 `height` 的 `SmallNumber` 的一個實例,這個實例使用默認最大值 12。調用 `SmallNumber(wrappedValue: 2, maximum: 9)` 來創建包裝 `width` 的 `SmallNumber` 的一個實例。
### 從屬性包裝器中呈現一個值 {#projecting-a-value-from-a-property-wrapper}
除了被包裝值,屬性包裝器可以通過定義被呈現值暴露出其他功能。舉個例子,管理對數據庫的訪問的屬性包裝器可以在它的被呈現值上暴露出 `flushDatabaseConnection()` 方法。除了以貨幣符號(\$)開頭,被呈現值的名稱和被包裝值是一樣的。因為你的代碼不能夠定義以 $ 開頭的屬性,所以被呈現值永遠不會與你定義的屬性有沖突。
在之前 `SmallNumber` 的例子中,如果你嘗試把這個屬性設置為一個很大的數值,屬性包裝器會在存儲這個數值之前調整這個數值。以下的代碼把被呈現值添加到 `SmallNumber` 結構體中來追蹤在存儲新值之前屬性包裝器是否為這個屬性調整了新值。
```swift
@propertyWrapper
struct SmallNumber {
private var number: Int
var projectedValue: Bool
init() {
self.number = 0
self.projectedValue = 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"
```
寫下 `someStructure.$someNumber` 即可訪問包裝器的被呈現值。在存儲一個比較小的數值時,如 4 ,`someStructure.$someNumber` 的值為 `false`。但是,在嘗試存儲一個較大的數值時,如 55 ,被呈現值變為 `true`。
屬性包裝器可以返回任何類型的值作為它的被呈現值。在這個例子里,屬性包裝器要暴露的信息是:那個數值是否被調整過,所以它暴露出布爾型值來作為它的被呈現值。需要暴露出更多信息的包裝器可以返回其他數據類型的實例,或者可以返回自身來暴露出包裝器的實例,并把其作為它的被呈現值。
當從類型的一部分代碼中訪問被呈現值,例如屬性 getter 或實例方法,你可以在屬性名稱之前省略 `self.`,就像訪問其他屬性一樣。以下示例中的代碼用 `$height` 和 `$width` 引用包裝器 `height` 和 `width` 的被呈現值:
```swift
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
}
}
```
因為屬性包裝器語法只是具有 getter 和 setter 的屬性的語法糖,所以訪問 `height` 和 `width` 的行為與訪問任何其他屬性的行為相同。舉個例子,`resize(to:)` 中的代碼使用它們的屬性包裝器來訪問 `height` 和 `width`。如果調用 `resize(to: .large)`,`.large` 的 switch case 分支語句把矩形的高度和寬度設置為 100。屬性包裝器防止這些屬性的值大于 12,且把被呈現值設置成為 `true` 來記下它調整過這些值的事實。在 `resize(to:)` 的最后,返回語句檢查 `$height` 和 `$width` 來確認是否屬性包裝器調整過 `height` 或 `width`。
## 全局變量和局部變量 {#global-and-local-variables}
計算屬性和觀察屬性所描述的功能也可以用于*全局變量*和*局部變量*。全局變量是在函數、方法、閉包或任何類型之外定義的變量。局部變量是在函數、方法或閉包內部定義的變量。
前面章節提到的全局或局部變量都屬于*存儲型變量*,跟存儲屬性類似,它為特定類型的值提供存儲空間,并允許讀取和寫入。
另外,在全局或局部范圍都可以定義*計算型變量*和為存儲型變量定義觀察器。計算型變量跟計算屬性一樣,返回一個計算結果而不是存儲值,聲明格式也完全一樣。
> 注意
>
> 全局的常量或變量都是延遲計算的,跟 [延時加載存儲屬性](#lazy-stored-properties) 相似,不同的地方在于,全局的常量或變量不需要標記 `lazy` 修飾符。
>
> 局部范圍的常量和變量從不延遲計算。
## 類型屬性 {#type-properties}
實例屬性屬于一個特定類型的實例,每創建一個實例,實例都擁有屬于自己的一套屬性值,實例之間的屬性相互獨立。
你也可以為類型本身定義屬性,無論創建了多少個該類型的實例,這些屬性都只有唯一一份。這種屬性就是*類型屬性*。
類型屬性用于定義某個類型所有實例共享的數據,比如*所有*實例都能用的一個常量(就像 C 語言中的靜態常量),或者所有實例都能訪問的一個變量(就像 C 語言中的靜態變量)。
存儲型類型屬性可以是變量或常量,計算型類型屬性跟實例的計算型屬性一樣只能定義成變量屬性。
> 注意
>
> 跟實例的存儲型屬性不同,必須給存儲型類型屬性指定默認值,因為類型本身沒有構造器,也就無法在初始化過程中使用構造器給類型屬性賦值。
>
> 存儲型類型屬性是延遲初始化的,它們只有在第一次被訪問的時候才會被初始化。即使它們被多個線程同時訪問,系統也保證只會對其進行一次初始化,并且不需要對其使用 `lazy` 修飾符。
### 類型屬性語法 {#type-property-syntax}
在 C 或 Objective-C 中,與某個類型關聯的靜態常量和靜態變量,是作為 *global*(全局)靜態變量定義的。但是在 Swift 中,類型屬性是作為類型定義的一部分寫在類型最外層的花括號內,因此它的作用范圍也就在類型支持的范圍內。
使用關鍵字 `static` 來定義類型屬性。在為類定義計算型類型屬性時,可以改用關鍵字 `class` 來支持子類對父類的實現進行重寫。下面的例子演示了存儲型和計算型類型屬性的語法:
```swift
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}
跟實例屬性一樣,類型屬性也是通過點運算符來訪問。但是,類型屬性是通過*類型*本身來訪問,而不是通過實例。比如:
```swift
print(SomeStructure.storedTypeProperty)
// 打印“Some value.”
SomeStructure.storedTypeProperty = "Another value."
print(SomeStructure.storedTypeProperty)
// 打印“Another value.”
print(SomeEnumeration.computedTypeProperty)
// 打印“6”
print(SomeClass.computedTypeProperty)
// 打印“27”
```
下面的例子定義了一個結構體,使用兩個存儲型類型屬性來表示兩個聲道的音量,每個聲道具有 `0` 到 `10` 之間的整數音量。
下圖展示了如何把兩個聲道結合來模擬立體聲的音量。當聲道的音量是 `0`,沒有一個燈會亮;當聲道的音量是 `10`,所有燈點亮。本圖中,左聲道的音量是 `9`,右聲道的音量是 `7`:
<img src="https://docs.swift.org/swift-book/_images/staticPropertiesVUMeter_2x.png" alt="Static Properties VUMeter" width="243" height="357" />
上面所描述的聲道模型使用 `AudioChannel` 結構體的實例來表示:
```swift
struct AudioChannel {
static let thresholdLevel = 10
static var maxInputLevelForAllChannels = 0
var currentLevel: Int = 0 {
didSet {
if currentLevel > AudioChannel.thresholdLevel {
// 將當前音量限制在閾值之內
currentLevel = AudioChannel.thresholdLevel
}
if currentLevel > AudioChannel.maxInputLevelForAllChannels {
// 存儲當前音量作為新的最大輸入音量
AudioChannel.maxInputLevelForAllChannels = currentLevel
}
}
}
}
```
`AudioChannel` 結構定義了 2 個存儲型類型屬性來實現上述功能。第一個是 `thresholdLevel`,表示音量的最大上限閾值,它是一個值為 `10` 的常量,對所有實例都可見,如果音量高于 `10`,則取最大上限值 `10`(見后面描述)。
第二個類型屬性是變量存儲型屬性 `maxInputLevelForAllChannels`,它用來表示所有 `AudioChannel` 實例的最大輸入音量,初始值是 `0`。
`AudioChannel` 也定義了一個名為 `currentLevel` 的存儲型實例屬性,表示當前聲道現在的音量,取值為 `0` 到 `10`。
屬性 `currentLevel` 包含 `didSet` 屬性觀察器來檢查每次設置后的屬性值,它做如下兩個檢查:
- 如果 `currentLevel` 的新值大于允許的閾值 `thresholdLevel`,屬性觀察器將 `currentLevel` 的值限定為閾值 `thresholdLevel`。
- 如果修正后的 `currentLevel` 值大于靜態類型屬性 `maxInputLevelForAllChannels` 的值,屬性觀察器就將新值保存在 `maxInputLevelForAllChannels` 中。
> 注意
>
> 在第一個檢查過程中,`didSet` 屬性觀察器將 `currentLevel` 設置成了不同的值,但這不會造成屬性觀察器被再次調用。
可以使用結構體 `AudioChannel` 創建兩個聲道 `leftChannel` 和 `rightChannel`,用以表示立體聲系統的音量:
```swift
var leftChannel = AudioChannel()
var rightChannel = AudioChannel()
```
如果將左聲道的 `currentLevel` 設置成 `7`,類型屬性 `maxInputLevelForAllChannels` 也會更新成 `7`:
```swift
leftChannel.currentLevel = 7
print(leftChannel.currentLevel)
// 輸出“7”
print(AudioChannel.maxInputLevelForAllChannels)
// 輸出“7”
```
如果試圖將右聲道的 `currentLevel` 設置成 `11`,它會被修正到最大值 `10`,同時 `maxInputLevelForAllChannels` 的值也會更新到 `10`:
```swift
rightChannel.currentLevel = 11
print(rightChannel.currentLevel)
// 輸出“10”
print(AudioChannel.maxInputLevelForAllChannels)
// 輸出“10”
```
- 1.關于 Swift
- 2.Swift 初見
- 2-1基礎部分
- 2-2基本運算符
- 2-3字符串和字符
- 2-4集合類型
- 2-5控制流
- 2-6函數
- 2-7閉包
- 2-8枚舉
- 2-9類和結構體
- 2-10屬性
- 2-11方法
- 2-12下標
- 2-13繼承
- 2-14構造過程
- 2-15析構過程
- 2-16可選鏈
- 2-17錯誤處理
- 2-18類型轉換
- 2-19嵌套類型
- 2-20擴展
- 2-21協議
- 2-22泛型
- 2-23不透明類型
- 2-24自動引用計數
- 2-25內存安全
- 2-26訪問控制
- 2-27高級運算符
- 3-1關于語言參考
- 3-2詞法結構
- 3-3類型
- 3-4表達式
- 3-5語句
- 3-6聲明
- 3-7特性
- 3-8模式
- 3-9泛型參數
- 4語法總結