Swift - 屬性

屬性


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

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

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

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

存儲屬性

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

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

下面的例子定義了一個名為 FixedLengthRange 的結構體,該結構體用于描述整數(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 的實例包含一個名為 firstValue 的變量存儲屬性和一個名為 length 的常量存儲屬性。在上面的例子中,length 在創(chuàng)建實例的時候被初始化,且之后無法修改它的值,因為它是一個常量存儲屬性。

常量結構體實例的存儲屬性

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

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

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

這種行為是由于結構體屬于值類型。當值類型的實例被聲明為常量的時候,它的所有屬性也就成了常量。

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

延時加載存儲屬性

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

注意

必須將延時加載屬性聲明成變量(使用 var 關鍵字),因為屬性的初始值可能在實例構造完成之后才會得到。而常量屬性在構造過程完成之前必須要有初始值,因此無法聲明成延時加載。

當屬性的值依賴于一些外部因素且這些外部因素只有在構造過程結束之后才會知道的時候,延時加載屬性就會很有用?;蛘弋敨@得屬性的值因為需要復雜或者大量的計算,而需要采用需要的時候再計算的方式,延時加載屬性也會很有用。

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

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

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 實例的 importer 屬性還沒有被創(chuàng)建

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

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

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

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

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

注意

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

存儲屬性和實例變量

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

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

計算屬性

除存儲屬性外,類、結構體和枚舉可以定義計算屬性。計算屬性不直接存儲值,而是提供一個 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))")
// 打印“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 來獲取和設置矩形的中心點,就像它有一個存儲屬性一樣。

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

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

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

Computed Properties sample

簡化 Setter 聲明

如果計算屬性的 setter 沒有定義表示新值的參數(shù)名,則可以使用默認名稱 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 聲明

如果整個 getter 是單一表達式,getter 會隱式地返回這個表達式結果。下面是另一個版本的 Rect 結構體,用到了簡化的 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 的計算屬性叫只讀計算屬性。只讀計算屬性總是返回一個值,可以通過點運算符訪問,但不能設置新的值。

注意

必須使用 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)")
// 打印“the volume of fourByFiveByTwo is 40.0”

這個例子定義了一個名為 Cuboid 的結構體,表示三維空間的立方體,包含 width、heightdepth 屬性。結構體還有一個名為 volume 的只讀計算屬性用來返回立方體的體積。為 volume 提供 setter 毫無意義,因為無法確定如何修改 width、heightdepth 三者的值來匹配新的 volume。然而,Cuboid 提供一個只讀計算屬性來讓外部用戶直接獲取體積是很有用的。

屬性觀察器

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

你可以在以下位置添加屬性觀察器:

  • 自定義的存儲屬性

  • 繼承的存儲屬性

  • 繼承的計算屬性

對于繼承的屬性,你可以在子類中通過重寫屬性的方式為它添加屬性觀察器。對于自定義的計算屬性來說,使用它的 setter 監(jiān)控和響應值的變化,而不是嘗試創(chuàng)建觀察器。屬性重寫請參考 重寫。

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

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

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

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

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

注意

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

有關構造器代理的更多信息,請參考 值類型的構造器代理類的構造器代理

下面是一個 willSetdidSet 實際運用的例子,其中定義了一個名為 StepCounter 的類,用來統(tǒng)計一個人步行時的總步數(shù)。這個類可以跟計步器或其他日常鍛煉的統(tǒng)計裝置的輸入數(shù)據(jù)配合使用。

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 類定義了一個叫 totalStepsInt 類型的屬性。它是一個存儲屬性,包含 willSetdidSet 觀察器。

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

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

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

注意

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

屬性包裝器

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

定義一個屬性包裝器,你需要創(chuàng)建一個定義 wrappedValue 屬性的結構體、枚舉或者類。在下面的代碼中,TwelveOrLess 結構體確保它包裝的值始終是小于等于 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 的實現(xiàn)中使用。寫在其他地方的代碼通過使用 wrappedValue 的 getter 和 setter 來獲取這個值,但不能直接使用 number。有關 private 的更多信息,請參考 訪問控制

通過在屬性之前寫上包裝器名稱作為特性的方式,你可以把一個包裝器應用到一個屬性上去。這里有個存儲小矩形的結構體,通過 TwelveOrLess 屬性包裝器來確保它的長寬均小于等于 12。

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 設置為 0。把數(shù)字 10 存進 rectangle.height 中的操作能成功,是因為數(shù)字 10 很小。嘗試存儲 24 的操作實際上存儲的值為 12,這是因為對于這個屬性的 setter 的規(guī)則來說,24 太大了。

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

設置被包裝屬性的初始值

上面例子中的代碼通過在 TwelveOrLess 的定義中賦予 number 一個初始值來設置被包裝屬性的初始值。使用這個屬性包裝器的代碼沒法為被 TwelveOrLess 包裝的屬性指定其他初始值。舉例來說,SmallRectangle 的定義沒法給 height 或者 width 一個初始值。為了支持設定一個初始值或者其他自定義操作,屬性包裝器需要添加一個構造器。這是 TwelveOrLess 的擴展版本,稱為 SmallNumberSmallNumber 定義了能設置被包裝值和最大值的構造器:

@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:)——下面的示例使用這三個構造器來設置被包裝值和最大值。有關構造過程和構造器語法的更多信息,請參考 構造過程。

當你把包裝器應用于屬性且沒有設定初始值時,Swift 使用 init() 構造器來設置包裝器。舉個例子:

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

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

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

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

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

當你在自定義特性后面把實參寫在括號里時,Swift 使用接受這些實參的構造器來設置包裝器。舉例來說,如果你提供初始值和最大值,Swift 使用 init(wrappedValue:maximum:) 構造器:

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 的一個實例。調(diào)用 SmallNumber(wrappedValue: 3, maximum: 4) 來創(chuàng)建包裝 widthSmallNumber 的一個實例。

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

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

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

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

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

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

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

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

當從類型的一部分代碼中訪問被呈現(xiàn)值,例如屬性 getter 或實例方法,你可以在屬性名稱之前省略 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
    }
}

因為屬性包裝器語法只是具有 getter 和 setter 的屬性的語法糖,所以訪問 heightwidth 的行為與訪問任何其他屬性的行為相同。舉個例子,resize(to:) 中的代碼使用它們的屬性包裝器來訪問 heightwidth。如果調(diào)用 resize(to: .large).large 的 switch case 分支語句把矩形的高度和寬度設置為 100。屬性包裝器防止這些屬性的值大于 12,且把被呈現(xiàn)值設置成為 true 來記下它調(diào)整過這些值的事實。在 resize(to:) 的最后,返回語句檢查 $height$width 來確認是否屬性包裝器調(diào)整過 heightwidth。

全局變量和局部變量

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

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

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

注意

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

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

可以在局部存儲型變量上使用屬性包裝器,但不能在全局變量或者計算型變量上使用。比如下面的代碼,myNumber 使用 SmallNumber 作為屬性包裝器。

func someFunction() {
    @SmallNumber var myNumber: Int = 0

    myNumber = 10
    // 這時 myNumber 是 10

    myNumber = 24
    // 這時 myNumber 是 12
}

就像將 SmallNumber 應用到屬性上一樣,將 myNumber 賦值為 10 是有效的。而因為這個屬性包裝器不允許值大于 12,將 myNumber 賦值為 24 時則會變成 12。

類型屬性

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

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

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

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

注意

跟實例的存儲型屬性不同,必須給存儲型類型屬性指定默認值,因為類型本身沒有構造器,也就無法在初始化過程中使用構造器給類型屬性賦值。

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

類型屬性語法

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

使用關鍵字 static 來定義類型屬性。在為類定義計算型類型屬性時,可以改用關鍵字 class 來支持子類對父類的實現(xià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
    }
}

注意

例子中的計算型類型屬性是只讀的,但也可以定義可讀可寫的計算型類型屬性,跟計算型實例屬性的語法相同。

獲取和設置類型屬性的值

跟實例屬性一樣,類型屬性也是通過點運算符來訪問。但是,類型屬性是通過類型本身來訪問,而不是通過實例。比如:

print(SomeStructure.storedTypeProperty)
// 打印“Some value.”
SomeStructure.storedTypeProperty = "Another value."
print(SomeStructure.storedTypeProperty)
// 打印“Another value.”
print(SomeEnumeration.computedTypeProperty)
// 打印“6”
print(SomeClass.computedTypeProperty)
// 打印“27”

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

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

Static Properties VUMeter

上面所描述的聲道模型使用 AudioChannel 結構體的實例來表示:

struct AudioChannel {
    static let thresholdLevel = 10
    static var maxInputLevelForAllChannels = 0
    var currentLevel: Int = 0 {
        didSet {
            if currentLevel > AudioChannel.thresholdLevel {
                // 將當前音量限制在閾值之內(nèi)
                currentLevel = AudioChannel.thresholdLevel
            }
            if currentLevel > AudioChannel.maxInputLevelForAllChannels {
                // 存儲當前音量作為新的最大輸入音量
                AudioChannel.maxInputLevelForAllChannels = currentLevel
            }
        }
    }
}

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

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

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

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

  • 如果 currentLevel 的新值大于允許的閾值 thresholdLevel,屬性觀察器將 currentLevel 的值限定為閾值 thresholdLevel

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

注意

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

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

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

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

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

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

rightChannel.currentLevel = 11
print(rightChannel.currentLevel)
// 輸出“10”
print(AudioChannel.maxInputLevelForAllChannels)
// 輸出“10”

繼續(xù)閱讀 Swift - 方法

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容