前言
屬性將值跟特定的類、結構或枚舉關聯(lián)。存儲屬性存儲常量或變量作為實例的一部分,而計算屬性計算(不是存儲)一個值。計算屬性可以用于類、結構體和枚舉,存儲屬性只能用于類和結構體。
存儲屬性和計算屬性通常與特定類型的實例關聯(lián)。但是,屬性也可以直接作用于類型本身,這種屬性稱為類型屬性。
另外,還可以定義屬性觀察器來監(jiān)控屬性值的變化,以此來觸發(fā)一個自定義的操作。屬性觀察器可以添加到自己定義的存儲屬性上,也可以添加到從父類繼承的屬性上。
Swift 屬性可以分為 實例屬性和類型屬性
實例屬性
實例屬性可以分為 存儲屬性 和 計算屬性。
存儲屬性
一個存儲屬性就是存儲在特定類或結構體實例里的一個常量或變量。存儲屬性可以是變量存儲屬性(用關鍵字 var 定義),也可以是常量存儲屬性(用關鍵字 let 定義)。
- 類似于成員變量
- 存儲在實例的內(nèi)存中
- 結構體、類可以定義存儲屬性
- 枚舉
定義存儲屬性
關于存儲屬性,Swift 中明確規(guī)定,在創(chuàng)建類或者結構體的實例時,必須為所有的存儲屬性設置一個合適的初始值:
1)可以在初始化器里為存儲屬性設置設置一個初始值;
2)可以分配一個默認的屬性值作為屬性定義的一部分。
struct Point {
var x: Int
let y: Int
}
var p1 = Point(x: 1, y: 1)
p1.x = 2
p1.y = 2 // 報錯,y 是常量存儲屬性
let p2 = Point(x: 10, y: 10)
p2.x = 12 // 報錯,結構體(Point)是值類型。當值類型的實例被聲明為常量的時候,其屬性也就成了常量
class Point {
var x: Int
let y: Int
init(x: Int, y: Int) {
self.x = x
self.y = y
}
}
var p1 = Point(x: 1, y: 1)
p1.x = 2
p1.y = 2 // 報錯,y 是常量存儲屬性
let p2 = Point(x: 10, y: 10)
p2.x = 12 // 不報錯,類引用類型,把引用類型的實例賦給一個常量后,仍然可以修改該實例的變量屬性。
延遲存儲屬性
延遲存儲屬性是指當?shù)谝淮伪徽{(diào)用的時候才會計算其初始值的屬性。在屬性聲明前使用 lazy 來標示一個延遲存儲屬性。
實例:Person 類和 House 類,Person 類中有個存儲屬性 house,在實例化 Person 對象的時候由于 house 使用了 lazy,所以 house 只有在被第一使用的時候才會創(chuàng)建(調(diào)用 House 類中的相關方法)
class House {
var name: String
init(name: String) {
self.name = name
print("init \(name) House")
}
func sleep() {
print("sleep")
}
}
class Person {
lazy var house = House(name: "天橋")
init() {
print("init Person")
}
func goHome() {
house.sleep()
}
}
let p = Person()
print("House 還沒被創(chuàng)建")
p.goHome()
輸出結果:
init Person
House 還沒被創(chuàng)建
init 天橋 House
sleep
如果不使用 lazy 則:
init 天橋 House
init Person
House 還沒被創(chuàng)建
sleep
延遲屬性很有用,當屬性的值依賴于在實例的構造過程結束后才會知道影響值的外部因素時,或者當獲得屬性的初始值需要復雜或大量計算時,可以只在需要的時候計算它。
? 1.必須將延遲存儲屬性聲明成變量(使用 var 關鍵字),因為屬性的初始值可能在實例構造完成之后才會得到。而常量屬性(let)在構造過程完成之前必須要有初始值,因此無法聲明成延遲屬性。
? 2. 如果一個被標記為 lazy 的屬性在沒有初始化時就同時被多個線程訪問,則無法保證該屬性只會被初始化一次。
計算屬性(Computed Property)
計算屬性不直接存儲值,不用初始化,而是提供一個 getter 和一個可選的 setter,來間接獲取和設置其他屬性或變量的值。
- 本質就是方法(函數(shù))
- 不占用實例的內(nèi)存
- 枚舉、結構體、類都可以定義計算屬性
struct Square {
var width: Double
var are: Double {
get {
width * width // 只有一條 return 語句,省略 return 關鍵字
}
set(newAre){
width = sqrt(newAre)
}
/**
set 傳入新值的默認默認名稱為 newValue,所以可以簡化如下
set {
width = sqrt(newValue)
}
*/
}
}
var s = Square(width: 3)
print("width:\(s.width), are:\(s.are)")
計算屬性的 set 方法,傳入的新值默認叫 newValue,如上面實例中注釋掉的 set 方法,當然也可以自定義新值的名稱,如上面的 newAre。
只讀計算屬性
只有 getter 沒有 setter 的計算屬性就是只讀計算屬性。只讀計算屬性總是返回一個值,可以通過點運算符訪問,但不能設置新的值。
struct Square {
var width = 0.0
var are:Double {
get {
width * width
}
}
}
var s = Square(width: 2)
print("are:\(s.are)")
只讀計算屬性的聲明可以去掉 get 關鍵字和花括號,所以:
struct Square {
var width = 0.0
var are: Double {
width * width
}
}
必須使用 var 關鍵字定義計算屬性,包括只讀計算屬性,因為它們的值不是固定的。let 關鍵字只用來聲明常量屬性,表示初始化后再也無法修改的值。
屬性觀察器
屬性觀察器監(jiān)控和響應屬性值的變化,每次屬性被設置值的時候都會調(diào)用屬性觀察器,即使新值和當前值相同的時候也不例外。
可以為非
lazy 修飾的 var 的存儲屬性添加觀察器。
class Animal {
var age: Int {
//新值存儲之前調(diào)用
willSet {
print("Animal willSet newValue:\(newValue)")
}
//新值存儲之后調(diào)用
didSet {
print("Animal didSet oldValue:\(oldValue), age:\(age)")
}
}
init(age: Int) {
self.age = age
}
}
var a = Animal(age: 1)
a.age = 5
Animal willSet newValue:5
Animal didSet oldValue:1, age:5
-
willSet在新的值被設置之前調(diào)用:
willSet觀察器會將新的屬性值作為常量參數(shù)傳入,在willSet的實現(xiàn)代碼中可以為這個參數(shù)指定一個名稱,如果不指定則參數(shù)仍然可用,這時使用默認名稱newValue表示。 -
didSet在新的值被設置之后立即調(diào)用:
didSet觀察器會將舊的屬性值作為參數(shù)傳入,可以為該參數(shù)命名或者使用默認參數(shù)名oldValue。如果在didSet方法中再次對該屬性賦值,那么新值會覆蓋舊的值。 - 在初始化器中設置屬性值,不會出發(fā)
willSet和didSet。
父類和子類針對同一個存儲屬性同時存在屬性觀察器
class Dog: Animal {
override var age: Int {
willSet {
print("Dog willSet newValue:\(newValue)")
}
didSet {
print("Dog didSet oldValue:\(oldValue), age:\(age)")
}
}
}
var d = Dog(age: 6)
d.age = 10
Dog willSet newValue:10
Animal willSet newValue:10
Animal didSet oldValue:6, age:10
Dog didSet oldValue:6, age:10
對于同一個屬性,子類和父類都有屬性觀察者,其順序是:先子類willSet,后父類 willSet,再父類 didSet, 子類的 didSet
父類的屬性在子類的構造器中被賦值時,它在父類中的 willSet 和 didSet 觀察器會被調(diào)用,隨后才會調(diào)用子類的觀察器。
番外 - 各種實例屬性通過 inout 方式傳入函數(shù)
存儲屬性通過 inout 方式傳入函數(shù)
func inoutFunc(v1: inout Int) {
v1 = 666
print("inoutFunc")
}
struct Line {
var width = 1
}
var l = Line()
inoutFunc(v1: &l.width)
print("Line inout:\(l.width)") //輸出 Line inout:666
帶有屬性觀察器的存儲屬性通過 inout 方式傳入函數(shù)
struct Line {
var width = 1 {
willSet {
print("Line willSet")
}
didSet {
print("Line didSet")
}
}
}
var l = Line()
inoutFunc(v1: &l.width)
print("Line inout:\(l.width)")
輸出信息:
inoutFunc
Line willSet
Line didSet
Line inout:666
可以看到在 inoutFunc 函數(shù)打印信息之后調(diào)用了 willSet 和 didSet
計算屬性通過 inout 方式傳入函數(shù)
struct Line {
var width = 1
var widthTest: Int {
set {
width = newValue
print("Line set")
}
get {
print("Line get")
return width
}
}
}
var l = Line()
inoutFunc(v1: &l.widthTest)
print("Line inout:\(l.width)")
輸出:
Line get
inoutFunc
Line set
Line inout:666
可以看到,傳入 inoutFunc 函數(shù)時,通過 get 取到值,完成 inoutFunc 后,又調(diào)用了 set 方法,設置值。
- 如果實參有物理內(nèi)存地址,且沒有設置屬性觀察器
直接將實參的內(nèi)存地址傳入函數(shù)(實參進行引用傳遞) - 如果實參是計算屬性 或者 設置了屬性觀察器
采取了 拷入拷出模式(在函數(shù)內(nèi)部使用的是參數(shù)的copy,函數(shù)結束后,又對參數(shù)重新賦值。)
1.調(diào)用該函數(shù)時,先復制實參的值,產(chǎn)生副本【get】
2.將副本的內(nèi)存地址傳入函數(shù)(副本進行引用傳遞),在函數(shù)內(nèi)部可以修改副本的值
3.函數(shù)返回后,再將副本的值覆蓋實參的值【set】
總結: inout 的本質就是引用傳遞(地址傳遞)
全局變量和局部變量
計算屬性和屬性觀察器所描述的功能也可以用于全局變量和局部變量。全局變量是在函數(shù)、方法、閉包或任何類型之外定義的變量。局部變量是在函數(shù)、方法或閉包內(nèi)部定義的變量。
全局或局部變量都屬于存儲型變量,跟存儲屬性類似,它為特定類型的值提供存儲空間,并允許讀取和寫入。
另外,在全局或局部范圍都可以定義計算型變量和為存儲型變量定義觀察器。計算型變量跟計算屬性一樣,返回一個計算結果而不是存儲值,聲明格式也完全一樣。
var number: Int {
set {
print("number set")
}
get {
print("number get")
return 1
}
}
number = 11
print(number)
打印信息:
number set
number get
1
類型屬性
為類型本身定義屬性,無論創(chuàng)建了多少個該類型的實例,這些屬性都只有唯一一份,這種屬性就是類型屬性。只能通過類型去訪問。
類型屬性可以分為:存儲型類型屬性和計算型類型屬性。
存儲型類型屬性 - 可以是變量或常量,在整個程序運行過程中,只有 1 份內(nèi)存(類似全局變量)。
計算型類型屬性 - 跟實例的計算型屬性一樣只能定義成變量屬性。
- 跟實例的存儲型屬性不同,必須給存儲型類型屬性指定默認值,因為類型本身沒有構造器,也就無法在初始化過程中使用構造器給類型屬性賦值。
- 存儲型類型屬性是延遲初始化的,它們只有在第一次被訪問的時候才會被初始化。即使它們被多個線程同時訪問,系統(tǒng)也保證只會對其進行一次初始化,并且不需要對其使用
lazy修飾符。 - 存儲型類型屬性在初始化的時候,通過斷點調(diào)試可以看到起調(diào)用 swift_once 并最終調(diào)用 dispatch_once 對存儲型類型屬性進行初始化(賦初值)
使用關鍵字 static 來定義類型屬性。如果是類的計算型類型屬性,可以改用關鍵字 class。
枚舉、結構體和類均可以定義類型屬性:
enum PointEnum {
static var x: Int = 0
static var y: Int {
return 0
}
}
struct PointStuct {
static var x: Int = 1
static var y: Int {
return 1
}
}
class PointClass {
static var x: Int = 2
static var y: Int {
return 2
}
class var z: Int {
return 2
}
}