屬性分類
Swift中跟實例相關的屬性可以分爲2大類:
- 存儲屬性(Stored Property)
- 類似於成員變量這個概念
- 存儲在實例的內存中
- 結構體、類可以定義存儲屬性
- 枚舉不可以定義存儲屬性
- 計算屬性(Computed Property)
- 本質就是方法(函數)
- 不佔用實例的內存
- 枚舉、結構體、類都可以定義計算屬性
由反彙編我們可以看出,只有radius這個存儲屬性會存儲在實力對象,佔用堆空間,而diameter這個計算屬性相當於方法,不會佔用堆空間,所以類佔用的內存空間爲8字節,只是radius這個存儲屬性的8字節
存儲屬性
關於存儲屬性,Swift有個明確的規定 :在創建類 或 結構體的實例時,必須爲所有的存儲屬性設置一個合適的初始值
- 可以在初始化器裏爲存儲屬性設置一個初始值
struct Point {
var x : Int
var y : Int
init () {
x = 11
y = 22
}
}
2. 可以分配一個默認的屬性值作爲屬性定義的一部分
class Point {
var x : Int = 11
var y : Int = 22
}
計算屬性
set傳入的新值默認叫做newValue,也可以自定義
只讀計算屬性:只有get,沒有set
==> 也可以直接寫爲:
定義計算屬性只能用var,不能用let ,因爲:
- let代表常量:值是一成不變的
- 計算屬性的值是可能發生變化的(即使是隻讀計算屬性)
注:但是計算屬性不能只有set,沒有get
枚舉rawValue原理
枚舉原始值rawValue的本質是:只讀計算屬性
通過反彙編,如下圖:
我們可以知道,rawValue只是調用了getter方法,在上面的代碼中重寫了rawValue的getter方法,所以結果產生了變化,由3變爲12,如圖:
延遲存儲屬性(Lazy Stored Property)
使用lazy可以定義一個延遲存儲屬性,在第一次用到屬性的時候纔會進行初始化
從下面的輸出結果我們可以看出,Person初始化的時候並不會初始化Car,只有在調用Car的時候纔會初始化Car,這就是延遲存儲屬性
爲什麼要使用延遲存儲屬性? 因爲有些比如數據下載等的耗時操作沒必要初始化就加載,用的時候加載就好,比如下圖:
延遲存儲屬性注意點
當結構體包含一個延遲存儲屬性時,只有var才能訪問延遲存儲屬性,
因爲延遲屬性初始化時需要改變結構體的內存
在上面的代碼中,當調用let修飾的實例對象p的延遲存儲屬性z時是不被允許的,因爲此時會初始化z並修改p的內存,這是let修飾的常量禁止的,但是你可以調用x和y,因爲他們在p初始化的時候已經初始化了,只有把p修改成var類型的,才允許調用z。
屬性觀察器(Property Observer)
可以爲非lazy的var存儲屬性設置屬性觀察器
在給radius賦值的時候,會先觸發willset代表即將賦值,賦完值後會觸發didSet,代表賦完值
- willSet會傳遞新值,默認叫newValue
- didSet會傳遞舊值,默認叫oldValue
- 在初始化器中設置屬性值不會觸發willSet和didSet ,即調用Circle(),觸發init()方法時
- 在屬性定義時設置初始值也不會觸發willSet和didSet
全局變量 局部變量
屬性觀察器、計算屬性的功能,同樣可以應用在全局變量、局部變量身上
num是全局變量,使用了計算屬性
age在函數中,是局部變量,使用了屬性觀察期
inout的本質總結
==> 輸出結果
test(&s.width): 直接將width屬性的地址值傳了進去
test(&s.girth): 通過反彙編,我們可以看到先走girth的getter方法,獲取到girth的值放到一個局部變量中,作爲臨時存儲空間(棧空間),再將局部變量的地址值傳入test函數,函數內部通過地址值找到存儲空間,將20這個值放進去,接下來調用setter方法,將局部變量的20賦給girth的newValue,完成賦值。彙編下圖:
test(&s.side): 帶有觀察器的存儲屬性賦值過程:現將side的值放到局部變量,將局部變量的地址傳入test方法,完成賦值,最後通過setter方法完成賦值。
帶有觀察器的存儲屬性賦值爲什麼不和普通的存儲屬性一樣直接傳入地址值進行修改? 這是因爲直接傳入地址值進行修改不會觸發willSet和didSet方法,走一個複雜的賦值過程是爲了觸發屬性觀察器。
總結:
- 如果實參有物理內存地址,且沒有設置屬性觀察器 : 直接將實參的內存地址傳入函數(實參進行引用傳遞)
- 如果實參是計算屬性 或者 設置了屬性觀察器
- 採取了Copy In Copy Out的做法
- 調用該函數時,先複製實參的值,產生副本【get】
- 將副本的內存地址傳入函數(副本進行引用傳遞),在函數內部可以修改副本的值
- 函數返回後,再將副本的值覆蓋實參的值【set】
總結:inout的本質就是引用傳遞(地址傳遞)
類型屬性
嚴格來說,屬性可以分爲
- 實例屬性(Instance Property):只能通過實例去訪問
- 存儲實例屬性(Stored Instance Property):存儲在實例的內存中,每個實例都有1份
- 計算實例屬性(Computed Instance Property):帶set和get方法的
- 類型屬性(Type Property):只能通過類型去訪問,但是並不佔用類的內存,它與全局變量類似,存儲區域在全局區
- 存儲類型屬性(Stored Type Property):整個程序運行過程中,就只有1份內存(類似於全局變量)
- 想要證明存儲類型屬性只佔用一份內存,並且線程安全,就要通過反彙編來查看:
1)通過反彙編,我們可以看到代碼的底層調用首先來到了swift_once方法
2)接下來繼續往下走,我們可以看到它走到了核心的dispatch_once_f方法,也就是GCD申請單例的方法,這樣類型屬性就實現了只申請一塊內存,並保證線程安全
3. 計算類型屬性(Computed Type Property)
- 可以通過static定義類型屬性 ,如果是類,也可以用關鍵字class,但是結構體(struct)只能使用static,不能用class
我們看到結果爲3,因爲c1, c2, c3這三個實例對象中的count屬性共用一塊內存,所以三次調用時累加的,所以爲3.
類型屬性細節
- 不同於存儲實例屬性,你可以不給存儲類型屬性設定初始值 :因爲類型沒有像實例那樣的init初始化器來初始化存儲屬性
- 存儲類型屬性默認就是lazy,會在第一次使用的時候才初始化
- 就算被多個線程同時訪問,保證只會初始化一次
- 存儲類型屬性可以是let
- 枚舉類型也可以定義類型屬性(存儲類型屬性、計算類型屬性):因爲存儲類型屬性不同於存儲實例屬性,不在實例對象中佔用內存,而枚舉中正是因爲不能開闢空間存儲實例屬性所以不能包含存儲實例屬性,但是由於存儲類型屬性不佔用實例對象內存空間的特性,所以枚舉類型可以定義類型屬性。
單例模式
第一種創建方式:不需要添加其他處理操作時使用,直接創建一個單例
第二種創建方式:需要添加其他處理操作時使用,用閉包的方式創建一個單例,把要添加的其他操作放在閉包中
何時用存儲屬性和計算屬性
當你的屬性想要存儲下來的時候使用存儲屬性
當你的屬性可以通過別的屬性通過一定的計算方法得到,就用計算屬性,否則我們還需要加個方法去表示這個屬性和其他屬性的關係。