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 小时前
JavaScript新手必看系列之预编译
前端·javascript
小哀21 小时前
🌸 入职写了一个月全栈next.js 感想
前端·后端·ai编程
用户6600676685391 小时前
搞懂作用域链与闭包:JS底层逻辑变简单
前端·javascript
yinuo1 小时前
前端跨页面通讯终极指南②:BroadcastChannel 用法全解析
前端
没落英雄1 小时前
简单了解 with
前端·javascript
越努力越幸运5081 小时前
webpack的学习打包工具
前端·学习·webpack
IT古董1 小时前
微前端的新纪元:Vite + Module Federation 最强指南(2025 全面技术解析)
前端
小小弯_Shelby2 小时前
vue项目源码泄露漏洞修复
前端·javascript·vue.js
兔子零10242 小时前
CSS 视口单位进化论:从 100vh 的「骗局」到 dvh 的救赎
前端·css