swift的inout的用法
基础用法、底层原理、高级特性和注意事项四个方面详细讲解。
1. 基础概念:为什么要用 inout?
在 Swift 中,函数的参数默认是常量(Constant/let) 。这意味着你不能在函数内部修改参数的值。
错误示例:
func doubleValue(value: Int) {
value *= 2 // ❌ 报错:Left side of mutating operator isn't mutable: 'value' is a 'let' constant
}
如果你希望函数能修改外部传进来的变量,就需要使用 inout。
正确示例:
func doubleValue(value: inout Int) {
value *= 2
}
var myNumber = 10
// 调用时必须在变量前加 '&' 符号,显式表明这个值会被修改
doubleValue(value: &myNumber)
print(myNumber) // 输出:20
2. 核心原理:输入输出模型 (Copy-In Copy-Out)
这是面试或深入理解时最重要的部分。虽然 inout 看起来像“引用传递”,但 Swift 官方将其描述为 Copy-In Copy-Out(输入复制,输出复制) ,也就是“值结果模式(Call by Value Result)”。
完整过程如下:
- Copy In(输入复制): 当函数被调用时,参数的值被复制一份传入函数内部。
- Modification(修改): 函数内部修改的是这个副本。
- Copy Out(输出复制): 当函数返回时,修改后的副本值被**赋值(写回)**给原本的变量。
底层优化:
- 对于物理内存中的变量:编译器通常会进行优化,直接传递内存地址(也就是真正的引用传递),避免不必要的复制开销。
- 对于计算属性(Computed Properties) :必须严格执行 Copy-In Copy-Out 流程(因为计算属性没有物理内存地址,只有 getter 和 setter)。
代码证明(计算属性也能用 inout):
struct Rect {
var width = 0
var height = 0
// 计算属性:面积
var area: Int {
get { width * height }
set {
// 简单逻辑:假设保持 width 不变,调整 height
height = newValue / width
}
}
}
func triple(number: inout Int) {
number *= 3
}
var square = Rect(width: 10, height: 10) // area = 100
// 这里传入的是计算属性 area
// 流程:
// 1. 调用 area 的 get,得到 100,Copy In 给 triple
// 2. triple 将 100 * 3 = 300
// 3. 函数结束,将 300 Copy Out,调用 area 的 set(300)
triple(number: &square.area)
print(square.height) // 输出:30 (因为 300 / 10 = 30)
3. inout 的常见应用场景
A. 交换值 (Standard Swap)
Swift 标准库的 swap 就是用 inout 实现的。
func mySwap<T>(_ a: inout T, _ b: inout T) {
let temp = a
a = b
b = temp
}
var x = 1
var y = 2
mySwap(&x, &y)
print("x: (x), y: (y)") // x: 2, y: 1
B. 修改复杂的结构体 (Mutating Structs)
当结构体嵌套很深时,使用 inout 可以避免冗长的赋值代码。
struct Color {
var r: Int, g: Int, b: Int
}
struct Settings {
var themeColor: Color
}
var appSettings = Settings(themeColor: Color(r: 0, g: 0, b: 0))
// 能够直接修改嵌套深处的属性
func updateBlueComponent(color: inout Color) {
color.b = 255
}
// 传入路径
updateBlueComponent(color: &appSettings.themeColor)
print(appSettings.themeColor.b) // 255
4. 关键规则与内存安全 (Memory Safety)
这是 Swift 相比 C++ 指针更先进的地方。Swift 编译器会强制执行独占访问权限(Law of Exclusivity) ,防止内存冲突。
规则 1:同一个变量不能同时作为两个 inout 参数传递
如果两个 inout 参数指向同一个变量,会发生“别名(Aliasing)”问题,导致行为不可预测。
var step = 1
func increment(_ number: inout Int, by amount: inout Int) {
number += amount
}
// ❌ 运行时崩溃或编译错误:Simultaneous accesses to 0x...
// increment(&step, by: &step)
规则 2:不能将 let 常量或字面量作为 inout 参数
因为它们本质上不可写。
Swift
func change(val: inout Int) {}
// change(val: &5) // ❌ 错误:字面量不可变
let num = 10
// change(val: &num) // ❌ 错误:常量不可变
规则 3:inout 参数在闭包中的捕获(Capture)
inout 参数在逃逸闭包(Escaping Closure)中是不能被捕获的,因为逃逸闭包可能在函数返回后才执行,而那时 inout 的生命周期(Copy-In Copy-Out 过程)已经结束了。
func performAsync(action: @escaping () -> Void) {
// 异步执行...
}
func badFunction(x: inout Int) {
// ❌ 错误:Escaping closure captures 'inout' parameter 'x'
/*
performAsync {
x += 1
}
*/
}
解决办法: 使用非逃逸闭包,或者显式地捕获变量的副本(如果逻辑允许)。
5. inout vs 类 (Reference Types)
这是一个常见的误区: “类本来就是引用类型,还需要 inout 吗?”
-
不需要
inout: 如果你只想修改类实例内部的属性。 -
需要
inout: 如果你想替换掉整个类实例本身(即改变指针的指向)。
代码对比:
class Hero {
var name: String
init(name: String) { self.name = name }
}
// 情况 1:修改内部属性(不需要 inout)
func renameHero(hero: Hero) {
hero.name = "Batman" // 合法,因为 hero 引用本身没变,变的是堆内存里的数据
}
var h1 = Hero(name: "Superman")
renameHero(hero: h1)
print(h1.name) // Batman
// 情况 2:替换整个实例(需要 inout)
func switchHero(hero: inout Hero) {
hero = Hero(name: "Iron Man") // 将外部变量指向全新的内存地址
}
var h2 = Hero(name: "Spiderman")
switchHero(hero: &h2)
print(h2.name) // Iron Man
总结
-
语法: 定义用
inout,调用用&。 - 本质: Copy-In Copy-Out(值结果模式),但在物理内存操作上通常优化为引用传递。
- 使用场景: 需要在函数内部修改外部值类型(Struct/Enum)状态,或交换数据。
- 限制: 遵守独占访问原则(Exclusivity),不可在逃逸闭包中捕获。