swift的inout的用法

基础用法底层原理高级特性注意事项四个方面详细讲解。

1. 基础概念:为什么要用 inout?

在 Swift 中,函数的参数默认是常量(Constant/let) 。这意味着你不能在函数内部修改参数的值。

错误示例:

Swift 复制代码
func doubleValue(value: Int) {
    value *= 2 // ❌ 报错:Left side of mutating operator isn't mutable: 'value' is a 'let' constant
}

如果你希望函数能修改外部传进来的变量,就需要使用 inout

正确示例:

Swift 复制代码
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)"。

完整过程如下:

  1. Copy In(输入复制): 当函数被调用时,参数的值被复制一份传入函数内部。
  2. Modification(修改): 函数内部修改的是这个副本
  3. Copy Out(输出复制): 当函数返回时,修改后的副本值被**赋值(写回)**给原本的变量。

底层优化:

  • 对于物理内存中的变量:编译器通常会进行优化,直接传递内存地址(也就是真正的引用传递),避免不必要的复制开销。
  • 对于计算属性(Computed Properties) :必须严格执行 Copy-In Copy-Out 流程(因为计算属性没有物理内存地址,只有 getter 和 setter)。

代码证明(计算属性也能用 inout):

Swift 复制代码
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 实现的。

Swift 复制代码
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 可以避免冗长的赋值代码。

Swift 复制代码
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)"问题,导致行为不可预测。

Swift 复制代码
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

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 过程)已经结束了。

Swift 复制代码
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 如果你想替换掉整个类实例本身(即改变指针的指向)。

代码对比:

Swift 复制代码
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

总结

  1. 语法: 定义用 inout,调用用 &
  2. 本质: Copy-In Copy-Out(值结果模式),但在物理内存操作上通常优化为引用传递。
  3. 使用场景: 需要在函数内部修改外部值类型(Struct/Enum)状态,或交换数据。
  4. 限制: 遵守独占访问原则(Exclusivity),不可在逃逸闭包中捕获。
相关推荐
却尘1 小时前
Next.js 请求最佳实践 - vercel 2026一月发布指南
前端·react.js·next.js
ccnocare1 小时前
浅浅看一下设计模式
前端
Lee川1 小时前
🎬 从标签到屏幕:揭秘现代网页构建与适配之道
前端·面试
Ticnix1 小时前
ECharts初始化、销毁、resize 适配组件封装(含完整封装代码)
前端·echarts
纯爱掌门人2 小时前
终焉轮回里,藏着 AI 与人类的答案
前端·人工智能·aigc
twl2 小时前
OpenClaw 深度技术解析
前端
崔庆才丨静觅2 小时前
比官方便宜一半以上!Grok API 申请及使用
前端
星光不问赶路人2 小时前
vue3使用jsx语法详解
前端·vue.js
天蓝色的鱼鱼2 小时前
shadcn/ui,给你一个真正可控的UI组件库
前端
布列瑟农的星空2 小时前
前端都能看懂的Rust入门教程(三)——控制流语句
前端·后端·rust