Swift 中的指针

2,097 阅读5分钟
题图来自互联网

指针是 C / C++ 中一个很重要的概念,是这些相对低级的语言的灵魂,然而 Swift 似乎天生对指针十分不友好,繁琐的用法让很多初学者一上来十分摸不着头脑。本文就简单谈谈 Swift 中指针的一些用法。

为什么要用指针?

其实使用 Swift 来做 iOS 开发时不会经常与指针打交道,FoundationUIKit 等常用的 frameworks 都很好地 bridge 过,很多符号也桥接符合了 Swift 的命名规范。然而很多底层的 frameworks 和一些 libc 里的函数依然在大规模地使用指针。Core FoundationSecurityCore TextCore Audio、etc...这些框架在很多大型专业应用中使用广泛,然而它们就是指针的重灾地。

基本用法

首先我们来看看怎么获取到一个变量的指针,我们预先定义了下面这个结构体:

struct Foo {
    let a: Int
    let b: Bool
}

(P.S. 辣鸡知乎不支持 Swift 语法高亮,语言就选个JavaScript 吧)

最简单拿到这个结构体实例指针的方法就是将其当作函数参数,就像这样:

func passPointer(_ pointer: UnsafePointer<Foo>) {
    print(pointer.pointee)
}

var foo = Foo(a: 43690, b: true)
passPointer(&foo)

这里注意一个与 inout 关键字的区别,如果函数参数的类型前用 inout 修饰过,则表示函数体内可以修改这个参数的实参,而参数本身并不是指针类型的。这有点类似 C++ 中的引用类型,传参时依然使用 & 取引用。

其次,Swift 中的指针是有 Mutability 的区别的,通常我们用指针的话,最好用 var 声明变量,而不要用 let,避免不必要的麻烦。

除了使用函数传参可以拿到一个变量的指针以外,也可以用 inline 的方式获取到:

withUnsafePointer(to: &foo) { pointer -> Void in
    print(pointer.pointee)
}

其实也是一个标准库里的函数了。


下面我们来看看这个指针对象可以做什么:

1. 访问、修改指针所指的内容:

使用 pointee 属性,这个属性可读可写,pointer.pointee = x 就类似 C 中的 *pointer = x

2. 地址计算:

使用 advanced(by:) 函数可以得到一个偏移后的指针对象,这里地址计算的行为与 C 一致,地址偏移是根据指针类型的大小来计算的,而不是根据字节数。

3. 获取 Hash:

这个哈希值不一定是真实的内存地址,但指向相同地址的指针,哈希值一定相同。


类型转换

上面的例子中,我们取得了一个 Foo 对象的指针,如何将其 cast 为其他类型呢?

使用 withMemoryRebound(to:capacity:body:) 函数:

withUnsafePointer(to: &foo) { pointer -> Void in
    pointer.withMemoryRebound(to: Int.self, capacity: 1, { (p2) -> Void in
        print(p2.pointee)
    })
}

此时 p2 就是一个 Int 类型的指针了。这个函数等同于下面这个用法:

withUnsafePointer(to: &foo) { pointer -> Void in
    print(UnsafeRawPointer(pointer).bindMemory(to: Int.self, capacity: 1).pointee)
}

通过这个方法我们就可以获取到这个结构体的每个字节了,方法就是将其 cast 到 UInt8 类型的指针。但是标准库提供了一个更好的方法,那就是使用 withUnsafeBytes(of:body:) 函数,body 中能得到 UnsafeRawBufferPointer 对象,这是个什么鬼?Raw 在 Swift 中代表了 UInt8 类型,也就是为指定类型的指针类型,对其操作视为对字节的操作,Buffer 则提供了一系列操作数组的便利方法,例如你可以获取 count,也能生成迭代器,也支持 map、filter 等方法,犹如操作一个数组一样。

对于 Swift 的数组和序列对象,也可以转换成指针:

[1, 2, 3].withUnsafeBufferPointer { pointer -> Void in
    print(pointer.baseAddress) // 得到 UnsafePointer<Int> 对象
    print(pointer.first) // 得到起始地址指向的 Int 对象
}


Unmanaged 与 Objective-C 对象

由于 Objective-C 对象具有引用计数特性,单纯的指针可能无法满足使用需求,因此 Swift 引入了 Unmanaged 对象来管理引用计数。将一个对象声明为非托管有两个方法:

  • passRetained
  • passUnretained

如果这个非托管对象的使用全程,能够保障被封装对象一直存活,我们就可以使用 passUnretained 方法,对象的生命周期还归编译器管理。如果非托管对象使用周期超过了编译器认为的生命周期,比如超出作用域,编译器自动插入 release 的 ARC 语义,那么这个非托管对象就是一个野指针了,此时我们必须手动 retain 这个对象,也就是使用 passRetained 方法。一旦你手动 retain 了一个对象,就不要忘记 release 掉它,方法就是调用非托管对象的 release 方法,或者用 takeRetainedValue 取出封装的对象,并将其管理权交回 ARC。但注意,一定不要对一个用 passUnretained 构造的非托管对象调用 release 或者 takeRetainedValue,这会导致原来的对象被 release 掉,从而引发异常。

我们在自己的开发过程中基本也不会用到 Unmanaged 来管理对象生命周期和引用计数,但是如果要与 MRC 的库交互时,还是需要它的,Unmanaged 对象提供了转换为指针的方法。

Wrap Up

本文不够全面地简述了一下 Swift 中指针的一些用法,大家在开发的时候当然能避免使用指针就避免使用,Swift 之所以如此设计,还是为了类型安全和内存安全。不直接与指针打交道能明显降低程序的复杂度,这也是现代语言需要解决的。好在 Swift 没有像 Rust 那样,为了安全性又引入了一堆新的概念。

如果文章有错误,欢迎大家在评论中提出。

就是这样。