乐趣区

关于ios:转Swift-中的指针使用

原文作者:王巍 (onevcat)
原文链接:https://onevcat.com/2015/01/s…


Apple 冀望在 Swift 中指针可能尽量减少退场几率,因而在 Swift 中指针被映射为了一个泛型类型,并且还比拟形象。这在肯定水平上造成了在 Swift 中指针应用的艰难,特地是对那些并不相熟指针,也没有多少指针操作教训的开发者 (包含我本人也是) 来说,在 Swift 中应用指针的确是一个挑战。在这篇文章里,我心愿能从最根本的应用开始,总结一下在 Swift 中应用指针的一些常见形式和场景。这篇文章假设你至多晓得指针是什么,如果对指针自身的概念不太分明的话,能够先看看这篇五分钟 C 指针教程 (或者它的中文版本),应该会很有帮忙。


初步

在 Swift 中,指针都应用一个非凡的类型来示意,那就是 UnsafePointer<T>。遵循了 Cocoa 的一贯不可变准则,UnsafePointer<T> 也是不可变的。当然对应地,它还有一个可变变体,UnsafeMutablePointer<T>。绝大部分工夫里,C 中的指针都会被以这两种类型引入到 Swift 中:C 中 const 润饰的指针对应 UnsafePointer (最常见的应该就是 C 字符串的 const char * 了),而其余可变的指针则对应 UnsafeMutablePointer。除此之外,Swift 中存在示意一组间断数据指针的 UnsafeBufferPointer<T>,示意非残缺构造的不通明指针 COpaquePointer 等等。另外你可能曾经留神到了,可能确定指向内容的指针类型都是泛型的 struct,咱们能够通过这个泛型来对指针指向的类型进行束缚以提供肯定安全性。

对于一个 UnsafePointer<T> 类型,咱们能够通过 pointee 属性对其进行取值,如果这个指针是可变的 UnsafeMutablePointer<T> 类型,咱们还能够通过 pointee 对它进行赋值。比方咱们想要写一个利用指针间接操作内存的计数器的话,能够这么做:

func incrementor(ptr: UnsafeMutablePointer<Int>) {ptr.pointee += 1}

var a = 10
incrementor(&a)

a  // 11

这里和 C 的指针应用相似,咱们通过在变量名后面加上 & 符号就能够将指向这个变量的指针传递到承受指针作为参数的办法中去。在下面的 incrementor 中咱们通过间接操作 pointee 属性扭转了指针指向的内容。

与这种做法相似的是应用 Swift 的 inout 关键字。咱们在将变量传入 inout 参数的函数时,同样也应用 & 符号示意地址。不过区别是在函数体外部咱们不须要解决指针类型,而是能够对参数间接进行操作。

func incrementor1(inout num: Int) {num += 1}

var b = 10
incrementor1(&b)

b  // 11

尽管 & 在参数传递时示意的意义和 C 中一样,是某个“变量的地址”,然而在 Swift 中咱们没有方法间接通过这个符号获取一个 UnsafePointer 的实例。须要留神这一点和 C 有所不同:

// 无奈编译
let a = 100
let b = &a

指针初始化和内存治理
在 Swift 中不能间接取到现有对象的地址,咱们还是能够创立新的 UnsafeMutablePointer 对象。与 Swift 中其余对象的主动内存治理不同,对于指针的治理,是须要咱们手动进行内存的申请和开释的。一个 UnsafeMutablePointer 的内存有三种可能状态:

  • 内存没有被调配,这意味着这是一个 null 指针,或者是之前曾经开释过
  • 内存进行了调配,然而值还没有被初始化
  • 内存进行了调配,并且值曾经被初始化

其中只有第三种状态下的指针是能够保障失常应用的。UnsafeMutablePointer 的初始化办法 (init) 实现的都是从其余类型转换到 UnsafeMutablePointer 的工作。咱们如果想要新建一个指针,须要做的是应用 allocate(capacity:) 这个类办法。该办法依据参数 capacity: Int 向零碎申请 capacity 个数的对应泛型类型的内存。上面的代码申请了一个 Int 大小的内存,并返回指向这块内存的指针:

var intPtr = UnsafeMutablePointer<Int>.allocate(capacity: 1)
// "UnsafeMutablePointer(0x7FD3A8E00060)"

接下来应该做的是对这个指针的内容进行初始化,咱们能够应用 initialize(to:) 办法来实现初始化:.

intPtr.initialize(to: 10)
// intPtr.pointee 为 10

在实现初始化后,咱们就能够通过 pointee 来操作指针指向的内存值了。

在应用之后,咱们最好尽快开释指针指向的内容和指针自身。与 initialize: 配对应用的 deinitialize: 用来销毁指针指向的对象,而与 allocate(capacity:) 对应的 deallocate(capacity:) 用来开释之前申请的内存。它们都应该被配对应用:

intPtr.deinitialize()
intPtr.deallocate(capacity: 1)
intPtr = nil

留神其实在这里对于 Int 这样的在 C 中映射为 int 的“平庸值”来说,deinitialize 并不是必要的,因为这些值被调配在常量段上。然而对于像类的对象或者构造体实例来说,如果不保障初始化和捣毁配对的话,是会呈现内存泄露的。所以没有非凡思考的话,不管内存中到底是什么,保障 initialize: 和 deinitialize 配对会是一个好习惯。

指向数组的指针

在 Swift 中将一个数组作为参数传递到 C API 时,Swift 曾经帮忙咱们实现了转换,这在 Apple 的官网博客中有个很好的例子:

import Accelerate

let a: [Float] = [1, 2, 3, 4]
let b: [Float] = [0.5, 0.25, 0.125, 0.0625]
var result: [Float] = [0, 0, 0, 0]

vDSP_vadd(a, 1, b, 1, &result, 1, 4)

// result now contains [1.5, 2.25, 3.125, 4.0625]

对于个别的承受 const 数组的 C API,其要求的类型为 UnsafePointer,而非 const 的数组则对应 UnsafeMutablePointer。应用时,对于 const 的参数,咱们间接将 Swift 数组传入 (上例中的 ab);而对于可变的数组,在后面加上 & 后传入即可 (上例中的 result)。

对于传参,Swift 进行了简化,应用起来十分不便。然而如果咱们想要应用指针来像之前用 pointee 的形式间接操作数组的话,就须要借助一个非凡的类型:UnsafeMutableBufferPointer。Buffer Pointer 是一段间断的内存的指针,通常用来表白像是数组或者字典这样的汇合类型。

var array = [1, 2, 3, 4, 5]
var arrayPtr = UnsafeMutableBufferPointer<Int>(start: &array, count: array.count)     // baseAddress 是第一个元素的指针,类型为 UnsafeMutablePointer<Int>     if let basePtr = arrayPtr.baseAddress {print(basePtr.pointee)  // 1
    basePtr.pointee = 10
    print(basePtr.pointee) // 10
    
    // 下一个元素
    let nextPtr = basePtr.successor()
    print(nextPtr.pointee) // 2
}

指针操作和转换
withUnsafePointer / withUnsafeMutablePointer
下面咱们说过,在 Swift 中不能像 C 里那样应用 & 符号间接获取地址来进行操作。如果咱们想对某个变量进行指针操作,咱们能够借助 withUnsafePointer withUnsafeMutablePointer 这两个辅助办法。这两个办法承受两个参数,第一个是 inout 的任意类型,第二个是一个闭包。Swift 会将第一个输出转换为指针,而后将这个转换后的 Unsafe 的指针作为参数,去调用闭包。withUnsafePointerwithUnsafeMutablePointer 的差异是前者转化后的指针不可变,后者转化后的指针可变。应用起来大略是这个样子:

var test = 10
test = withUnsafeMutablePointer(to: &test, { (ptr: UnsafeMutablePointer<Int>) -> Int in
    ptr.pointee += 1
    return ptr.pointee
})

test // 11

这里其实咱们做了和文章一开始的 incrementor 雷同的事件,区别在于不须要通过办法的调用来将值转换为指针。这么做的益处对于那些只会执行一次的指针操作来说是不言而喻的,能够将“咱们就是想对这个指针做点事儿”这个用意表白得更加清晰明确。

unsafeBitCast
unsafeBitCast 是十分危险的操作,它会将一个指针指向的内存强制按位转换为指标的类型。因为这种转换是在 Swift 的类型治理之外进行的,因而编译器无奈确保失去的类型是否的确正确,你必须明确地晓得你在做什么。比方:

let arr = NSArray(object: "meow")
let str = unsafeBitCast(CFArrayGetValueAtIndex(arr, 0), to: CFString.self)
str //“meow”

因为 NSArray 是能够寄存任意 NSObject 对象的,当咱们在应用 CFArrayGetValueAtIndex 从中取值的时候,失去的后果将是一个 UnsafePointer<Void>。因为咱们很明确其中寄存的是 String 对象,因而能够间接将其强制转换为 CFString

对于 unsafeBitCast 一种更常见的应用场景是不同类型的指针之间进行转换。因为指针自身所占用的的大小是肯定的,所以指针的类型进行转换是不会出什么致命问题的。这在与一些 C API 合作时会很常见。比方有很多 C API 要求的输出是 void *,对应到 Swift 中为 UnsafePointer<Void>。咱们能够通过上面这样的形式将任意指针转换为 UnsafePointer。

var count = 100
let voidPtr = withUnsafePointer(to: &count, { (a: UnsafePointer<Int>) -> UnsafePointer<Void> in
    return unsafeBitCast(a, to: UnsafePointer<Void>.self)
})
// voidPtr 是 UnsafePointer<Void>。相当于 C 中的 void *

// 转换回 UnsafePointer<Int>
let intPtr = unsafeBitCast(voidPtr, to: UnsafePointer<Int>.self)
intPtr.pointee //100

总结
Swift 从设计上来说就是以平安作为重要准则的,尽管可能有些啰嗦,然而还是要重申在 Swift 中间接应用和操作指针应该作为最初的伎俩,它们始终是无奈确保安全的。从传统的 C 代码和与之无缝配合的 Objective-C 代码迁徙到 Swift 并不是一件小工程,咱们的代码库必定会时不时呈现一些和 C 合作的中央。咱们当然能够抉择应用 Swift 重写局部古老代码,然而对于像是平安或者性能至关重要的局部,咱们可能除了持续应用 C API 以外别无选择。如果咱们想要持续应用那些 API 的话,理解一些根本的 Swift 指针操作和应用的常识会很有帮忙。

对于新的代码,尽量避免应用 Unsafe 结尾的类型,意味着能够防止很多不必要的麻烦。Swift 给开发者带来的最大益处是能够让咱们用更加先进的编程思维,进行更快和更专一的开发。只有在尊重这种思维的前提下,咱们能力更好地享受这门新语言带来的种种劣势。显然,这种思维是不包含到处应用 UnsafePointer 的 :)

退出移动版