Swift(5.1)-屬性

屬性

屬性將值與特定的類、結(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ù)雜類中不必要的初始化工作。例子中定義了 DataImporterDataManager 兩個(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è)名為 squareRect 實(shí)例,初始值原點(diǎn)是 (0, 0),寬度高度都是 10。如下圖中藍(lán)色正方形所示。

squarecenter 屬性可以通過點(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 來修改屬性 originxy 的值,從而實(shí)現(xiàn)移動(dòng)正方形到新的位置。

image.png

簡(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、heightdepth 屬性。結(jié)構(gòu)體還有一個(gè)名為 volume 的只讀計(jì)算屬性用來返回立方體的體積。為 volume 提供 setter 毫無意義,因?yàn)闊o法確定如何修改 width、heightdepth 三者的值來匹配新的 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)用父類屬性的 willSetdidSet 觀察器。而在父類初始化方法調(diào)用之前,給子類的屬性賦值時(shí)不會(huì)調(diào)用子類屬性的觀察器。

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

下面是一個(gè) willSetdidSet 實(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è)叫 totalStepsInt 類型的屬性。它是一個(gè)存儲(chǔ)屬性,包含 willSetdidSet 觀察器。

當(dāng) totalSteps 被設(shè)置新值的時(shí)候,它的 willSetdidSet 觀察器都會(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ù),willSetdidSet 也會(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"

heightwidth 屬性從 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í)例,即 TwelveOrLessheightwidth 的 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)建包裝 heightwidthSmallNumber 的實(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)建包裝 heightwidthSmallNumber 的實(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)建包裝 heightSmallNumber 的一個(gè)實(shí)例。調(diào)用 SmallNumber(wrappedValue: 3, maximum: 4) 來創(chuàng)建包裝 widthSmallNumber 的一個(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)建包裝 heightSmallNumber 的一個(gè)實(shí)例,這個(gè)實(shí)例使用默認(rèn)最大值 12。調(diào)用 SmallNumber(wrappedValue: 2, maximum: 9) 來創(chuàng)建包裝 widthSmallNumber的一個(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)整了新值。

注意:wrappedValueprojectedValue 為內(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 引用包裝器 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 的行為與訪問任何其他屬性的行為相同。舉個(gè)例子,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。

全局變量和局部變量

計(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è)聲道具有 010 之間的整數(shù)音量。

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

image.png

上面所描述的聲道模型使用 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)在的音量,取值為 010

屬性 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è)聲道 leftChannelrightChannel,用以表示立體聲系統(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”
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

  • 屬性 屬性將值與特定的類、結(jié)構(gòu)體或枚舉關(guān)聯(lián)。存儲(chǔ)屬性會(huì)將常量和變量存儲(chǔ)為實(shí)例的一部分,而計(jì)算屬性則是直接計(jì)算(而不...
    xiaofu666閱讀 533評(píng)論 0 0
  • 引言 繼續(xù)學(xué)習(xí)Swift文檔,從上一章節(jié):結(jié)構(gòu)體和類,我們學(xué)習(xí)了Swift結(jié)構(gòu)體和類相關(guān)的內(nèi)容,如結(jié)構(gòu)體和類的定義...
    shiyueZ閱讀 1,609評(píng)論 0 2
  • 屬性將值與特定的類,結(jié)構(gòu)或枚舉關(guān)聯(lián)。存儲(chǔ)的屬性將常量和變量值存儲(chǔ)為實(shí)例的一部分,而計(jì)算的屬性將計(jì)算(而不是存儲(chǔ))值...
    正十七邊閱讀 475評(píng)論 0 0
  • 當(dāng)處理代表某種狀態(tài)形式的屬性時(shí),通常會(huì)在每次修改值時(shí)觸發(fā)某種關(guān)聯(lián)的邏輯。例如,我們可以根據(jù)一組規(guī)則驗(yàn)證每個(gè)新值,可...
    韋弦Zhy閱讀 1,011評(píng)論 0 3
  • 存儲(chǔ)屬性 - Stored Properties 相當(dāng)于 OC 的下劃線成員變量 適用于:結(jié)構(gòu)體 、 類 類型:常...
    Sunday_David閱讀 409評(píng)論 0 0

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