這里的教程為Swift官方教程中文版。

屬性

屬性將值與特定的類、結(jié)構(gòu)體或枚舉關(guān)聯(lián)。存儲屬性會將常量和變量存儲為實(shí)例的一部分,而計算屬性則是直接計算(而不是存儲)值。計算屬性可以用于類、結(jié)構(gòu)體和枚舉,而存儲屬性只能用于類和結(jié)構(gòu)體。

存儲屬性和計算屬性通常與特定類型的實(shí)例關(guān)聯(lián)。但是,屬性也可以直接與類型本身關(guān)聯(lián),這種屬性稱為類型屬性。

另外,還可以定義屬性觀察器來監(jiān)控屬性值的變化,以此來觸發(fā)自定義的操作。屬性觀察器可以添加到類本身定義的存儲屬性上,也可以添加到從父類繼承的屬性上。

你也可以利用屬性包裝器來復(fù)用多個屬性的 getter 和 setter 中的代碼。

存儲屬性

簡單來說,一個存儲屬性就是存儲在特定類或結(jié)構(gòu)體實(shí)例里的一個常量或變量。存儲屬性可以是變量存儲屬性(用關(guān)鍵字 var 定義),也可以是常量存儲屬性(用關(guān)鍵字 let 定義)。

可以在定義存儲屬性的時候指定默認(rèn)值,請參考 默認(rèn)構(gòu)造器 一節(jié)。也可以在構(gòu)造過程中設(shè)置或修改存儲屬性的值,甚至修改常量存儲屬性的值,請參考 構(gòu)造過程中常量屬性的修改 一節(jié)。

下面的例子定義了一個名為 FixedLengthRange 的結(jié)構(gòu)體,該結(jié)構(gòu)體用于描述整數(shù)的區(qū)間,且這個范圍值在被創(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í)例包含一個名為 firstValue 的變量存儲屬性和一個名為 length 的常量存儲屬性。在上面的例子中,length 在創(chuàng)建實(shí)例的時候被初始化,且之后無法修改它的值,因?yàn)樗且粋€常量存儲屬性。

常量結(jié)構(gòu)體實(shí)例的存儲屬性

如果創(chuàng)建了一個結(jié)構(gòu)體實(shí)例并將其賦值給一個常量,則無法修改該實(shí)例的任何屬性,即使被聲明為可變屬性也不行:

let rangeOfFourItems = FixedLengthRange(firstValue: 0, length: 4)
// 該區(qū)間表示整數(shù) 0,1,2,3
rangeOfFourItems.firstValue = 6
// 盡管 firstValue 是個可變屬性,但這里還是會報錯

因?yàn)?rangeOfFourItems 被聲明成了常量(用 let 關(guān)鍵字),所以即使 firstValue 是一個可變屬性,也無法再修改它了。

這種行為是由于結(jié)構(gòu)體屬于值類型。當(dāng)值類型的實(shí)例被聲明為常量的時候,它的所有屬性也就成了常量。

屬于引用類型的類則不一樣。把一個引用類型的實(shí)例賦給一個常量后,依然可以修改該實(shí)例的可變屬性。

延時加載存儲屬性

延時加載存儲屬性是指當(dāng)?shù)谝淮伪徽{(diào)用的時候才會計算其初始值的屬性。在屬性聲明前使用 lazy 來標(biāo)示一個延時加載存儲屬性。

注意

必須將延時加載屬性聲明成變量(使用 var 關(guān)鍵字),因?yàn)閷傩缘某跏贾悼赡茉趯?shí)例構(gòu)造完成之后才會得到。而常量屬性在構(gòu)造過程完成之前必須要有初始值,因此無法聲明成延時加載。

當(dāng)屬性的值依賴于一些外部因素且這些外部因素只有在構(gòu)造過程結(jié)束之后才會知道的時候,延時加載屬性就會很有用。或者當(dāng)獲得屬性的值因?yàn)樾枰獜?fù)雜或者大量的計算,而需要采用需要的時候再計算的方式,延時加載屬性也會很有用。

下面的例子使用了延時加載存儲屬性來避免復(fù)雜類中不必要的初始化工作。例子中定義了 DataImporterDataManager 兩個類,下面是部分代碼:

class DataImporter {
    /*
    DataImporter 是一個負(fù)責(zé)將外部文件中的數(shù)據(jù)導(dǎo)入的類。
    這個類的初始化會消耗不少時間。
    */
    var fileName = "data.txt"
    // 這里會提供數(shù)據(jù)導(dǎo)入功能
}

class DataManager {
    lazy var importer = DataImporter()
    var data = [String]()
    // 這里會提供數(shù)據(jù)管理功能
}

let manager = DataManager()
manager.data.append("Some data")
manager.data.append("Some more data")
// DataImporter 實(shí)例的 importer 屬性還沒有被創(chuàng)建

DataManager 類包含一個名為 data 的存儲屬性,初始值是一個空的字符串?dāng)?shù)組。這里沒有給出全部代碼,只需知道 DataManager 類的目的是管理和提供對這個字符串?dāng)?shù)組的訪問即可。

DataManager 的一個功能是從文件中導(dǎo)入數(shù)據(jù)。這個功能由 DataImporter 類提供,DataImporter 完成初始化需要消耗不少時間:因?yàn)樗膶?shí)例在初始化時可能需要打開文件并讀取文件中的內(nèi)容到內(nèi)存中。

DataManager 管理數(shù)據(jù)時也可能不從文件中導(dǎo)入數(shù)據(jù)。所以當(dāng) DataManager 的實(shí)例被創(chuàng)建時,沒必要創(chuàng)建一個 DataImporter 的實(shí)例,更明智的做法是第一次用到 DataImporter 的時候才去創(chuàng)建它。

由于使用了 lazyDataImporter 的實(shí)例 importer 屬性只有在第一次被訪問的時候才被創(chuàng)建。比如訪問它的屬性 fileName 時:

print(manager.importer.fileName)
// DataImporter 實(shí)例的 importer 屬性現(xiàn)在被創(chuàng)建了
// 輸出“data.txt”

注意

如果一個被標(biāo)記為 lazy 的屬性在沒有初始化時就同時被多個線程訪問,則無法保證該屬性只會被初始化一次。

存儲屬性和實(shí)例變量

如果您有過 Objective-C 經(jīng)驗(yàn),應(yīng)該知道 Objective-C 為類實(shí)例存儲值和引用提供兩種方法。除了屬性之外,還可以使用實(shí)例變量作為一個備份存儲將變量值賦值給屬性。

Swift 編程語言中把這些理論統(tǒng)一用屬性來實(shí)現(xiàn)。Swift 中的屬性沒有對應(yīng)的實(shí)例變量,屬性的備份存儲也無法直接訪問。這就避免了不同場景下訪問方式的困擾,同時也將屬性的定義簡化成一個語句。屬性的全部信息——包括命名、類型和內(nèi)存管理特征——作為類型定義的一部分,都定義在一個地方。

計算屬性

除存儲屬性外,類、結(jié)構(gòu)體和枚舉可以定義計算屬性。計算屬性不直接存儲值,而是提供一個 getter 和一個可選的 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)”

這個例子定義了 3 個結(jié)構(gòu)體來描述幾何形狀:

  • Point 封裝了一個 (x, y) 的坐標(biāo)
  • Size 封裝了一個 width 和一個 height
  • Rect 表示一個有原點(diǎn)和尺寸的矩形

Rect 也提供了一個名為 center 的計算屬性。一個 Rect 的中心點(diǎn)可以從 origin(原點(diǎn))和 size(大?。┧愠觯圆恍枰獙⒅行狞c(diǎn)以 Point 類型的值來保存。Rect 的計算屬性 center 提供了自定義的 getter 和 setter 來獲取和設(shè)置矩形的中心點(diǎn),就像它有一個存儲屬性一樣。

上述例子中創(chuàng)建了一個名為 squareRect 實(shí)例,初始值原點(diǎn)是 (0, 0),寬度高度都是 10。如下圖中藍(lán)色正方形所示。

squarecenter 屬性可以通過點(diǎn)運(yùn)算符(square.center)來訪問,這會調(diào)用該屬性的 getter 來獲取它的值。跟直接返回已經(jīng)存在的值不同,getter 實(shí)際上通過計算然后返回一個新的 Point 來表示 square 的中心點(diǎn)。如代碼所示,它正確返回了中心點(diǎn) (5, 5)。

center 屬性之后被設(shè)置了一個新的值 (15, 15),表示向右上方移動正方形到如下圖橙色正方形所示的位置。設(shè)置屬性 center 的值會調(diào)用它的 setter 來修改屬性 originxy 的值,從而實(shí)現(xiàn)移動正方形到新的位置。

Computed Properties sample

簡化 Setter 聲明

如果計算屬性的 setter 沒有定義表示新值的參數(shù)名,則可以使用默認(rèn)名稱 newValue。下面是使用了簡化 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)
        }
    }
}

簡化 Getter 聲明

如果整個 getter 是單一表達(dá)式,getter 會隱式地返回這個表達(dá)式結(jié)果。下面是另一個版本的 Rect 結(jié)構(gòu)體,用到了簡化的 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ī)則相同,請參考 隱式返回的函數(shù)。

只讀計算屬性

只有 getter 沒有 setter 的計算屬性叫只讀計算屬性。只讀計算屬性總是返回一個值,可以通過點(diǎn)運(yùn)算符訪問,但不能設(shè)置新的值。

注意

必須使用 var 關(guān)鍵字定義計算屬性,包括只讀計算屬性,因?yàn)樗鼈兊闹挡皇枪潭ǖ摹?code>let 關(guān)鍵字只用來聲明常量屬性,表示初始化后再也無法修改的值。

只讀計算屬性的聲明可以去掉 get 關(guān)鍵字和花括號:

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 的結(jié)構(gòu)體,表示三維空間的立方體,包含 widthheightdepth 屬性。結(jié)構(gòu)體還有一個名為 volume 的只讀計算屬性用來返回立方體的體積。為 volume 提供 setter 毫無意義,因?yàn)闊o法確定如何修改 width、heightdepth 三者的值來匹配新的 volume。然而,Cuboid 提供一個只讀計算屬性來讓外部用戶直接獲取體積是很有用的。

屬性觀察器

屬性觀察器監(jiān)控和響應(yīng)屬性值的變化,每次屬性被設(shè)置值的時候都會調(diào)用屬性觀察器,即使新值和當(dāng)前值相同的時候也不例外。

你可以為除了延時加載存儲屬性之外的其他存儲屬性添加屬性觀察器,你也可以在子類中通過重寫屬性的方式為繼承的屬性(包括存儲屬性和計算屬性)添加屬性觀察器。你不必為非重寫的計算屬性添加屬性觀察器,因?yàn)槟憧梢灾苯油ㄟ^它的 setter 監(jiān)控和響應(yīng)值的變化。屬性重寫請參考 重寫。

可以為屬性添加其中一個或兩個觀察器:

  • willSet 在新的值被設(shè)置之前調(diào)用
  • didSet 在新的值被設(shè)置之后調(diào)用

willSet 觀察器會將新的屬性值作為常量參數(shù)傳入,在 willSet 的實(shí)現(xiàn)代碼中可以為這個參數(shù)指定一個名稱,如果不指定則參數(shù)仍然可用,這時使用默認(rèn)名稱 newValue 表示。

同樣,didSet 觀察器會將舊的屬性值作為參數(shù)傳入,可以為該參數(shù)指定一個名稱或者使用默認(rèn)參數(shù)名 oldValue。如果在 didSet 方法中再次對該屬性賦值,那么新值會覆蓋舊的值。

注意

在父類初始化方法調(diào)用之后,在子類構(gòu)造器中給父類的屬性賦值時,會調(diào)用父類屬性的 willSetdidSet 觀察器。而在父類初始化方法調(diào)用之前,給子類的屬性賦值時不會調(diào)用子類屬性的觀察器。

有關(guān)構(gòu)造器代理的更多信息,請參考 值類型的構(gòu)造器代理類的構(gòu)造器代理

下面是一個 willSetdidSet 實(shí)際運(yùn)用的例子,其中定義了一個名為 StepCounter 的類,用來統(tǒng)計一個人步行時的總步數(shù)。這個類可以跟計步器或其他日常鍛煉的統(tǒng)計裝置的輸入數(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 類定義了一個叫 totalStepsInt 類型的屬性。它是一個存儲屬性,包含 willSetdidSet 觀察器。

當(dāng) totalSteps 被設(shè)置新值的時候,它的 willSetdidSet 觀察器都會被調(diào)用,即使新值和當(dāng)前值完全相同時也會被調(diào)用。

例子中的 willSet 觀察器將表示新值的參數(shù)自定義為 newTotalSteps,這個觀察器只是簡單的將新的值輸出。

didSet 觀察器在 totalSteps 的值改變后被調(diào)用,它把新值和舊值進(jìn)行對比,如果總步數(shù)增加了,就輸出一個消息表示增加了多少步。didSet 沒有為舊值提供自定義名稱,所以默認(rèn)值 oldValue 表示舊值的參數(shù)名。

注意

如果將帶有觀察器的屬性通過 in-out 方式傳入函數(shù),willSetdidSet 也會調(diào)用。這是因?yàn)?in-out 參數(shù)采用了拷入拷出內(nèi)存模式:即在函數(shù)內(nèi)部使用的是參數(shù)的 copy,函數(shù)結(jié)束后,又對參數(shù)重新賦值。關(guān)于 in-out 參數(shù)詳細(xì)的介紹,請參考 輸入輸出參數(shù)

屬性包裝器

屬性包裝器在管理屬性如何存儲和定義屬性的代碼之間添加了一個分隔層。舉例來說,如果你的屬性需要線程安全性檢查或者需要在數(shù)據(jù)庫中存儲它們的基本數(shù)據(jù),那么必須給每個屬性添加同樣的邏輯代碼。當(dāng)使用屬性包裝器時,你只需在定義屬性包裝器時編寫一次管理代碼,然后應(yīng)用到多個屬性上來進(jìn)行復(fù)用。

定義一個屬性包裝器,你需要創(chuàng)建一個定義 wrappedValue 屬性的結(jié)構(gòu)體、枚舉或者類。在下面的代碼中,TwelveOrLess 結(jié)構(gòu)體確保它包裝的值始終是小于等于 12 的數(shù)字。如果要求它存儲一個更大的數(shù)字,它則會存儲 12 這個數(shù)字。

@propertyWrapper
struct TwelveOrLess {
    private var number = 0
    var wrappedValue: Int {
        get { return number }
        set { number = min(newValue, 12) }
    }
}

這個 setter 確保新值小于 12,而且返回被存儲的值。

注意

上面例子以 private 的方式聲明 number 變量,這使得 number 僅在 TwelveOrLess 的實(shí)現(xiàn)中使用。寫在其他地方的代碼通過使用 wrappedValue 的 getter 和 setter 來獲取這個值,但不能直接使用 number。有關(guān) private 的更多信息,請參考 訪問控制

通過在屬性之前寫上包裝器名稱作為特性的方式,你可以把一個包裝器應(yīng)用到一個屬性上去。這里有個存儲小矩形的結(jié)構(gòu)體。通過 TwelveOrLess 屬性包裝器實(shí)現(xiàn)類似(挺隨意的)對“小”的定義。

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"

heightwidth 屬性從 TwelveOrLess 的定義中獲取它們的初始值。該定義把 TwelveOrLess.number 設(shè)置為 0。把數(shù)字 10 存進(jìn) rectangle.height 中的操作能成功,是因?yàn)閿?shù)字 10 很小。嘗試存儲 24 的操作實(shí)際上存儲的值為 12,這是因?yàn)閷τ谶@個屬性的 setter 的規(guī)則來說,24 太大了。

當(dāng)你把一個包裝器應(yīng)用到一個屬性上時,編譯器將合成提供包裝器存儲空間和通過包裝器訪問屬性的代碼。(屬性包裝器只負(fù)責(zé)存儲被包裝值,所以沒有合成這些代碼。)不利用這個特性語法的情況下,你可以寫出使用屬性包裝器行為的代碼。舉例來說,這是先前代碼清單中的 SmallRectangle 的另一個版本。這個版本將其屬性明確地包裝在 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 屬性存著這個屬性包裝器的一個實(shí)例,即 TwelveOrLess。heightwidth 的 getter 和 setter 把對 wrappedValue 屬性的訪問包裝起來。

設(shè)置被包裝屬性的初始值

上面例子中的代碼通過在 TwelveOrLess 的定義中賦予 number 一個初始值來設(shè)置被包裝屬性的初始值。使用這個屬性包裝器的代碼沒法為被 TwelveOrLess 包裝的屬性指定其他初始值。舉例來說,SmallRectangle 的定義沒法給 height 或者 width 一個初始值。為了支持設(shè)定一個初始值或者其他自定義操作,屬性包裝器需要添加一個構(gòu)造器。這是 TwelveOrLess 的擴(kuò)展版本,稱為 SmallNumberSmallNumber 定義了能設(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òu)造器——init()、init(wrappedValue:)init(wrappedValue:maximum:)——下面的示例使用這三個構(gòu)造器來設(shè)置被包裝值和最大值。有關(guān)構(gòu)造過程和構(gòu)造器語法的更多信息,請參考 構(gòu)造過程。

當(dāng)你把包裝器應(yīng)用于屬性且沒有設(shè)定初始值時,Swift 使用 init() 構(gòu)造器來設(shè)置包裝器。舉個例子:

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)建包裝 heightwidthSmallNumber 的實(shí)例。構(gòu)造器內(nèi)部的代碼使用默認(rèn)值 0 和 12 設(shè)置初始的被包裝值和初始的最大值。像之前使用在 SmallRectangle 中使用 TwelveOrLess 的例子,這個屬性包裝器仍然提供所有的初始值。與這個例子不同的是,SmallNumber 也支持把編寫這些初始值作為聲明屬性的一部分。

當(dāng)你為屬性指定初始值時,Swift 使用 init(wrappedValue:) 構(gòu)造器來設(shè)置包裝器。舉個例子:

struct UnitRectangle {
    @SmallNumber var height: Int = 1
    @SmallNumber var width: Int = 1
}

var unitRectangle = UnitRectangle()
print(unitRectangle.height, unitRectangle.width)
// 打印 "1 1"

當(dāng)你對一個被包裝的屬性寫下 = 1 時,這被轉(zhuǎn)換為調(diào)用 init(wrappedValue:) 構(gòu)造器。調(diào)用 SmallNumber(wrappedValue: 1)來創(chuàng)建包裝 heightwidthSmallNumber 的實(shí)例。構(gòu)造器使用此處指定的被包裝值,且使用的默認(rèn)最大值為 12。

當(dāng)你在自定義特性后面把實(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)建包裝 heightSmallNumber 的一個實(shí)例。調(diào)用 SmallNumber(wrappedValue: 3, maximum: 4) 來創(chuàng)建包裝 widthSmallNumber 的一個實(shí)例。

通過將實(shí)參包含到屬性包裝器中,你可以設(shè)置包裝器的初始狀態(tài),或者在創(chuàng)建包裝器時傳遞其他的選項。這種語法是使用屬性包裝器最通用的方法。你可以為這個屬性提供任何所需的實(shí)參,且它們將被傳遞給構(gòu)造器。

當(dāng)包含屬性包裝器實(shí)參時,你也可以使用賦值來指定初始值。Swift 將賦值視為 wrappedValue 參數(shù),且使用接受被包含的實(shí)參的構(gòu)造器。舉個例子:

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)建包裝 heightSmallNumber 的一個實(shí)例,這個實(shí)例使用默認(rèn)最大值 12。調(diào)用 SmallNumber(wrappedValue: 2, maximum: 9) 來創(chuàng)建包裝 widthSmallNumber 的一個實(shí)例。

從屬性包裝器中呈現(xiàn)一個值

除了被包裝值,屬性包裝器可以通過定義被呈現(xiàn)值暴露出其他功能。舉個例子,管理對數(shù)據(jù)庫的訪問的屬性包裝器可以在它的被呈現(xiàn)值上暴露出 flushDatabaseConnection() 方法。除了以貨幣符號($)開頭,被呈現(xiàn)值的名稱和被包裝值是一樣的。因?yàn)槟愕拇a不能夠定義以 $ 開頭的屬性,所以被呈現(xiàn)值永遠(yuǎn)不會與你定義的屬性有沖突。

在之前 SmallNumber 的例子中,如果你嘗試把這個屬性設(shè)置為一個很大的數(shù)值,屬性包裝器會在存儲這個數(shù)值之前調(diào)整這個數(shù)值。以下的代碼把被呈現(xiàn)值添加到 SmallNumber 結(jié)構(gòu)體中來追蹤在存儲新值之前屬性包裝器是否為這個屬性調(diào)整了新值。

@propertyWrapper
struct SmallNumber {
    private var number = 0
    var 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"

寫下 s.$someNumber 即可訪問包裝器的被呈現(xiàn)值。在存儲一個比較小的數(shù)值時,如 4 ,s.$someNumber 的值為 false。但是,在嘗試存儲一個較大的數(shù)值時,如 55 ,被呈現(xiàn)值變?yōu)?true。

屬性包裝器可以返回任何類型的值作為它的被呈現(xiàn)值。在這個例子里,屬性包裝器要暴露的信息是:那個數(shù)值是否被調(diào)整過,所以它暴露出布爾型值來作為它的被呈現(xiàn)值。需要暴露出更多信息的包裝器可以返回其他數(shù)據(jù)類型的實(shí)例,或者可以返回自身來暴露出包裝器的實(shí)例,并把其作為它的被呈現(xiàn)值。

當(dāng)從類型的一部分代碼中訪問被呈現(xiàn)值,例如屬性 getter 或?qū)嵗椒ǎ憧梢栽趯傩悦Q之前省略 self.,就像訪問其他屬性一樣。以下示例中的代碼用 $height$width 引用包裝器 heightwidth 的被呈現(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 的屬性的語法糖,所以訪問 heightwidth 的行為與訪問任何其他屬性的行為相同。舉個例子,resize(to:) 中的代碼使用它們的屬性包裝器來訪問 heightwidth。如果調(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)整過 heightwidth。

全局變量和局部變量

計算屬性和觀察屬性所描述的功能也可以用于全局變量局部變量。全局變量是在函數(shù)、方法、閉包或任何類型之外定義的變量。局部變量是在函數(shù)、方法或閉包內(nèi)部定義的變量。

前面章節(jié)提到的全局或局部變量都屬于存儲型變量,跟存儲屬性類似,它為特定類型的值提供存儲空間,并允許讀取和寫入。

另外,在全局或局部范圍都可以定義計算型變量和為存儲型變量定義觀察器。計算型變量跟計算屬性一樣,返回一個計算結(jié)果而不是存儲值,聲明格式也完全一樣。

注意

全局的常量或變量都是延遲計算的,跟 延時加載存儲屬性 相似,不同的地方在于,全局的常量或變量不需要標(biāo)記 lazy 修飾符。

局部范圍的常量和變量從不延遲計算。

類型屬性

實(shí)例屬性屬于一個特定類型的實(shí)例,每創(chuàng)建一個實(shí)例,實(shí)例都擁有屬于自己的一套屬性值,實(shí)例之間的屬性相互獨(dú)立。

你也可以為類型本身定義屬性,無論創(chuàng)建了多少個該類型的實(shí)例,這些屬性都只有唯一一份。這種屬性就是類型屬性。

類型屬性用于定義某個類型所有實(shí)例共享的數(shù)據(jù),比如所有實(shí)例都能用的一個常量(就像 C 語言中的靜態(tài)常量),或者所有實(shí)例都能訪問的一個變量(就像 C 語言中的靜態(tài)變量)。

存儲型類型屬性可以是變量或常量,計算型類型屬性跟實(shí)例的計算型屬性一樣只能定義成變量屬性。

注意

跟實(shí)例的存儲型屬性不同,必須給存儲型類型屬性指定默認(rèn)值,因?yàn)轭愋捅旧頉]有構(gòu)造器,也就無法在初始化過程中使用構(gòu)造器給類型屬性賦值。

存儲型類型屬性是延遲初始化的,它們只有在第一次被訪問的時候才會被初始化。即使它們被多個線程同時訪問,系統(tǒng)也保證只會對其進(jìn)行一次初始化,并且不需要對其使用 lazy 修飾符。

類型屬性語法

在 C 或 Objective-C 中,與某個類型關(guān)聯(lián)的靜態(tài)常量和靜態(tài)變量,是作為 global(全局)靜態(tài)變量定義的。但是在 Swift 中,類型屬性是作為類型定義的一部分寫在類型最外層的花括號內(nèi),因此它的作用范圍也就在類型支持的范圍內(nèi)。

使用關(guān)鍵字 static 來定義類型屬性。在為類定義計算型類型屬性時,可以改用關(guān)鍵字 class 來支持子類對父類的實(shí)現(xiàn)進(jìn)行重寫。下面的例子演示了存儲型和計算型類型屬性的語法:

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
    }
}

注意

例子中的計算型類型屬性是只讀的,但也可以定義可讀可寫的計算型類型屬性,跟計算型實(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”

下面的例子定義了一個結(jié)構(gòu)體,使用兩個存儲型類型屬性來表示兩個聲道的音量,每個聲道具有 010 之間的整數(shù)音量。

下圖展示了如何把兩個聲道結(jié)合來模擬立體聲的音量。當(dāng)聲道的音量是 0,沒有一個燈會亮;當(dāng)聲道的音量是 10,所有燈點(diǎn)亮。本圖中,左聲道的音量是 9,右聲道的音量是 7

Static Properties VUMeter

上面所描述的聲道模型使用 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 {
                // 存儲當(dāng)前音量作為新的最大輸入音量
                AudioChannel.maxInputLevelForAllChannels = currentLevel
            }
        }
    }
}

AudioChannel 結(jié)構(gòu)定義了 2 個存儲型類型屬性來實(shí)現(xiàn)上述功能。第一個是 thresholdLevel,表示音量的最大上限閾值,它是一個值為 10 的常量,對所有實(shí)例都可見,如果音量高于 10,則取最大上限值 10(見后面描述)。

第二個類型屬性是變量存儲型屬性 maxInputLevelForAllChannels,它用來表示所有 AudioChannel 實(shí)例的最大輸入音量,初始值是 0

AudioChannel 也定義了一個名為 currentLevel 的存儲型實(shí)例屬性,表示當(dāng)前聲道現(xiàn)在的音量,取值為 010。

屬性 currentLevel 包含 didSet 屬性觀察器來檢查每次設(shè)置后的屬性值,它做如下兩個檢查:

  • 如果 currentLevel 的新值大于允許的閾值 thresholdLevel,屬性觀察器將 currentLevel 的值限定為閾值 thresholdLevel。
  • 如果修正后的 currentLevel 值大于靜態(tài)類型屬性 maxInputLevelForAllChannels 的值,屬性觀察器就將新值保存在 maxInputLevelForAllChannels 中。

注意

在第一個檢查過程中,didSet 屬性觀察器將 currentLevel 設(shè)置成了不同的值,但這不會造成屬性觀察器被再次調(diào)用。

可以使用結(jié)構(gòu)體 AudioChannel 創(chuàng)建兩個聲道 leftChannelrightChannel,用以表示立體聲系統(tǒng)的音量:

var leftChannel = AudioChannel()
var rightChannel = AudioChannel()

如果將左聲道的 currentLevel 設(shè)置成 7,類型屬性 maxInputLevelForAllChannels 也會更新成 7

leftChannel.currentLevel = 7
print(leftChannel.currentLevel)
// 輸出“7”
print(AudioChannel.maxInputLevelForAllChannels)
// 輸出“7”

如果試圖將右聲道的 currentLevel 設(shè)置成 11,它會被修正到最大值 10,同時 maxInputLevelForAllChannels 的值也會更新到 10

rightChannel.currentLevel = 11
print(rightChannel.currentLevel)
// 輸出“10”
print(AudioChannel.maxInputLevelForAllChannels)
// 輸出“10”
? 類和結(jié)構(gòu)體 方法 ?
?