[关闭]
@pockry 2015-07-01T14:44:45.000000Z 字数 5061 阅读 1704

Swifter之UnsafePointer、接口和类方法中的Self、多元组

移动 iOS 品味书香 Swift


编者按:InfoQ开设新栏目“品味书香”,精选技术书籍的精彩章节,以及分享看完书留下的思考和收获,欢迎大家关注。本文节选自王巍著《Swifter : 100 个 Swift 开发必备 Tip》中的三个章节UnsafePointer、接口和类方法中的Self、多元组,分享了作者对Swift语言的研究和发现。

UnsafePointer

Swift本身从设计上来说是一门非常安全的语言,在Swift的思想中,所有的引用或者变量的类型都是确定并且正确对应它们的实际类型的,你应当无法进行任意的类型转换,也不能直接通过指针做出一些“出格”的事情。这种安全性在日常的程序开发中对于避免不必要的bug,以及迅速而且稳定地找出代码错误是非常有帮助的。但是凡事都有两面性,在安全性高的同时,Swift也相应地丧失了部分的灵活性。

现阶段想要完全抛弃C的一套东西还是相当困难的,特别是在很多“上古”级别的C API框架还在使用(或者被间接使用)。开发者,尤其是偏向较底层的框架的开发者不得不与C API打交道的时候,一个在Swift中不被鼓励的东西就出现了,那就是指针。

为了与庞大的“C系帝国”进行合作,Swift定义了一套指针的访问和转换方法,那就是UnsafePointer和它的一系列变体。对于使用C API时遇到接受内存地址作为参数,或者返回是内存地址的情况,在Swift里会将它们转换为UnsafePointer<Type>的类型,比如说如果某个API在C中是这样的话:

  1. void method(const int *num) {
  2. printf("%d",*num);
  3. }

其对应的Swift方法应该是:

  1. func method(num: UnsafePointer<CInt>) {
  2. print(num.memory);
  3. }

本节中所说的UnsafePointer,就是Swift中专门针对指针的转换。对于其他的C中的基础类型,在Swift中对应的类型都遵循统一的命名规则:在前面加上一个字母C并将原来的第一个字母大写:比如int、bool和char的对应类型分别是CInt、CBool和CChar。在上面的C方法中,我们接受一个int的指针,转换到Swift里所对应的就是一个C Int的UnsafePointer类型。这里原来的C API中已经指明了输入的num指针是不可变的(const),因此在Swift中我们与之对应的是UnsafePointer这个不可变版本。如果只是一个普通的可变指针的话,我们可以使用UnsafeMutablePointer来对应:

  1. [c]@ll@ C API & Swift APIconst Type * & UnsafePointerType * & UnsafeMutablePointer

在C中,对某个指针进行取值使用的是*,而在Swift中我们可以使用memory属性来读取相应内存中存储的内容。在通过传入指针地址进行方法调用的时候就都比较相似了,都是在前面加上&符号,C的版本和Swift的版本只在申明变量的时候有所区别:

  1. // C
  2. int a = 123;
  3. method(&a); // 输出 123
  4. // Swift
  5. var a: CInt = 123
  6. method(&a) // 输出 123

遵守这些原则,使用UnsafePointer在Swift中进行C API的调用就应该不会有很大问题了。

另外一个重要的课题是如何在指针的内容和实际的值之间进行转换。比如我们如果由于某种原因需要直接使用CFArray的方法来获取数组中元素的时候,我们会用到这个方法:

  1. func CFArrayGetValueAtIndex(theArray: CFArray!, idx: CFIndex)
  2. -> UnsafePointer<Void>

因为CFArray中是可以存放任意对象的,所以这里的返回是一个任意对象的指针,相当于C中的void。这显然不是我们想要的东西。Swift为我们提供了一个强制转换的方法unsafeBitCast,通过下面的代码,我们可以看到应当如何使用类似这样的API,将一个指针强制按位转换成所需类型的对象:

  1. let arr = NSArray(object: "meow")
  2. let str = unsafeBitCast(CFArrayGetValueAtIndex(arr, 0), CFString.self)
  3. // str = "meow"

unsafeBitCast会将第一个参数的内容按照第二个参数的类型进行转换,而不去关心实际是不是可行,这也正是UnsafePointer的不安全性所在,因为我们不必遵守类型转换的检查,而拥有了在指针层面直接操作内存的机会。

其实说了这么多,Apple将直接的指针访问冠以Unsafe的前缀,就是提醒我们:这些东西不安全,大家能不用就别用了吧(Apple的另一个重要的考虑是,避免指针可以减少很多系统漏洞)!在日常开发中,我们确实不需要经常和这些东西打交道(除了传入NSError指针这个历史遗留问题以外)。总之,尽可能地在高抽象层级编写代码,会是高效和正确率的有力保证。无数先辈已经用“血淋淋”的教训告诉我们,要避免去做这样的不安全的操作,除非你确实知道你做的是什么。

接口和类方法中的Self

我们在看一些接口的定义时,可能会注意到首字母大写的Self出现在类型的位置上,例如:

  1. protocol IntervalType {
  2. //...
  3. /// Return `rhs` clamped to `self`. The bounds of the result, even

  4. /// if it is empty, are always within the bounds of `self`

  5. func clamp(intervalToClamp: Self) -> Self


  6. //...

  7. }

上面这个IntervalType的接口定义了一个方法,接受实现该接口的自身的类型,并返回一个同样的类型。

这么定义是因为接口本身其实没有自己的上下文类型信息,在声明接口的时候,我们并不知道最后究竟会是什么样的类型来实现这个接口,Swift中也不能在接口中定义泛型进行限制。而在声明接口时,我们如果希望在接口中使用的类型就是实现这个接口本身的类型的话,就需要使用Self进行指代。

但是在这种情况下,Self不仅指代实现该接口的类型本身,也包括了这个类型的子类。从概念上来说,Self十分简单,但是实际实现一个这样的方法却要稍微转个弯。为了说明这个问题,我们假设要实现一个Copyable的接口,满足这个接口的类型需要返回一个和接受方法调用的实例相同的拷贝。一开始我们可能考虑这样的接口:

  1. protocol Copyable {
  2. func copy() -> Self
  3. }

这是很直接明了的,它应该做的是创建一个和接受这个方法的对象同样的东西,然后将其返回,返回的类型不应该发生改变,所以写为Self。然后开始尝试实现一个MyClass来满足这个接口:

  1. class MyClass: Copyable {


  2. var num = 1


  3. func copy() -> Self {
  4. // TODO: 返回什么?

  5. // return

  6. }
  7. }

我们一开始的时候可能会写类似这样的代码:

  1. //这是错误代码
  2. func copy() -> Self {
  3. let result = MyClass()
  4. result.num = num

  5. return result

  6. }

但显然类型是有问题的,因为该方法要求返回一个抽象的、表示当前类型的Self,我们却返回了它的真实类型MyClass,这会导致无法编译。也许你会尝试把方法声明中的Self改为MyClass,这样声明就和实际返回一致了,但是你很快会发现,如果这样的话,实现的方法又和接口中的定义不一样了,依然不能编译。

为了解决这个问题,我们需要通过一个和上下文(也就是和MyClass)无关的,又能够指代当前类型的方式进行初始化。希望你还能记得我们在“获取对象类型”一节中所提到的dynamicType,在这里我们就可以使用它来做初始化,以保证方法与当前类型上下文无关,这样不论是MyClass还是它的子类,都可以正确地返回合适的类型满足Self的要求:

  1. func copy() -> Self {
  2. let result = self.dynamicType()
  3. result.num = num

  4. return result

  5. }

但是很不幸,单单是这样还是无法通过编译,编译器提示我们如果想要构建一个Self类型的对象的话,需要有required关键字修饰的初始化方法,这是因为Swift必须保证当前类和其子类都能响应这个init方法。在这个例子中,我们添加一个required的init就行了。最后,MyClass类型是这样的:

  1. class MyClass: Copyable {


  2. var num = 1


  3. func copy() -> Self {
  4. let result = self.dynamicType()
  5. result.num = num

  6. return result

  7. }


  8. required init() {


  9. }
  10. }

我们可以通过测试来验证一下此行为的正确性:

  1. let object = MyClass()
  2. object.num = 100


  3. let newObject = object.copy()
  4. object.num = 1


  5. println(object.num) // 1

  6. println(newObject.num) // 100

而对于MyClass的子类,copy()方法也能正确地返回子类的经过拷贝的对象了。
另一个可以使用Self的地方是在类方法中,使用起来也与此十分相似,核心就在于保证子类也能返回恰当的类型。

多元组(Tuple)

多元组是我们的“新朋友”,多尝试使用这个新特性,会让工作轻松不少。

比如交换输入,普通程序员“亘古以来”可能都是这么写的:

  1. func swapMe<T>(inout a: T, inout b: T) {
  2. let temp = a

  3. a = b

  4. b = temp

  5. }

但是要是使用多元组的话,我们不使用额外空间就可以完成交换,一下子就实现了“文艺程序员”的写法:

  1. func swapMe<T>(inout a: T, inout b: T) {
  2. (a,b) = (b,a)
  3. }

另外一个挺常用的地方是错误处理。在Objective-C时代我们已经习惯了在需要错误处理的时候先做一个NSError的指针,然后将地址传到方法里等待填充:

  1. NSError *error = nil;
BOOL success = [[NSFileManager defaultManager]
  2. moveItemAtPath:@"/path/to/target"
 toPath:@"/path/to/destination"
 error:&error];
  3. if (!success) {
  4. NSLog(@"%@", error);
  5. }

现在我们写库的时候可以考虑直接返回一个带有NSError 的多元组,而不是去填充地址了:

  1. func doSomethingMightCauseError() -> (Bool, NSError?) {
  2. //...做某些操作,成功结果放在 success 中

  3. if success {
  4. return (true, nil)
  5. } else {
  6. return (false, NSError(domain:"SomeErrorDomain", code:1, userInfo: nil))
  7. }
  8. }

在使用的时候,与之前的做法相比,现在就更简单了:

  1. let (success, maybeError) = doSomethingMightCauseError()
  2. if let error = maybeError {
  3. // 发生了错误
  4. }

一个有趣但是不被注意的事实,其实在Swift中任何东西都是放在多元组里的。

不相信?试试看输出这个吧:

  1. var num = 42
println(num)
  2. println(num.0.0.0.0.0.0.0.0.0.0)

书籍简介

【图片】

本书是 Swift 语言的知识点的集合,本书的写作目的是为广大已经入门了 Swift 的开发者提供一些参考,以期能迅速提升他们在实践中的能力。本书非常适合用作官方文档的参考和补充,也是中级开发人员适用的 Swift 进阶读本。

作者简介

王巍 (onevcat) 是来自中国的一线 iOS 开发者,毕业于清华大学。在校期间就开始进行 iOS 开发,拥有丰富的 Cocoa 和 Objective-C 开发经验。另外,王巍还是翻译项目 objc 中国的组织者和管理者,为中国的 Objective-C 社区的发展做出了贡献。同时,他也是著名的 Xcode 插件 VVDocumenter 的作者。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注