Swift中UnsafePointer和UnsafeMutablePointer详解

Home / iOS MrLee 2016-3-14 7479

在一朋友的博客听看到这篇文章,对Swift中的引用C语言的指针传值有了一个新的认识。主要是:UnsafeMutablePointer UnsafePointer。
如果你建一个Swift工程,当你创建一个C文件时,然后添加一些方法,比如返回char*类型方法(已经桥接完成),然后你在Swift中书写这个C定义的方法时,xcode会自动转换到swift中的类型。char*对应UnsafeMutablePointer,const char*是UnsafePointer。
好了,我开个简单的头。看文章主要内容。
大多数时候,C语言指针有两种方法导入到Swift中:
UnsafePointer
UnsafeMutablePointer

这里的T是C类型的等价的Swift类型。声明为常量的指针被导入为UnsafePointer,非常量的指针则被导入为UnsafeMutablePoinger。
这里有一些示例:
C:
void myFunction(const int *myConstIntPointer);
Swift:
func myFunction(myConstIntPointer: UnsafePointer)
C:
void myOtherFunction(unsigned int *myUnsignedIntPointer);
Swift:
func myOtherFunction(myUnsignedIntPointer: UnsafeMutablePointer)
C:
void iTakeAVoidPointer(void *aVoidPointer);
Swift:
func iTakeAVoidPointer(aVoidPointer: UnsafeMutablePointer)

如果不知道指针的类型,比如一个为前置声明的指针,则使用COpaquePointer。
C:
struct SomeThing;
void iTakeAnOpaquePointer(struct SomeThing *someThing);
Swift:
func iTakeAnOpaquePointer(someThing: COpaquePointer)

 

传递指针到Swift对象

在很多情况下,传递指针到Swift对象和使用inout运算符一样简单,后者类似于C语言中的and运算符。
Swift:
let myInt: = 42
myFunction(&myInt)
var myUnsignedInt: UInt = 7
myOtherFunction(&myUnsignedInt)

 
这里有两个非常重要但容易被忽视的细节。
1. 当使用inout运算符时,使用var声明的变量和使用let声明的常量被分别转换到UnsafePointer和 UnsafeMutablePoinger,如果你不注意原来代码中的类型,就很容易出错。你可以试着向本来是UnsafeMutablePoinger 的地方传递一个UnsafePointer看看,编译器会报错。
2. 这个运算符只在将Swift值和引用作为函数参数传递的上下文时生效,且该函数参数只接受UnsafePointer和UnsafeMutablePoinger两种类型。你不能在其它上下文获得这些指针。比如,下面的代码是无效的,并且会返回编译错误。
Swift:
let x = 42
let y = &x

你可能会不时的需要交互操作一个API。来获取或返回一个空指针以代替显式类型,不幸的是这种做法在C语言里很普遍,导致无法指定一个通用类型。
C:
void takesAnObject(void *theObject);

如果你确定函数需要获取什么类型的参数,你可以使用withUnsafePointer和unsafeBitCast将对象强制转换为空指针。比如,假设takesAnObject需要获取指向int的指针。
var test = 42
withUnsafePointer(&test, { (ptr: UnsafePointer) -> Void in
    var voidPtr: UnsafePointer = unsafeBitCast(ptr, UnsafePointer.self)
    takesAnObject(voidPtr)
})

 
为了转换它,首先我们需要调用withUnsafeMutablePointer,这个通用函数包含两个参数。
第一个参数是T类型的inout运算符,第二个是(UnsafePointer) -> ResultType的闭包。函数通过指向第一个参数的指针来调用闭包,然后然后将其作为闭包唯一的参数传递,最后函数返回闭包的结果。在上面的例子里, 闭包的类型被设置为Void,因此将不返回值。返回值的例子如下:
let ret = withUnsafePointer(&test, { (ptr: UnsafePointer) -> Int32 in
    var voidPtr: UnsafePointer = unsafeBitCast(ptr, UnsafePointer.self)
    return takesAnObjectAndReturnsAnInt(voidPtr)
})
println(ret)

 
注意:你需要自己修改指针,通过withUnsafeMutablePointer变体来完成修改。
为方便起见,Swift也包括传递两个指针的变体:
var x: Int = 7
var y: Double = 4
withUnsafePointers(&x, &y, { (ptr1: UnsafePointer, ptr2: UnsafePointer) -> Void in
    var voidPtr1: UnsafePointer = unsafeBitCast(ptr1, UnsafePointer.self)
    var voidPtr2: UnsafePointer = unsafeBitCast(ptr2, UnsafePointer.self)
    takesTwoPointers(voidPtr1, voidPtr2)
})

 

关于unsafeBitCast

unsafeBitCast 是一个极度危险的操作。文档将其描述为“将某物强制转换为和其他东西相同的长度”。在上面的代码我们能够安全的使用它的原因是,我们只是简单的转换不同类型的指针,并且这些指针的长度都是相同的。这也是我们为什么必须先调用withUnsafePointer来获取UnsafePointer,然后将其转换为UnsafePointer的原因。
在一开始这可能会造成迷惑,特别是当处理与指针相同的类型时,比如Swift里的Int(在目前所有可用的平台,一个指针的长度是一个字符,Int的长度同样也是一个字符)。
比如很容易就会犯下面的错误:
var x: Int = 7
let xPtr = unsafeBitCast(x, UnsafePointer.self)

 
这段代码的意图是获取一个指针并传递给x。它会给人造成误解,尽管编译能通过并且能运行,但会导致一个意外错误。这是因为C API没有获取指针并传递给x,而是接收位于0x7或者其他地方的指针。
因为unsafeBitCast要求类型的长度相等,所以当试图转换Int8或者一个字节的整型时没那么阴险了。
var x: Int8 = 7
let xPtr = unsafeBitCast(x, UnsafePointer.self)

 
这段代码会简单的导致unsafeBitCast抛出异常和程序崩溃。

与C语言中的结构体交互

让 我们用实际的示例来展示这一部分。如果你想检索计算机所运行的系统信息,有一个C API:uname(2)可以达到目的。它接收指针指到一个数据结构,并且用系统信息填充所提供的对象,如OS名称和版本或者硬件识别符。但这里有一个问 题,导入到Swift的结构体是这样:
struct utsname {
    var sysname: (Int8, Int8, ...253 times..., Int8)
    var nodename: (Int8, Int8, ...253 times..., Int8)
    var release: (Int8, Int8, ...253 times..., Int8)
    var version: (Int8, Int8, ...253 times..., Int8)
    var machine: (Int8, Int8, ...253 times..., Int8)
}

Swift将C中的数组字面量作为元组导入,并且默认的初始化程序要求每个字段都有值,所以如果你用Swift通常的做法来做的话,它将会变成:
var name = utsname(sysname: (0, 0, 0, ..., 0), nodename: (0, 0, 0, ..., 0), etc)
utsname(&name)
var machine = name.machine
println(machine)

 
这不是一个好方法。并且还存在另一个问题。因为utsname里的machine字段是元组,所以当使用println时将输出256位的Int8,但实际上只有字符串开始的几个ASCII值是我们需要的。
那么,如何解决这个问题?
Swift里的UnsafeMutablePointer提供两个方法,alloc(Int)和dealloc(Int),分别用来分配和解除分配模板T的数量参数。我们可以用这些API来简化我们的代码:
let name = UnsafeMutablePointer.alloc(1)
uname(name)
let machine = withUnsafePointer(&name.memory.machine, { (ptr) -> String? in
let int8Ptr = unsafeBitCast(ptr, UnsafePointer.self)
return String.fromCString(int8Ptr)
})
name.dealloc(1)
if let m = machine {
println(m)
}

 
第一步是调用withUnsafePointer,将机器的元组传递给它,并通知它我们的闭包将返回一个附加字符串。
在 闭包里面我们将指针转换为UnsafePointer,即该值的最等价的表述。除此之外,Swift的String包含一个类方法来初始化 UnsafePointer,这里的CChar是Int8类型的别名(typealias),所以我们能够将我们的新指针传递给初始化程序并且返回所需要 的信息。
在获取withUnsafePointer的结果之后,我们能够测试它是否是let的条件声明,并打印出结果。对这个例子来说,它输出了期望的字段“x86_64”。

总结

最 后,说一下免责声明。在Swift中使用不安全的API应该被视为最后手段,因为它们是潜在不安全的。当我们转换遗留的C和Objective-C代码到 Swift中,有很大可能性我们会继续需要这些API来兼容现有工具。然而,当使用withUnsafePointer和unsafeBitCast作为 首选手段时应该始终抱有怀疑态度,并寻找其他更好的解决方案。
新的代码应该尽可能符合语言习惯,不要在Swift代码中使用不安全的API。作为软件开发者,你应该了解如何使用你的工具以及什么地方该使用和什么地方不该使用。Swift将现代化带给了OS X和iOS开发,我们必须尊重它的理念。

本文链接:https://www.it72.com/8271.htm

推荐阅读
最新回复 (0)
返回