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
关键字对类是透明的,加不加效果都一样。