Swift方法mutating关键字的本质

2022-08-26  本文已影响0人  chonglingliu

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寄存器中的值),也就是直接在传入的内存地址上直接修改

结论

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关键字,加上mutating关键字后参数值会变成地址传递。
类对象是指针,传递的本身就是地址值,所以 mutating关键字对类是透明的,加不加效果都一样。

上一篇下一篇

猜你喜欢

热点阅读