案例代碼下載
屬性
屬性將值與特定類,結(jié)構(gòu)或枚舉相關聯(lián)楷怒。存儲的屬性將常量和變量值存儲為實例的一部分蛋勺,而計算屬性則計算(而不是存儲)值。計算屬性由類鸠删,結(jié)構(gòu)和枚舉提供抱完。存儲的屬性僅由類和結(jié)構(gòu)提供。
存儲和計算屬性通常與特定類型的實例相關聯(lián)刃泡。但是乾蛤,屬性也可以與類型本身相關聯(lián)。這些屬性稱為類型屬性捅僵。
此外,可以定義屬性觀察器以監(jiān)視屬性值的更改眨层,可以使用自定義操作進行響應庙楚。可以將屬性觀察器添加到自定義的存儲性中趴樱,也可以添加到子類從其超類繼承的屬性中馒闷。
存儲屬性
在其最簡單的形式中,存儲屬性是一個常量或變量叁征,存儲為特定類或結(jié)構(gòu)的實例的一部分纳账。存儲的屬性可以是變量存儲屬性(由var關鍵字引入),也可以是常量存儲屬性(由let關鍵字引入)捺疼。
可以為存儲屬性提供默認值作為其定義的一部分疏虫,如“ 默認屬性值”中所述。還可以在初始化期間設置和修改存儲屬性的初始值啤呼。即使對于常量存儲屬性也是如此卧秘,如初始化期間分配常量屬性中所述。
下面的示例定義了一個名為FixedLengthRange的結(jié)構(gòu)官扣,它描述了一系列整數(shù)翅敌,其范圍長度在創(chuàng)建后無法更改:
struct FixedLengthRange {
var firstValue: Int
let length: Int
}
var rangeOfThreeItems = FixedLengthRange(firstValue: 0, length: 3);
rangeOfThreeItems.firstValue = 6;
FixedLengthRange的實例具有變量存儲屬性firstValue和常量存儲屬性length。在上面的示例中惕蹄,length在創(chuàng)建新范圍時初始化蚯涮,此后無法更改治专,因為它是常量屬性。
常數(shù)結(jié)構(gòu)實例的存儲性質(zhì)
如果創(chuàng)建結(jié)構(gòu)的實例并將該實例分配給常量遭顶,則無法修改實例的屬性张峰,即使它們被聲明為變量屬性:
let rangeOfThreeItems = FixedLengthRange(firstValue: 0, length: 3)
rangeOfThreeItems.firstValue = 6
因為rangeOfFourItems聲明為常量(使用let關鍵字),無法更改其firstValue屬性液肌,即使firstValue是變量屬性挟炬。
此行為是由于結(jié)構(gòu)是值類型。當值類型的實例標記為常量時嗦哆,其所有屬性也都標記為常量谤祖。
對于類情況不同,類是引用類型老速。如果將引用類型的實例分配給常量粥喜,則仍可以更改該實例的變量屬性。
懶加載存儲屬性
懶加載存儲屬性是一個初始值是不計算的第一次直到使用它橘券。通過在聲明之前編寫lazy修飾符來指示懶加載存儲的屬性额湘。
注意: 必須始終將懶加載屬性聲明為變量(使用var關鍵字),因為在實例初始化完成之后旁舰,可能無法檢索其初始值锋华。常量屬性在初始化完成之前必須始終具有值,因此不能聲明為懶加載屬性箭窜。
當屬性的初始值依賴于外部因素時毯焕,懶加載屬性非常有用,這些外部因素的值在實例初始化完成之后才知道磺樱。當屬性的初始值的設置需要復雜或昂貴的計算時纳猫,懶加載屬性也很有用,除非需要竹捉,否則不應執(zhí)行芜辕。
下面的示例使用懶加載存儲屬性來避免復雜類的不必要的初始化。本實施例中定義了兩個類叫DataImporter和DataManager:
class 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")
DataManager類有一個存儲屬性叫做data块差,用一個新的侵续,空String數(shù)組初始化。雖然未顯示其余功能憾儒,但DataManager類的目的是管理并提供對此String數(shù)據(jù)數(shù)組的訪問询兴。
DataManager類的部分功能是從文件導入數(shù)據(jù)的能力。此功能由DataImporter類提供起趾,假設需要花費大量時間進行初始化诗舰。這可能是因為DataImporter實例需要在DataImporter初始化實例時打開文件并將其內(nèi)容讀入內(nèi)存。
DataManager實例可以在不從文件導入數(shù)據(jù)的情況下管理其數(shù)據(jù)训裆,因此在創(chuàng)建DataManager自身時無需創(chuàng)建新DataImporter實例眶根。相反蜀铲,在第一次使用DataImporter時,創(chuàng)建實例更有意義属百。
因為它用lazy修飾符標記记劝,所以只有在首次訪問importer屬性時才會創(chuàng)建importer屬性的DataImporter實例,例如其filename屬性被訪問時:
print(manager.importer.filename)
注意: 如果同時由多個線程訪問標記有l(wèi)azy修飾符的屬性且該屬性尚未初始化族扰,則無法保證該屬性僅初始化一次厌丑。
存儲屬性和實例變量
如果有使用Objective-C的經(jīng)驗,可能知道它提供了兩種方法來存儲值和引用作為類實例的一部分渔呵。除了屬性之外怒竿,還可以使用實例變量作為存儲在屬性中的值的后備存儲。
Swift將這些概念統(tǒng)一到一個屬性聲明中扩氢。Swift屬性沒有相應的實例變量耕驰,并且不直接訪問屬性的后備存儲。這種方法避免了在不同的上下文中如何訪問值的混淆录豺,并將屬性的聲明簡化為單個明確的語句朦肘。有關屬性的所有信息(包括其名稱,類型和內(nèi)存管理特征)都在單個位置定義双饥,作為類型定義的一部分媒抠。
計算屬性
除了存儲屬性之外,類咏花,結(jié)構(gòu)和枚舉還可以定義計算屬性领舰,這些屬性實際上不存儲值。相反迟螺,它們提供了一個getter和一個可選的setter來間接檢索和設置其他屬性和值。
struct Point {
var x = 0.0, y = 0.0
}
struct Size {
var with = 0.0, height = 0.0
}
struct Rect {
var origin = Point()
var size = Size()
var center: Point {
get {
let centerX = origin.x + size.with/2
let centerY = origin.y + size.height/2
return Point(x: centerX, y: centerY)
}
set(newCenter) {
origin.x = newCenter.x - size.with/2
origin.y = newCenter.y - size.height/2
}
}
}
var square = Rect(origin: Point(x: 0.0, y: 0.0), size: Size(with: 10.0, height: 10.0))
let initialSquareCenter = square.center
square.center = Point(x: 15.0, y: 15.0)
print(square)
此示例定義了三種用于處理幾何形狀的結(jié)構(gòu):
- Point 封裝了一個點的x坐標和y坐標舍咖。
- Size封裝a width和a height矩父。
- Rect 按原點和大小定義矩形。
Rect結(jié)構(gòu)還提供了一個名為center的計算屬性排霉。Rect的當前中心位置總是可以從它的origin和size確定窍株,所以不需要中心點存儲為一個明確的Point值。Rect而是自定義一個getter和setter來計算center變量攻柠,使得能夠使用矩形center球订,就像它是一個真正的存儲屬性一樣。
上面的例子創(chuàng)建了一個Rect名為square的新變量瑰钮。square變量被原點(0, 0)冒滩,和寬度與高度為10初始化。該正方形由下圖中的藍色方塊表示浪谴。
該square變量的center屬性开睡,然后通過點語法訪問(square.center)因苹,這會導致需要getter center被調(diào)用,獲取當前的屬性值篇恒。getter實際上不是返回現(xiàn)有值扶檐,而是實際計算并返回一個新的Point來表示方形的中心。如上所示胁艰,getter正確返回中心點(5, 5)款筑。
然后將center屬性設置為新值(15, 15),該值將方塊向上和向右移動到下圖中橙色方塊所示的新位置腾么。設置center屬性會調(diào)用center的setter方法奈梳,它會修改存儲屬性origin的x值和y值,并將方塊移動到新位置哮翘。
速記setter聲明
如果計算屬性的setter沒有為要設置的新值定義名稱颈嚼,newValue則使用默認名稱。這是該Rect結(jié)構(gòu)的替代版本饭寺,它利用了這種速記符號:
struct AlternativeRect {
var origin = Point()
var size = Size()
var center: Point {
get {
let centerX = origin.x + size.with/2
let centerY = origin.y + size.height/2
return Point(x: centerX, y: centerY)
}
set {
origin.x = newValue.x - size.with/2
origin.y = newValue.y - size.height/2
}
}
}
只讀計算屬性
具有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(fourByFiveByTwo.volume)
這個例子定義了一個新的結(jié)構(gòu)叫做Cuboid旺上,其表示立方體的width,height和depth屬性糖埋。此結(jié)構(gòu)還具有一個只讀的計算屬性volume宣吱,它可以計算并返回立方體的當前體積。設置volume是沒有任何意義的瞳别,因為這將弄不清width征候,height以及depth的哪個值應該用于特定的volume值。盡管如此祟敛,Cuboid提供只讀計算屬性以使外部用戶能夠發(fā)現(xiàn)其當前計算的體積是有用的疤坝。
屬性觀察者
屬性觀察者觀察并響應屬性值的變化。每次設置屬性值時都會調(diào)用屬性觀察者馆铁,即使新值與屬性的當前值相同跑揉。
可以將屬性觀察者添加到定義的任何存儲屬性,但惰加載存儲屬性除外埠巨。還可以通過覆蓋子類中的屬性畔裕,將屬性觀察者添加到任何繼承的屬性(無論是存儲還是計算屬性)衣撬。不需要為非重寫的計算屬性定義屬性添加觀察者,因為可以在計算屬性的setter中觀察并響應其值的更改扮饶。Overriding中描述了屬性覆蓋具练。
可以選擇在屬性上定義其中一個或兩個觀察者:
- willSet 在存儲值之前調(diào)用。
- didSet 在存儲新值后立即調(diào)用甜无。
如果實現(xiàn)了一個willSet觀察者扛点,它會將新屬性值作為常量參數(shù)傳遞∑袂穑可以在willSet實現(xiàn)過程中指定此參數(shù)的名稱陵究。如果未在實現(xiàn)中編寫參數(shù)名稱和括號,則該參數(shù)的默認參數(shù)名稱為newValue奥帘。
類似地铜邮,如果您實現(xiàn)了一個didSet觀察者,它會傳遞一個包含舊屬性值的常量參數(shù)寨蹋∷伤猓可以為參數(shù)命名或使用默認參數(shù)名稱oldValue。如果為其自己的didSet觀察者中的屬性分配值已旧,則分配的新值將替換剛剛設置的值秸苗。
注意: 在父類的初始化調(diào)用之后,子類初始化時設置一個屬性运褪,父類屬性的willSet與didSet會被調(diào)用惊楼。在調(diào)用父類初始化程序之前,類在設置自己的屬性時不會調(diào)用它們秸讹。
下面是一個willSet檀咙,和didSet功能的例子。下面的示例定義了一個名為StepCounter的新類璃诀,它跟蹤一個人在行走時所采取的步伐總數(shù)攀芯。該類可以與來自計步器或其他步數(shù)計數(shù)器的輸入數(shù)據(jù)一起使用,以記錄人們在日常生活中的運動文虏。
class StepCounter {
var totalSteps = 0 {
willSet {
print("willSet \(newValue)")
}
didSet {
if totalSteps > oldValue {
print("didSet \(totalSteps)")
}
}
}
}
let stepCounter = StepCounter()
stepCounter.totalSteps = 200
stepCounter.totalSteps = 360
stepCounter.totalSteps = 896
在StepCounter類聲明了一個Int類型的totalSteps屬性。這是一個有willSet和didSet觀察者的存儲屬性殖演。
每當totalSteps屬性分配一個新的值時willSet和didSet觀察者被調(diào)用氧秘。即使新值與當前值相同,也是如此趴久。
此示例的willSet觀察者使用自定義參數(shù)名稱newTotalSteps來表示即將到來的新值丸相。在此示例中,它只是打印出即將設置的值彼棍。
在更新totalSteps值之后調(diào)用didSet觀察者灭忠。它將totalSteps新值與舊值進行比較膳算。如果步伐總數(shù)增加,則會打印一條消息弛作,指示已執(zhí)行了多少新步數(shù)涕蜂。didSet觀察者不提供舊值自定義參數(shù)名稱,用默認的名稱oldValue來代替映琳。
注意: 如果將具有觀察者的屬性作為輸入輸出參數(shù)傳遞給函數(shù)机隙,則始終會調(diào)用willSet和didSet觀察者。這是因為in-out參數(shù)的copy-in copy-out內(nèi)存模型:該值總是在寫回屬性萨西。有關輸入輸出參數(shù)行為的詳細討論有鹿,請參閱輸入輸出參數(shù)。
全局和局部變量
上面描述的用于計算和觀察屬性的功能也可用于全局變量和局部變量谎脯。全局變量是在任何函數(shù)葱跋,方法懈息,閉包或類型上下文之外定義的變量咽安。局部變量是在函數(shù)蕊连,方法或閉包上下文中定義的變量陕壹。
在前面章節(jié)中遇到的全局變量和局部變量都是存儲變量舵变。存儲變量(如存儲屬性)為特定類型的值提供存儲奢浑,并允許設置和檢索該值邦马。
還可以在全局或局部區(qū)域內(nèi)定義計算變量眼虱、存儲變量觀察者脑溢。計算變量計算它們的值僵朗,而不是存儲它們,它們的編寫方式與計算屬性相同屑彻。
注意: 全局常量和變量總是懶加載計算的验庙,與懶加載屬性類似。與懶加載存儲屬性不同社牲,全局常量和變量不需要使用lazy修飾符標記粪薛。局部常量和變量永遠不會懶加載計算。
類型屬性
實例屬性是屬于特定類型的實例的屬性搏恤。每次創(chuàng)建該類型的新實例時违寿,它都有自己的設置屬性值,與任何其他實例分開熟空。
還可以定義屬于該類型本身的屬性藤巢,而不是該類型的任何一個實例。無論創(chuàng)建的該類型的實例有多少息罗,這些屬性都只會有一個副本掂咒。這些屬性稱為類型屬性。
類型屬性對于定義對特定類型的所有實例通用的值很有用,例如所有實例都可以使用的常量屬性(如C中的靜態(tài)常量)绍刮,或者存儲該類型所以實例全局值的變量屬性(如C中的靜態(tài)變量)温圆。
存儲類型屬性可以是變量或常量。計算類型屬性始終聲明為變量屬性孩革,與計算實例屬性的方式相同岁歉。
注意: 與存儲實例屬性不同,必須始終為存儲類型屬性提供默認值嫉戚。這是因為類型本身沒有可以在初始化時為存儲類型屬性賦值的初始化程序刨裆。存儲類型屬性在首次訪問時會被初始化。它們只保證初始化一次彬檀,即使在同時由多個線程訪問時也是如此帆啃,并且它們不需要用lazy修飾符標記。
類型屬性語法
在C和Objective-C中窍帝,將與類型關聯(lián)的靜態(tài)常量和變量定義為全局靜態(tài)變量努潘。但是,在Swift中坤学,類型屬性是作為類型定義的一部分寫入的疯坤,在類型的外部花括號中,并且每個類型屬性都顯式限定為它支持的類型深浮。
可以使用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)
SomeStructure.storedTypeProperty = "another value"
print(SomeStructure.storedTypeProperty)
print(SomeEnumeration.computedTypeProperty)
print(SomeClass.computedTypeProperty)
以下示例使用兩個存儲類型屬性作為的結(jié)構(gòu)體的一部分建模多個音頻通道音量。每個通道都有一個介于0和10之間的整數(shù)音量贸街。
下圖說明了如何組合其中兩個音頻通道來模擬立體音頻音量庵寞。當通道的音量為0時,該通道的任何燈都不會亮起薛匪。當音頻音量為10時捐川,該通道的所有燈都會亮起。在此圖中蛋辈,左聲道的當前音量為9,右聲道的當前音量為7:
上述音頻通道由AudioChannel結(jié)構(gòu)實例表示:
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結(jié)構(gòu)定義了兩個存儲的類型屬性以支持其功能。第一個冷溶,thresholdLevel定義音量可以采用的最大閾值渐白。這個的常量值10所有AudioChannel實例擁有。如果音頻信號的值高于10逞频,則將限制為此閾值(如下所述)纯衍。
第二個類型屬性是一個名為maxInputLevelForAllChannels的變量存儲屬性。這會跟蹤任何 AudioChannel實例接收的最大輸入值苗胀。它以初始值開始0襟诸。
AudioChannel結(jié)構(gòu)還定義了一個名為currentLevel的存儲實例屬性,它表示通道的當前音量0到10基协。
currentLevel屬性有一個didSet屬性觀察者來檢查設置的currentLevel值歌亲。該觀察者執(zhí)行兩項檢查:
- 如果currentLevel新值大于允許值thresholdLevel,則currentLevel屬性觀察者將限制為thresholdLevel澜驮。
- 如果currentLevel的新值(在任何賦值之后)高于先前由任何 AudioChannel實例接收的任何值陷揪,則屬性觀察者將新currentLevel值存儲在maxInputLevelForAllChannelstype屬性中。
注意: 在這兩個檢查的第一個中杂穷,didSet觀察者設置currentLevel為不同的值悍缠。但是,這不會導致再次調(diào)用觀察者耐量。
可以使用AudioChannel結(jié)構(gòu)來創(chuàng)建兩個新的音頻通道叫l(wèi)eftChannel和rightChannel飞蚓,代表立體聲音響系統(tǒng)的音量:
var leftChannel = AudioChannel()
var rightChannel = AudioChannel()
如果currentLevel將左通道設置為7,則可以看到maxInputLevelForAllChannelstype屬性更新為7:
leftChannel.currentLevel = 7
print(leftChannel.currentLevel)
print(AudioChannel.maxInputLevelForAllChannels)
如果您嘗試將右側(cè)通道currentLevel設置為11廊蜒,則可以看到右側(cè)通道的currentLevel屬性上限為最大值10趴拧,并且maxInputLevelForAllChannelstype屬性更新為10:
rightChannel.currentLevel = 11
print(rightChannel.currentLevel)
print(AudioChannel.maxInputLevelForAllChannels)