inout 的实现和避坑

1,955 阅读3分钟

这里每天分享一个 iOS 的新知识,快来关注我吧

前言

上次介绍了 mutating 关键字的用法和实现原理,其中就提到他的实现原理是在方法中传入了一个被标记了 inoutself

今天就来讲讲 inout 这个关键字的实现原理,以及在实际应用中遇到的坑。

inout 的使用

默认情况下,传递到方法中的所有参数都是常量 (let),因此无法更改它们。如果需要在函数内部更改参数,可以传入一个或多个参数标记为 inout,这意味着它们可以在函数内部更改,并且这些更改会反映在函数外部的原始值中。

举个例子,如果你想将一个数字乘以 2,写一个函数,传入这个数字,在函数内部直接修改这个数字,而不是返回新值,可以编写如下函数:

func doubleInPlace(numberinout Int) {
    number *= 2
}

要使用这个函数,你首先需要创建一个可变整数,不能将常量整数与 inout 参数一起使用,传入的时候还需要在其名称之前使用 & 符号:

var myNum = 10 
doubleInPlace(number: &myNum)
print(myNum) // 打印:20

注意:inout 参数不能有默认值,而且可变参数不能标记为 inout

inout 的实现原理

首先,当函数被调用时,inout 参数的值会被复制一份(调用 get 方法),在函数体内修改这个参数,其实是副本被修改了。在函数最后结束的时候,副本的值将赋值给原始参数(调用 set 方法)。

这是苹果文档中的描述,这种行为被称为 copy-in,copy-out,当计算属性具有观察者的属性作为输入输出参数传递时,其 gettersetter 方法都会被调用。

苹果针对这种行为还做了一些优化,当参数是存储在内存中物理地址的值时,函数体内部和外部都使用相同的内存地址,这种优化称为引用调用,这么优化的好处是避免了一次复制的开销

可能存在的坑

上边介绍原理的时候说过,copy-in,copy-out 的过程会调用属性的 getset 方法,这里可能存在一个坑,因为即使你在函数中没有改动这个属性,getset 依然会执行一次,我们来验证一下:

struct MyStruct {
    var name: String = "iOS 新知" {
        willSet {
            print("willSet \(newValue)")
        }
        didSet {
            print("didSet \(name)")
        }
    }
}

func changeName(_ nameinout String) {
    // 什么都不做
}

var s = MyStruct()
changeName(&s.name)

首先定义了一个结构体 MyStruct,其中有一个名为 name 的存储属性,对该属性添加了 willSetdidSet 观察器。

然后定义了一个 changeName 函数,参数是 inout 类型的 String,后面创建 MyStruct 的实例 s,并调用 changeName 函数,传入 s.name的引用。

changeName 中对 name 并没有进行修改,但是输出结果是

willSet iOS 新知
didSet iOS 新知

证明 willSetdidSet 被调用了。

这里每天分享一个 iOS 的新知识,快来关注我吧

本文同步自微信公众号 “iOS新知”,每天准时分享一个新知识,这里只是同步,想要及时学到就来关注我吧!