Swift - mutating & inout
Swift - mutating & inout
前言
曾几何时,刚用swift
的时候,我想修改传入的参数,发现不能修改,于是就有了如下代码:
func testSwap(a: Int, b: Int) -> (a: Int, b: Int) {
return (b, a)
}
其实我的意思是:
func testSwap(a: Int, b: Int) {
let temp = a
a = b
b = temp
}
但是这样写会出现编译错误的:
16094054869591.jpg因为在swift
中结构体和枚举是值类型,直接修改其属性是不行的,如下:
struct Point {
var x = 0
var y = 0
func movePoint(x: Int, y: Int) {
self.x += x
}
}
enum Switch {
case top, bottom, left, right
func next() {
switch self {
case .top:
self = .bottom
case .bottom:
self = .left
case .left:
self = .right
case .right:
self = .top
}
}
}
16094069507649.jpg
那么该如何解决上述问题呢?
1. mutating
1.1 mutating 使用
默认情况下,实例方法中是不可以修改值类型的属性的,使用mutating
后可修改属性的值,上面编译报错的解决方法如下:
struct Point {
var x = 0
var y = 0
mutating func movePoint(x: Int, y: Int) {
self.x += x
}
}
enum Switch {
case top, bottom, left, right
mutating func next() {
switch self {
case .top:
self = .bottom
case .bottom:
self = .left
case .left:
self = .right
case .right:
self = .top
}
}
}
加上mutating
后则不会存在编译报错的问题
如果使用mutating
修饰,类中的方法是会编译报错的,因为类是引用类型,在其方法中默认情况下就可以修改属性值,不存在以上问题。
1.2 mutating 实现
- 那么为什么使用
mutating
就可以修改值类型中的属性了呢? - 使用前后的差别是什么呢?
-
mutating
的底层原理是什么呢?
使用前:
示例代码:
struct Point {
var x = 0
var y = 0
func movePoint(x: Int, y: Int) {
print(x)
}
}
sil代码:
16094086930611.jpg
我们可以通过sil
代码看到,在不使用mutating
修饰前,方法中的self
是let
不可修改的
那么使用mutating
修饰是什么样子的呢?下面我们就来看看
使用后:
示例代码:
struct Point {
var x = 0
var y = 0
mutating func movePoint(x: Int, y: Int) {
self.x += x
}
}
sil代码:
16097404014498.jpg
在使用mutating
修饰后,我们在sil代码中可以看到我们的Point
结构体使用了inout
修饰,并且在方法内部的self
也变成可变类型的了。
所以在添加mutating
修饰后:
- 我们的值类型前面加上了
inout
修饰 - 使用前后方法内的
self
分别是let
和var
- 所以
mutating
的本质就是inout
2. inout
通过上面对mutating
的探索我们知道它的本质就是inout
,那么inout
是怎么实现的呢?下面我们就一起来研究一下。
inout
是 Swift
中的关键字,可以放置于参数类型前,冒号之后。使用 inout
之后,函数体内部可以直接更改参数值,而且改变会保留。
我们知道Swift
中有值类型和引用类型,为了区分开来,我们先准备一些代码:
为了方便打印,PointClass
遵守了CustomStringConvertible
协议
struct PointStruct {
var x = 0.0
var y = 0.0
}
class PointClass: CustomStringConvertible {
var x = 0.0
var y = 0.0
var description: String {
return "PointClass(x: \(x), y: \(y))"
}
}
2.1 参数
为了更好的说明,下面我们先来说说参数:
在Swift
中,方法中的参数默认是常量类型,也就是说在函数内只能访问参数,不能修改参数值:
- 对于值类型的参数,不能修改其值
- 对于引用类型的参数,我们不能修改其指向的内存地址,但是其内部的可变的变量时可以修改的
如果我们要改变参数的值或引用,我们可以采用笨方法,就是在函数体内声明一个同类型的同名变量,然后操作这个新的变量,修改其值或引用。那么这样做会改变函数参数的生命周期吗?或者说函数参数的作用域会改变吗,下面我们来测试一下,定义一函数,交换改点的坐标。
值类型:
func swap(ps: PointStruct) -> PointStruct {
var ps = ps
withUnsafePointer(to: &ps) { print("地址1: \($0)") }
let temp = ps.x
ps.x = ps.y
ps.y = temp
return ps
}
var ps1 = PointStruct(x: 10, y: 20)
print(ps1)
withUnsafePointer(to: &ps1) { print("地址2: \($0)") }
print(swap(ps: ps1))
print(ps1)
withUnsafePointer(to: &ps1) { print("地址3: \($0)") }
打印结果:
16097456030165.jpg根据打印结果我们可以知道,对于值类型的参数:
- 函数调用前后,外界变量的和其值并没有因为内部对参数的修改而改变
- 函数体内的参数的内存地址和外界是不一样的,一个值栈区,一个是全局区
- 按照这种方法,相当于在函数体内深拷贝了一份参数,作用域仅在函数体内
引用类型:
func swap(pc: PointClass) -> PointClass {
withUnsafePointer(to: pc) { print("参数地址: \($0)") }
print("地址1: \(Unmanaged.passUnretained(pc).toOpaque())")
let temp = pc.x
pc.x = pc.y
pc.y = temp
return pc
}
let pc1 = PointClass()
withUnsafePointer(to: pc1) { print("外部变量地址: \($0)") }
pc1.x = 10
pc1.y = 20
print(pc1)
print("地址2: \(Unmanaged.passUnretained(pc1).toOpaque())")
print(swap(pc: pc1))
print(pc1)
print("地址3: \(Unmanaged.passUnretained(pc1).toOpaque())")
打印结果:
16097487777079.jpg根据打印结果我们可以知道,对于引用类型的参数:
- 函数内外的指针指向是一致的
- 作为参数时时创建了一个新的常量,指向源变量的引用
- 如果在函数体内修改其内部变量的值,外部也会受到影响
2.2 使用 inout
对于上面提到的,我们使用inout
来实现:
值类型:
func swap(ps: inout PointStruct) -> PointStruct {
withUnsafePointer(to: &ps) { print("地址1: \($0)") }
let temp = ps.x
ps.x = ps.y
ps.y = temp
return ps
}
var ps1 = PointStruct(x: 10, y: 20)
print(ps1)
withUnsafePointer(to: &ps1) { print("地址2: \($0)") }
print(swap(ps: &ps1))
print(ps1)
withUnsafePointer(to: &ps1) { print("地址3: \($0)") }
打印结果:
16097494590963.jpg根据打印结果我们可以知道,对于使用inout
修饰的值类型的参数:
- 外界和参数的地址保值一致
- 在函数内对值类型的修改得到了保留
引用类型:
func swap(pc: inout PointClass) -> PointClass {
withUnsafePointer(to: pc) { print("参数地址: \($0)") }
print("地址1: \(Unmanaged.passUnretained(pc).toOpaque())")
let temp = pc.x
pc.x = pc.y
pc.y = temp
return pc
}
var pc1 = PointClass()
withUnsafePointer(to: pc1) { print("外部变量地址: \($0)") }
pc1.x = 10
pc1.y = 20
print(pc1)
print("地址2: \(Unmanaged.passUnretained(pc1).toOpaque())")
print(swap(pc: &pc1))
print(pc1)
print("地址3: \(Unmanaged.passUnretained(pc1).toOpaque())")
打印结果:
16097514968779.jpg根据打印结果我们可以知道,对于使用inout
修饰的引用类型的参数:
- 参数的地址和外部变量的地址是一致的
- 它们指向的内存空间的地址也是一样的
- 对于修改,当然也是会保持一致的
- 其实对于引用类型的参数,使用
inout
的意义不是很大
2.3 注意事项
通过上面的一些解释,我们可以知道使用inout
关键字可以在函数体内修改参数,但其中也有一些注意事项:
- 使用
inout
关键字的函数,在调用时需要在该参数前加上&
符号 - 使用
inout
的参数在传入时必须为变量,不能为常量 - 使用
inout
的参数不能有默认值,不能为可变参数 - 使用
inout
的参数不等同与函数返回值,是一种使参数的作用域超出函数体的方式 - 多个使用
inout
的参数不能同时传入一个变量,因为拷入拷出的顺序不定,那么最终值也不能确定
2.4 inout 的原理
我们使用一段简单的代码,编译成sil
代码,看看其底层实现是什么样子的:
func testSwap(a: inout Int, b: inout Int) {
let temp = a
a = b
b = temp
}
sil代码:
// testSwap(a:b:)
sil hidden @main.testSwap(a: inout Swift.Int, b: inout Swift.Int) -> () : $@convention(thin) (@inout Int, @inout Int) -> () {
// %0 "a" // users: %11, %4, %2
// %1 "b" // users: %14, %8, %3
bb0(%0 : $*Int, %1 : $*Int):
debug_value_addr %0 : $*Int, var, name "a", argno 1 // id: %2
debug_value_addr %1 : $*Int, var, name "b", argno 2 // id: %3
%4 = begin_access [read] [static] %0 : $*Int // users: %5, %6
%5 = load %4 : $*Int // users: %15, %7
end_access %4 : $*Int // id: %6
debug_value %5 : $Int, let, name "temp" // id: %7
%8 = begin_access [read] [static] %1 : $*Int // users: %9, %10
%9 = load %8 : $*Int // user: %12
end_access %8 : $*Int // id: %10
%11 = begin_access [modify] [static] %0 : $*Int // users: %12, %13
store %9 to %11 : $*Int // id: %12
end_access %11 : $*Int // id: %13
%14 = begin_access [modify] [static] %1 : $*Int // users: %15, %16
store %5 to %14 : $*Int // id: %15
end_access %14 : $*Int // id: %16
%17 = tuple () // user: %18
return %17 : $() // id: %18
} // end sil function 'main.testSwap(a: inout Swift.Int, b: inout Swift.Int) -> ()'
通过sil
代码我们可以看到:
- 使用
inout
修饰的参数,在函数体内部都是var
可变类型的 - 在修改的时候,都是修改的其内存中的数据
As an optimization, when the argument is a value stored at a physical address in memory, the same memory location is used both inside and outside the function body. The optimized behavior is known as call by reference; it satisfies all of the requirements of the copy-in copy-out model while removing the overhead of copying. Write your code using the model given by copy-in copy-out, without depending on the call-by-reference optimization, so that it behaves correctly with or without the optimization.
译:作为一种优化,当实参是存储在内存中的物理地址中的值时,函数体内外都使用相同的内存位置。优化的行为称为引用调用;它满足了copy-in copy-out模型的所有要求,同时消除了复制的开销。使用copy-in copy-out给出的模型编写代码,而不依赖引用调用优化,这样无论优化与否,它都能正确地运行。
3. 总结
本文主要讲述了Swift
中的mutating
和inout
-
mutating
主要用于值类型中的方法,修改其属性 -
inout
主要用于想在方法中修改参数的场景