【QCon】精华内容上线85%,全面覆盖“人工智能+”的典型案例!>>> 了解详情
写点什么

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

  • 2015-07-01
  • 本文字数:4169 字

    阅读完需:约 14 分钟

编者按: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的类型,比如说如果某个 API 在 C 中是这样的话:

复制代码
void method(const int *num) {
printf("%d",*num);
}

其对应的 Swift 方法应该是:

复制代码
func method(num: UnsafePointer<CInt>) {
print(num.memory);
}

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

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

复制代码
// C
int a = 123;
method(&a); // 输出 123
// Swift
var a: CInt = 123
method(&a) // 输出 123

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

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

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

复制代码
let arr = NSArray(object: "meow")
let str = unsafeBitCast(CFArrayGetValueAtIndex(arr, 0), CFString.self)
// str = "meow"

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

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

接口和类方法中的 Self

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

复制代码
protocol IntervalType {
//...
/// Return `rhs` clamped to `self`. The bounds of the result, even
/// if it is empty, are always within the bounds of `self`
func clamp(intervalToClamp: Self) -> Self
//...
}

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

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

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

复制代码
protocol Copyable {
func copy() -> Self
}

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

复制代码
class MyClass: Copyable {
var num = 1
func copy() -> Self {
// TODO: 返回什么?
// return
}
}

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

复制代码
// 这是错误代码
func copy() -> Self {
let result = MyClass()
result.num = num
return result
}

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

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

复制代码
func copy() -> Self {
let result = self.dynamicType()
result.num = num
return result
}

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

复制代码
class MyClass: Copyable {
var num = 1
func copy() -> Self {
let result = self.dynamicType()
result.num = num
return result
}
required init() {
}
}

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

复制代码
let object = MyClass()
object.num = 100
let newObject = object.copy()
object.num = 1
println(object.num) // 1
println(newObject.num) // 100

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

多元组(Tuple)

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

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

复制代码
func swapMe<T>(inout a: T, inout b: T) {
let temp = a
a = b
b = temp
}

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

复制代码
func swapMe<T>(inout a: T, inout b: T) {
(a,b) = (b,a)
}

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

复制代码
NSError *error = nil;
BOOL success = [[NSFileManager defaultManager]
moveItemAtPath:@"/path/to/target"
toPath:@"/path/to/destination"
error:&error];
if (!success) {
NSLog(@"%@", error);
}

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

复制代码
func doSomethingMightCauseError() -> (Bool, NSError?) {
//... 做某些操作,成功结果放在 success 中
if success {
return (true, nil)
} else {
return (false, NSError(domain:"SomeErrorDomain", code:1, userInfo: nil))
}
}

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

复制代码
let (success, maybeError) = doSomethingMightCauseError()
if let error = maybeError {
// 发生了错误
}

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

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

复制代码
var num = 42
println(num)
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 的作者。

2015-07-01 01:463080
用户头像

发布了 59 篇内容, 共 19.4 次阅读, 收获喜欢 4 次。

关注

评论

发布
暂无评论
发现更多内容

2022第三届云原生编程挑战赛--Serverless VSCode WebIDE使用体验

六月的雨在InfoQ

Serverless 边缘容器 9月月更 Serverless VSCode WebIDE 线上ide

阿里云服务器ECS基本操作指南

六月的雨在InfoQ

阿里云 SSH xshell 云服务器ECS 9月月更

SAE 助力贵州酒店集团从容支撑贵州特产抢购

阿里巴巴中间件

阿里云 Serverless 云原生 SAE

50道Java集合高频面试题,看完面试成功率99%

钟奕礼

Java 面试 java;

Java | this和super关键字【深入理解子类和父类的继承关系】

Fire_Shield

super this 9月月更

手写vue-router核心原理

hellocoder2029

Vue

时隔一年多 jQuery 再度发布 3.6.1 新版本,你还在用JQ吗?

茶无味的一天

JavaScript 前端 框架 ​jQuery

字节半天*3面/5天拿offer,全凭自身硬实力和这份Java面试笔记

钟奕礼

Java 面试 java;

数据API开发如何快速上手:先了解什么是数据API生命周期管理

雨果

API 数据api

为什么大数据工程师比数据科学家的需求更大

雨果

数据工程师

模块一作业

架构实战训练营模块1作业--开启架构之旅

阿姆斯壮

架构实战营 #架构实战营

HTTP - TLS1.3 初次解读

懒时小窝

中心化决议管理——云端分析

字节跳动终端技术

ios 研发效能 CocoaPods 制品库 云化服务

总览 Java 容器--集合框架的体系结构

钟奕礼

Java 面试 java;

概述数据交换的构建策略

穿过生命散发芬芳

数据交换 9月月更

组装式交付-云巧 知多少

六月的雨在InfoQ

9月月更 云巧 组装式交付 云巧资产 云巧工坊

Java开发5年,复习1个月成功上岸京东物流,面试和复习思路分享

钟奕礼

Java 面试 java;

一比一手写迷你版vue,彻底搞懂vue运行机制

hellocoder2029

JavaScript

EMQ荣获工信部第五届“绽放杯”5G应用征集大赛智慧金融专题一等奖

EMQ映云科技

5G 物联网 IoT 数智化 9月月更

java基础面试题

钟奕礼

编程 java;

开发者有话说|成长之路

六月的雨在InfoQ

个人成长 开会 996 007 9月月更

玩转 Flowable 流程实例

江南一点雨

Java springboot workflow flowable

想从事运维岗位应该学习什么技能?谁能告诉一下?

行云管家

运维 网络运维 IT运维

idea 远程开发 client

黄敏

IP地址和MAC地址都可以确定目标地址,为什么二者都在使用,舍弃一个是否可行?

阿柠xn

Mac IP 网络 协议族 9月月更

深入剖析nodejs中间件

coder2028

node.js

公司用的堡垒机叫什么?多少钱?

行云管家

网络安全 堡垒机 等级保护 过等保

2022届秋招Java岗高频面试题盘点,老司机也未必全会,真的太卷了

钟奕礼

Java 面试 java;

LED显示屏价格与品质哪个更重要

Dylan

LED LED显示屏 led显示屏厂家

【Java深入学习】并发常见方法的注意事项

钟奕礼

Java 面试 java;

Swifter之UnsafePointer、接口和类方法中的Self、多元组_移动_王巍_InfoQ精选文章