写点什么

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:463222
用户头像

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

关注

评论

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

震惊,PostGIS还可以这样用!!!

华为云开发者联盟

数据库 分布式 GaussDB 地理数据库 PostGIS

SphereEx 获数百万美元天使融资,接力 ShardingSphere 开启 Database Plus 新篇章

SphereEx

阿里云 AI 编辑部获 CCBN 创新奖,揭秘传媒行业解决方案背后的黑科技

阿里云视频云

阿里云 媒体 CCBN

Spring 实例化方式有几种?为什么会用到 Cglib?

小傅哥

Java spring 小傅哥 cglib 手写框架

华云大咖说 | 华云数据助力高校建设实训室平台

华云数据

QCon 演讲实录 | 大型软件团队的数字化项目管理实践

万事ONES

研发管理 团队协作 数字化 ONES Qcon

OpenResty入门

捉虫大师

nginx openresty

阿里面试题:MySQL 磁盘满了,怎么办?

Java架构师迁哥

🔎【Java 源码探索】深入浅出的分析Mutex底层源码

洛神灬殇

Java JVM mutex Condition 5月日更

千亿级数据迁移mongodb成本节省及性能优化实践

杨亚洲(专注MongoDB及高性能中间件)

MySQL 数据库 mongodb 架构 分布式数据库mongodb

Java程序员简历这么写,还过不了筛选算我输!

Java架构师迁哥

新生代小鲜肉之代码生成器

蛋先生DX

node.js 效率工具 自动化 生成代码

五分钟开发属于你自己的代码生成器

蛋先生DX

node.js 效率工具 生成代码 JavaScrip

从零开始学习ThingJS之创建/销毁物体

ThingJS数字孪生引擎

JavaScript 3D 3D可视化 数字孪生

开发人员应该害怕低代码吗?

禅道项目管理

程序员 低代码 开发 低代码平台

.Net Core Configuration Etcd数据源

yi念之间

etcd .net core

怎样节省 2/3 的 GPU?爱奇艺 vGPU 的探索与实践

爱奇艺技术产品团队

深度学习 gpu

Geek 青年说北京沙龙分享

看山

Geek青年说

如何优化你的HTTPS?

运维研习社

https HTTP2.0 5月日更

非官方不权威Java面试宝典

北游学Java

Java 面试

Springboot actuator不可不注意的安全问题-可越权-可脱库

果果果

安全 springboot

高并发存储优化篇:诸多策略,缓存为王

Coder的技术之路

缓存 缓存击穿 缓存雪崩 缓存架构

知乎的一次29.7元的咨询

why技术

Java 程序员

和12岁小同志搞创客开发:如何选择合适的传感器?

不脱发的程序猿

DIY 传感器 创客开发 如何选择合适的传感器?

日常Bug排查-系统失去响应-Redis使用不当

无毁的湖光

Java redis

个推“D-M-P”三步走, 打造每日治数平台,助力行业数字化升级

个推

大数据 数据中台 数据治理 数据智能

并发王者课-青铜8:分工协作-从本质认知线程的状态和动作方法

MetaThoughts

Java 多线程 并发 并发王者课

简单又灵活的权限设计?

蛋先生DX

数据库设计 权限系统 权限 权限架构 rbac

带你读论文丨异常检测算法及发展趋势分析

华为云开发者联盟

深度学习 异常检测算法 深度异常检测算法 深度半监督 群体异常检测

6月日更,优质更文,“定制”来袭~

InfoQ写作社区官方

6月日更 热门活动

长连接网关技术专题(五):喜马拉雅自研亿级API网关技术实践

JackJiang

Netty nio 网关

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