Swift系列十 - inout的本質


inout是可以用來在函數內部修改外部屬性內存的。

一、inout回顧

示例代碼:

func test(_ num: inout Int) {
    num = 20
}
var a = 10
test(&a)
print(a) // 輸出:20
test(&a)

通過匯編分析,全局變量a的地址0x6c52(%rip)傳遞給了寄存器rdirdi作為參數傳遞給了test函數,所以inout的本質就是引用傳遞(地址傳遞)。

二、inout本質

示例代碼:

struct Shape {
    var width: Int
    var side: Int {
        willSet {
            print("willSet", newValue)
        }
        didSet {
            print("didSet", oldValue, side)
        }
    }
    var girth: Int {
        set {
            print("setGirth")
            width = newValue / side
        }
        get {
            print("getGirth")
            return width * side
        }
    }
    func show() {
        print("width=\(width), side=\(side), girth=\(girth)")
    }
}

func test(_ num: inout Int) {
    print("test")
    num = 20
}
var s = Shape(width: 10, side: 4)

2.1. 存儲屬性

test(&s.width)
s.show()
/*
 輸出:
 test
 getGirth
 width=20, side=4, girth=80
 */

分析:

  • 0x6c9d(%rip)是全局變量s的地址值;
  • s的內存地址和結構體Shape中第一個存儲屬性的地址是相同的(值類型);
  • 相當於把實例s中存儲屬性width的內存地址傳給了test函數;
  • 所以結構體的存儲屬性使用inout的本質和全局/局部變量都一樣。

結論:
由於存儲屬性有自己的內存地址,所以直接把存儲屬性的地址傳遞給需要修改的函數,在函數內部修改存儲屬性的值。

2.2. 計算屬性

test(&s.girth)
s.show()
/*
 輸出:
 getGirth
 test
 setGirth
 getGirth
 width=5, side=4, girth=20
 */

> 思考:上面的代碼中s.girth也是地址傳遞么?答案:不是,因為girth不是存儲屬性,所以不占用結構體的內存,但是使用&s.girth不會報錯,並且正常讀寫值,所以編譯器是允許我們這么做的。那它是如何傳遞修改值的呢?

分析:

  • 執行代碼test(&s.girth)首先調用了girthgetter方法;
  • 然后getter方法會返回一個值,這個值放在臨時空間內(局部變量);
  • 調用test方法時是把getter返回的臨時變量作為參數傳遞的(傳遞的還是地址值),這時候在test方法內部修改的是臨時變量內存的值;
  • 當修改局部變量內存時,會調用girthsetter方法,把局部變量的值作為參數傳遞;
  • 最終的結果就是值被修改了。

結論:
由於計算屬性沒有自己的地址值,所以會調用getter方法獲取一個局部變量,把局部變量的值傳遞給需要修改的函數,在函數內部修改局部變量的值,最后把局部變量的值傳遞給setter方法。

2.3. 屬性觀察器

test(&s.side)
s.show()
/*
 輸出:
 test
 willSet 20
 didSet 4 20
 getGirth
 width=10, side=20, girth=200
 */




分析:

  • 取出0x6cc3(%rip)的前8個字節給rax,而0x6cc3(%rip)的本質就是存儲屬性side(通過匯編注釋可以看出s偏移8個字節,而width占用8個字節,跳過width就是side);
  • rax的值又給了局部變量-0x28(%rbp)
  • 然后把局部變量rdi的值傳遞給了test函數,通過打印發現rdi保存的值就是20;
  • test函數執行完成后,開始執行sidesetter方法,並把之前的局部變量rdi作為參數傳遞過去;
  • willset之前沒有修改rdi,所以rdi保存的還是20,並且作為第一個參數傳遞給了willset
  • 由於willset之后才會真正修改屬性值,並且didset之前已經知道修改過的屬性值,所以真正修改屬性值是在willsetdidset之間;

結論:
修改帶有屬性觀察器的存儲屬性值時,和計算屬性的過程有點類似。先拿到屬性的值給局部變量,然后把局部變量的地址值傳遞給需要修改的函數,函數內部會修改局部變量的值。函數執行完成后把已經修改過的局部變量的值賦值給屬性。賦值時,優先執行屬性的willset方法,willset執行結束后,才會真正修改屬性的值,最后調用didset

小技巧:需要傳遞inout參數的函數,業務邏輯是非常獨立的,目的僅僅是修改傳遞過來的參數值,不會影響計算屬性/存儲屬性(屬性觀察器)的邏輯,所以除了計算屬性可以直接傳地址,其他屬性都需要一個局部變量做一個中轉。

2.4. inout的本質總結

  1. 如果實參有物理內存地址,且沒有設置屬性觀察器

    • 直接將實參的內存地址傳入函數(實參進行引用傳遞)
  2. 如果實參是計算屬性或設置了屬性觀察器,采取Copy In Copy Out的做法:

    • 調用該函數時,先復制實參的值,產生一個副本(局部變量-執行get方法)
    • 將副本的內存地址傳入函數(副本進行引用傳遞),在函數內部可以修改副本的值
    • 函數返回后,再將副本的值覆蓋實參的值(執行set方法)

總結:inout的本質就是引用傳遞(地址傳遞)。

什么是Copy In Copy Out?先Copy到函數里,修改后再Copy到外面。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM