0%

Swift 中的 inout & UnsafeMutablePointer 指针

问题的产生

最近在做一个需求,其中有一个场景是一个对象从外部传入一个默认的 Array,对象根据一些其他的配置自动对 Array 进行一些处理,同时使默认的 Array 也作出增减操作。

因为在ObjCNSMutableArray是引用类型,所以这就很简单了,不需要做什么特殊处理。但是在SwiftArray是值类型(struct),定义如下:

1
2
3
4
public struct Array<Element> : RandomAccessCollection, MutableCollection {
......
......
}

这时候如果使用 ObjC 的思路去做必定出错,因为 Struct 值类型的传递是复制值的操作,而不是传递地址。

解决方法

inout

很简单的第一时间想到的是函数的传递参数使用inout标记(相信很多人也会这么做)就像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

var inoutColors: [UIColor] = [UIColor.red]

class TestInout {
var colors: [UIColor]?

func addElement(colors: inout [UIColor]) {
let element = UIColor.orange
colors.append(element)
self.colors = colors
}
}

let tio = TestInout()
tio.addElement(colors: &inoutColors)

测试一下:

这时候感觉已经完美解决了?

但是此时如果我们再另外一个函数中再去操作对象中的数组colors,并不会引起原数组inoutColors的改变。

输出如下图:

- inout 是在函数参数传递的时候复制了一份值传进来
- 在函数内部做出改变的时候,原数据并不会改变
- 在函数运行的时候会将函数内部改变之后的值复制出去将传入的参数原数据进行替换

inout 参数在函数调用的时候传递的是该值的地址, 但是在函数体内使用的时候, 使用的是对应的真实值, 无法获取传递的地址, 将其赋值给别的变量也是一份对应值类型的拷贝.

在上面的例子中的tio.colors是改变之后的colors的一份复制,直接改变tio.colors并不会引起原数据inoutColors的改变。

可以测试一下,如下图:

可以发现inout的局限性,他只能在inout的函数作用范围内改变传入的参数才可以改变原来的数据;

一旦超出inout的函数的范围,你将无法改变传入的数据;

除非你直接更改原数据并且使用原数据,但是如果你写的是一个框架,并不知道原数据从哪里来到哪里去怎么办?

或许可以取一个折中的办法:在一个函数中对传入的Array做完初始化处理,之后就只进行读(使用)而不再进行写。

但是如果如何彻底的解决这种问题呢?

UnsafeMutablePointer<Pointee>

如何像ObjC中那样在Swift中使用Array

首先重新描述下具体的原因:

  • 主要的原因还是因为 Swift 中的值类型与引用类型在内存方式不同, 不能用 ObjC 中对 NSArray 的看法去看 Swift 中的 Array
  • ObjC 中的 NSMutableArray 是对象,传递参数是传递对应 array 的地址(指针),对指针进行操作,会使所有的使用该内存的数据都发生改变
  • Swift 中的 Array 是值类型,传递的参数是一份拷贝(类似ObjC中的Copy, 但是原理不同),传递后的内存空间与原来数据的内存空间并不是一块内存,之后的改变与原来的数据都没有任何关系。

那么我们可不可以在Swift中传递一个指针过去呢?很多人可能觉得Swift没有指针,毕竟都看不到*了…

但是Swift可以与C的的 API 进行无缝混合,这时候想到了UnsafeMutablePointer<Pointee> (Pointee是泛型),定义如下:

1
2
3
4
public struct UnsafeMutablePointer<Pointee> : Strideable, Hashable {
...
...
}

又是struct?不要怕,这个是存储Pointee对象的地址struct,使用方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var sourceColors: [UIColor] = [UIColor.red]

class TestUnsafeMutablePointer {

var colorsPoint: UnsafeMutablePointer<[UIColor]>?

init(colorsPoint: UnsafeMutablePointer<[UIColor]>?) {
self.colorsPoint = colorsPoint
}

func addElement() {
self.colorsPoint?.pointee.append(UIColor.orange)
}
}

let t = TestUnsafeMutablePointer.init(colorsPoint: &sourceColors)
t.addElement()
t.colorsPoint?.pointee.append(UIColor.brown)

测试一下:

使用引用符号获取对应的变量的地址传给 UnsafeMutablePointer<[UIColor]>类型的变量然后进行使用,pointee
有时候需要转换,&后面如果很长需要加括号,如:

UnsafeMutablePointer<SomeType>(&(someObject.someAttribute))

另外一些多线程操作也可以使用 UnsafeMutablePointer<SomeType> 这种方式会更加安全稳定。

具体的关于 swift 中指针的用法可以参考喵神的这篇文章:Swift 中的指针使用(喵神这篇文章应该是基于 swift 1,后续 swift 版本中有一些变化)