屬性的基本概念
Swift中跟實體相關的屬性可以分為2大類
-
存盤屬性(Stored Property)
- 類似于成員變數的概念
- 存盤在實體的記憶體中
- 結構體、類可以定義存盤屬性
- 列舉不可以定義存盤屬性
-
計算屬性(Computed Property)
- 本質就是方法(函式)
- 不占用實體的記憶體
- 列舉、結構體、類都可以定義計算屬性
存盤屬性
關于存盤屬性,Swift有個明確的規定:在創建類或結構體的實體時,必須為所有的存盤屬性設定一個合適的初始值
可以在初始化器里為存盤屬性設定一個初始值
struct Point {
// 存盤屬性
var x: Int
var y: Int
}
let p = Point(x: 10, y: 10)
可以分配一個默認的屬性值作為屬性定義的一部分
struct Point {
// 存盤屬性
var x: Int = 10
var y: Int = 10
}
let p = Point()
計算屬性
定義計算屬性只能用var,不能用let
let代表常量,值是一直不變的- 計算屬性的值是可能發生變化的(即使是只讀計算屬性)
struct Circle {
// 存盤屬性
var radius: Double
// 計算屬性
var diameter: Double {
set {
radius = newValue / 2
}
get {
radius * 2
}
}
}
var circle = Circle(radius: 5)
circle.diameter = 12
print(circle.diameter)
set傳入的新值默認叫做newValue,也可以自定義
struct Circle {
// 存盤屬性
var radius: Double
// 計算屬性
var diameter: Double {
set(newDiameter) {
radius = newDiameter / 2
}
get {
radius * 2
}
}
}
var circle = Circle(radius: 5)
circle.diameter = 12
print(circle.diameter)
只讀計算屬性,只有get,沒有set
struct Circle {
// 存盤屬性
var radius: Double
// 計算屬性
var diameter: Double {
get {
radius * 2
}
}
}
struct Circle {
// 存盤屬性
var radius: Double
// 計算屬性
var diameter: Double { radius * 2 }
}
}
列印Circle結構體的記憶體大小,其占用才8個位元組,其本質是因為計算屬性相當于函式
var circle = Circle(radius: 5)
print(Mems.size(ofVal: &circle)) // 8
我們可以通過反匯編來查看其內部做了什么
可以看到內部會呼叫set方法去計算

然后我們在往下執行,還會看到get方法的呼叫

所以可以用此證明計算屬性只會生成getter和setter
注意:
一旦將存盤屬性變為計算屬性,初始化構造器就會報錯,只允許傳入存盤屬性的值
因為存盤屬性是直接存盤在結構體記憶體中的,如果改成計算屬性則不會分配記憶體空間來存盤


如果只有setter也會報錯

列舉的計算屬性
列舉原始值rawValue的本質也是計算屬性,而且是只讀的計算屬性
enum TestEnum: Int {
case test1, test2, test3
var rawValue: Int {
switch self {
case .test1:
return 10
case .test2:
return 20
case .test3:
return 30
}
}
}
print(TestEnum.test1.rawValue)
下面我們去掉自己寫的rawValue,然后轉匯編看下本質是什么樣的
enum TestEnum: Int {
case test1, test2, test3
}
print(TestEnum.test1.rawValue)

可以看到底層確實是呼叫了getter
延遲存盤屬性(Lazy Stored Property)
使用lazy可以定義一個延遲存盤屬性,在第一次用到屬性的時候才會進行初始化
看下面的示例代碼,如果不加lazy,那么Person初始化之后就會進行Car的初始化
加上lazy,只有呼叫到屬性的時候才會進行Car的初始化
class Car {
init() {
print("Car init!")
}
func run() {
print("Car is running!")
}
}
class Person {
lazy var car = Car()
init() {
print("Person init!")
}
func goOut() {
car.run()
}
}
let p = Person()
print("----")
p.goOut()
// 列印:
// Person init!
// ----
// Car init!
// Car is running!
lazy屬性必須是var,不能是let
let必須在實體的初始化方法完成之前就擁有值
class PhotoView {
lazy var image: UIImage = {
let url = "http://www.***.com/logo.png"
let data = Data(url: url)
return UIImage(data: data)
}()
}
注意:lazy屬性和普通的存盤屬性記憶體布局是一樣的,不同的只是什么時候會被放進記憶體中而且
延遲存盤屬性的注意點
1.如果多條執行緒同時第一次訪問lazy屬性,無法保證屬性只被初始化一次
2.當結構體包含一個延遲存盤屬性時,只有var才能訪問延遲存盤屬性
因為延遲存盤屬性初始化時需要改變結構體的記憶體

屬性觀察器(Property Observer)
可以為非lazy的var屬性設定屬性觀察器
只有存盤屬性可以設定屬性觀察器
willSet會傳遞新值,默認叫newValue
didSet會傳遞舊值,默認叫oldValue
struct Circle {
// 存盤屬性
var radius: Double {
willSet {
print("willSet", newValue)
}
didSet {
print("didSet", oldValue, radius)
}
}
init() {
radius = 1.0
print("Circle init!")
}
}
var circle = Circle()
circle.radius = 10.5
// 列印
// willSet 10.5
// didSet 1.0 10.5
在初始化器中設定屬性值不會觸發willSet和didSet
struct Circle {
// 存盤屬性
var radius: Double {
willSet {
print("willSet", newValue)
}
didSet {
print("didSet", oldValue, radius)
}
}
init() {
radius = 1.0
print("Circle init!")
}
}
var circle = Circle()
在屬性定義時設定初始值也不會觸發willSet和didSet
struct Circle {
// 存盤屬性
var radius: Double = 1.0 {
willSet {
print("willSet", newValue)
}
didSet {
print("didSet", oldValue, radius)
}
}
}
var circle = Circle()
計算屬性設定屬性觀察器會報錯

全域變數和區域變數
屬性觀察器、計算屬性的功能,同樣可以應用在全域變數和區域變數身上
全域變數
var num: Int {
get {
return 10
}
set {
print("setNum", newValue)
}
}
num = 11 // setNum 11
print(num) // 10
區域變數
func test() {
var age = 10 {
willSet {
print("willSet", newValue)
}
didSet {
print("didSet", oldValue, age)
}
}
age = 11
// willSet 11
// didSet 10 11
}
test()
inout對屬性的影響
看下面的示例代碼,分別輸出什么,為什么?
struct Shape {
var width: Int
var side: Int {
willSet {
print("willSet", newValue)
}
didSet {
print("didSet", oldValue, side)
}
}
var girth: Int {
set {
width = newValue / side
print("setWidth", newValue)
}
get {
print("getWidth")
return width * side
}
}
func show() {
print("width=\(width), side=\(side), girth=\(girth)")
}
}
func test(_ num: inout Int) {
num = 20
}
var s = Shape(width: 10, side: 4)
test(&s.width)
s.show()
print("--------------------")
test(&s.side)
s.show()
print("--------------------")
test(&s.girth)
s.show()
// 列印:
// getWidth
// width=20, side=4, girth=80
// --------------------
// willSet 20
// didSet 4 20
// getWidth
// width=20, side=20, girth=400
// --------------------
// getWidth
// setWidth 20
// getWidth
// width=1, side=20, girth=20
第一段列印
初始化的時候會給width賦值為10,side賦值為4,并且不會呼叫side的屬性觀察器
然后呼叫test方法,并傳入width的地址值,width變成20
然后呼叫show方法,會呼叫girth的getter,然后先執行列印,再計算,girth為80
下面我們通過反匯編來進行分析




第二段列印
現在width的值是20,side的值是4,girth的值是80
然后呼叫test方法,并傳入side的地址值,side變成20,并且觸發屬性觀察器,執行列印
然后呼叫show方法,會呼叫girth的getter,然后先執行列印,再計算,girth為400
下面我們通過反匯編來進行分析


將地址值存盤到rdi中,并帶入到test函式中進行計算



setter中才會真正的呼叫willSet和didSet方法
willSet和didSet之間的計算才是真正的將改變了的值覆寫了全域變數里的side
真正改變了side的值的時候是呼叫完test函式之后,在內部的setter里進行的
第三段列印
現在width的值是20,side的值是20,girth的值是400
然后呼叫test方法,并傳入girth的getter的回傳值為400,然后將20賦值給girth的setter計算,width變為1
然后呼叫show方法,,會呼叫girth的getter,然后先執行列印,再計算,girth為20
下面我們通過反匯編來進行分析
















再后面都是計算的程序了,這里就不詳細跟進了
我們主要了解inout是怎么給計算屬性進行關聯呼叫的,從上面分析可以看出從呼叫girth的getter開始,都會將計算的結果放入一個暫存器中,然后通過這個暫存器的地址再進行傳遞,inout影響的也是修改這個暫存器中存盤的值,然后再進一步傳遞到setter里進行計算
inout的本質總結

對于沒有屬性觀察器的存盤屬性來說,inout的本質就是傳進來一個地址值,然后將值存盤到這個地址對應的存盤空間內
對于設定了屬性觀察器和計算屬性來說,inout會先將傳進來的地址值放到一個區域變數中,然后改變區域變數地址值對應的存盤空間
再將改變了的值覆寫最初傳進來的引數的值,這時會對應觸發屬性觀察器willSet、didSet和計算屬性的setter、getter的呼叫
如果不這么做,直接就改變了傳進來的地址值的存盤空間的話,就不會呼叫屬性觀察器了,而計算屬性因為沒有分配記憶體來存盤值,也就沒辦法更改了
inout的本質就是參考傳遞(地址傳遞)
型別屬性(Type Property)
嚴格來說,屬性可以分為兩大類
-
實體屬性(Instance Property):只能通過實體去訪問
- 存盤實體屬性(Stored Instance Property):存盤在實體的記憶體中,每個實體都有一份
- 計算實體屬性(Computed Instance Property)
-
型別屬性(Type Property):只能通過類去訪問
- 存盤型別屬性(Stored Type Property):整個程式運行程序中,就只有一份記憶體(類似于全域變數)
- 計算型別屬性(Computed Type Property)
可以通過static定義型別屬性
struct Car {
static var count: Int = 0
init() {
Car.count += 1
}
}
如果是類,也可以用關鍵字class修飾計算型別屬性
class Car {
class var count: Int {
return 10
}
}
print(Car.count)
類里面不能用class修飾存盤型別屬性

不同于存盤實體屬性,存盤型別屬性必須設定初始值,不然會報錯
因為型別沒有像實體那樣的init初始化器來初始化存盤屬性

存盤型別屬性可以用let
struct Car {
static let count: Int = 0
}
print(Car.count)
列舉型別也可以定義型別屬性(存盤型別屬性、計算型別屬性)
enum Shape {
static var width: Int = 0
case s1, s2, s3, s4
}
var s = Shape.s1
Shape.width = 5
存盤型別屬性默認就是lazy,會在第一次使用的時候進行初始化
就算被多個執行緒同時訪問,保證只會初始化一次
通過反匯編來分析型別屬性的底層實作
我們先通過列印下面兩組代碼來做對比,發現存盤型別屬性的記憶體地址和前后兩個全域變數正好相差8個位元組,所以可以證明存盤型別屬性的本質就是類似于全域變數,只是放在了結構體或者類里面控制了訪問權限
var num1 = 5
var num2 = 6
var num3 = 7
print(Mems.ptr(ofVal: &num1)) // 0x000000010000c1c0
print(Mems.ptr(ofVal: &num2)) // 0x000000010000c1c8
print(Mems.ptr(ofVal: &num3)) // 0x000000010000c1d0
var num1 = 5
class Car {
static var count = 1
}
Car.count = 6
var num3 = 7
print(Mems.ptr(ofVal: &num1)) // 0x000000010000c2f8
print(Mems.ptr(ofVal: &Car.count)) // 0x000000010000c300
print(Mems.ptr(ofVal: &num3)) // 0x000000010000c308
然后我們進行反匯編來觀察





通過呼叫我們可以發現最后會呼叫到GCD的dispatch_once,所以存盤型別屬性才會說是執行緒安全的,并且只執行一次
并且dispatch_once里面執行的代碼就是static var count = 1
單例模式
public class FileManager {
public static let shared = FileManager()
private init() { }
public func openFile() {
}
}
FileManager.shared.openFile()
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/271198.html
標籤:iOS
