当前位置 博文首页 > 1024星球:Swift系列十 - inout的本质

    1024星球:Swift系列十 - inout的本质

    作者:1024星球 时间:2021-05-27 18:21

    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到外面。

    bk
    下一篇:没有了