美文网首页
Swift方法mutating关键字的本质

Swift方法mutating关键字的本质

作者: chonglingliu | 来源:发表于2022-08-26 12:44 被阅读0次

    Swift结构体或者枚举的方法中,如果方法中需要修改当前结构体或者枚举属性值,则需要再func前面加上mutating关键字,否则编译器会直接报错。

    ✅ 方法中修改属性必须加上mutating

    struct Point {
        var x: Int
        mutating func setX(_ value: Int) {
            self.x = value
        }
    }
    

    ❌ 不加报错

    存储属性修改

    ❌ 不加报错


    计算属性修改

    接下来我们就来看看mutating关键字的底层实现逻辑到底是什么?

    汇编分析

    不加mutating关键字的setX方法:
    struct Point {
        var x: Int
        func setX(_ value: Int) {
            let _ = self.x
        }
    }
    

    测试代码如下:

    <!-- 代码 -->
    func test() {
        var p = Point(x: 1)
        p.setX(2)
    }
    
    <!-- 汇编 -->
    JJSwift`test():
        0x100003ed8 <+0>:  sub    sp, sp, #0x20            // 压栈32个字节
        0x100003edc <+4>:  stp    x29, x30, [sp, #0x10]
        0x100003ee0 <+8>:  add    x29, sp, #0x10          
        0x100003ee4 <+12>: str    xzr, [sp, #0x8]          // sp+0x8的内存地址清零 
        0x100003ee8 <+16>: mov    w8, #0x1                 
        0x100003eec <+20>: mov    x0, x8                   // 将1赋值给x0寄存器
    ->  0x100003ef0 <+24>: bl     0x100003ed4              // 结构体初始化
        0x100003ef4 <+28>: mov    x1, x0                   // 将1赋值给x1寄存器作为参数          
        0x100003ef8 <+32>: str    x1, [sp, #0x8]           // 将1赋值给p对象
        0x100003efc <+36>: mov    w8, #0x2                 
        0x100003f00 <+40>: mov    x0, x8                   // 将2存储在x0寄存器上,作为参数
        0x100003f04 <+44>: bl     0x100003eb8              // 调用JJSwift.Point.setX(Swift.Int)方法
        0x100003f08 <+48>: ldp    x29, x30, [sp, #0x10]
        0x100003f0c <+52>: add    sp, sp, #0x20            // 出栈
        0x100003f10 <+56>: ret                             // 返回    
    

    Point.setX(Swift.Int)有两个参数,参数1(x0寄存器)2参数2(x1寄存器):p对象的值1
    test()方法的函数栈占用32个字节

    mutating关键字的setX方法:
    struct Point {
        var x: Int
        mutating func setX(_ value: Int) {
            let _ = self.x
        }
    }
    

    测试代码如下:

    <!-- 代码 -->
    func test() {
        var p = Point(x: 1)
        p.setX(2)
    }
    
    <!-- 汇编 -->
    JJSwift`test():
        0x100003ecc <+0>:  sub    sp, sp, #0x30             // 压栈48个字节
        0x100003ed0 <+4>:  stp    x20, x19, [sp, #0x10]
        0x100003ed4 <+8>:  stp    x29, x30, [sp, #0x20]
        0x100003ed8 <+12>: add    x29, sp, #0x20            
        0x100003edc <+16>: add    x20, sp, #0x8             // 保存sp+0x8地址到x20寄存器上
        0x100003ee0 <+20>: str    xzr, [sp, #0x8]           // sp+0x8地址清零
        0x100003ee4 <+24>: mov    w8, #0x1              
        0x100003ee8 <+28>: mov    x0, x8                    // 1放在x0寄存器上
    ->  0x100003eec <+32>: bl     0x100003ec8               // 结构体初始化
        0x100003ef0 <+36>: str    x0, [sp, #0x8]            // 将p对象放在sp+0x8地址
        0x100003ef4 <+40>: mov    w8, #0x2
        0x100003ef8 <+44>: mov    x0, x8                    // 2 放在x0寄存器上 作为参数
        0x100003efc <+48>: bl     0x100003eac               // 调用Point.setX(Swift.Int) -> ()
        0x100003f00 <+52>: ldp    x29, x30, [sp, #0x20]
        0x100003f04 <+56>: ldp    x20, x19, [sp, #0x10]
        0x100003f08 <+60>: add    sp, sp, #0x30             // 出栈
        0x100003f0c <+64>: ret                              // 返回    
    

    Point.setX(Swift.Int)有两个参数,参数1(x0寄存器)2参数2(x20寄存器):p对象的存储地址sp+0x8
    test()方法的函数栈占用48个字节

    setX方法修改属性值
    struct Point {
        var x: Int
        mutating func setX(_ value: Int) {
            self.x = value
        }
    }
    
    func test() {
        var p = Point(x: 1)
        p.setX(2)
    }
    

    我们进入setX看看实现逻辑:

    <!-- 代码 -->
    p.setX(2)
    
    
    <!-- 汇编 -->
    JJSwift`Point.setX(_:):
    ->  0x100003ea8 <+0>:  sub    sp, sp, #0x10            
        0x100003eac <+4>:  str    xzr, [sp, #0x8]
        0x100003eb0 <+8>:  str    xzr, [sp]       
        0x100003eb4 <+12>: str    x0, [sp, #0x8]  
        0x100003eb8 <+16>: str    x20, [sp]        
        0x100003ebc <+20>: str    x0, [x20]       // 内存地址直接修改为新值
        0x100003ec0 <+24>: add    sp, sp, #0x10           
        0x100003ec4 <+28>: ret    
    

    setX中会对内存地址(x20寄存器中的值)直接修改成新值(x0寄存器中的值),也就是直接在传入的内存地址上直接修改

    结论
    • 普通函数传值参数是值传递,加mutating关键字后参数会变成地址传递;

    SIL分析

    函数的参数传递的是地址,这是不是很容易让人联想到mutating关键字是不是就是利用的inout关键字呢?

    我们就利用中间代码来看下:

    不加mutating关键字的setX方法:
    struct Point {
        var x: Int
        func setX(_ value: Int) {
            let _ = self.x
        }
    }
    
    // Point.setX(_:)
    sil hidden @$s4main5PointV4setXyySiF : $@convention(method) (Int, Point) -> () {
    // %0 "value"                                     // user: %2
    // %1 "self"                                      // users: %4, %3
    bb0(%0 : $Int, %1 : $Point):
      %4 = struct_extract %1 : $Point, #Point.x       // 获取point的值
      %5 = tuple ()                                   // user: %6
      return %5 : $()                                 // id: %6
    }
    
    mutating关键字的setX方法:
    struct Point {
        var x: Int
        func setX(_ value: Int) {
            let _ = self.x
        }
    }
    
    // Point.setX(_:)
    sil hidden @$s4main5PointV4setXyySiF : $@convention(method) (Int, @inout Point) -> () {
    // %0 "value"                                     // user: %2
    // %1 "self"                                      // users: %4, %3
    bb0(%0 : $Int, %1 : $*Point):
      %4 = begin_access [read] [static] %1 : $*Point  // 获取point内存地址
      %5 = struct_element_addr %4 : $*Point, #Point.x
      end_access %4 : $*Point                         // id: %6
      %7 = tuple ()                                   // user: %8
      return %7 : $()                                 // id: %8
    }
    
    结论
    • mutating关键字后,第二个参数确实变成了@inout参数
    • @inout修饰的参数是地址传递,所以符合汇编结果。

    总结

    mutating关键字本质是包装了inout关键字,加上mutating关键字后参数值会变成地址传递。
    类对象是指针,传递的本身就是地址值,所以 mutating关键字对类是透明的,加不加效果都一样。

    相关文章

      网友评论

          本文标题:Swift方法mutating关键字的本质

          本文链接:https://www.haomeiwen.com/subject/ttvtnrtx.html