写点什么

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

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

关注

评论

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

北邮一号卫星搭载 IoTDB 成功发射!高效轻量数据管理,助力航天观测分析

Apache IoTDB

数据库 时序数据库 国产数据库

一个 go-sql-driver 的离奇 bug

SOFAStack

来自掘金和 InfoQ 的“新年礼物”,时序数据库 TDengine 荣登两大知名技术社区重磅榜单

TDengine

数据库 tdengine 时序数据库

Kyligence 成功签约阿斯利康,助力提升商业洞察质量和决策效率

Kyligence

数据分析 指标中台

企业内部沟通,即时通讯软件要怎么选?

WorkPlus

理想问鼎销冠,数据库能帮助新能源车企赢得下半场么?

YMatrix 超融合数据库

案例分享 新能源汽车 造车新势力 超融合数据库 YMatrix

maven pom 中有用的两个配置

JefferLiu

IoTDB 可实现的基本操作 : 数据导入、基本查询、聚合查询 | 小白教程文档(三)

Apache IoTDB

国产时序数据库

IoTDB 可实现的基本操作 —— 数据写入、删除、导出、元数据管理、时区设置 | 小白教程文档(四)

Apache IoTDB

时序数据库 国产数据库

如何利用极狐GitLab 轻松管理NPM依赖发布与更新?

极狐GitLab

node.js DevOps npm 依赖 极狐GitLab

【提升团队运营效率】交易履约之订单中心实践

京东科技开发者

平台搭建 平台赋能 数智创新 效能提升 企业号 1 月 PK 榜

利用 IoTDB 替换 Druid.io 服务太极股份电厂、军工制造类项目,采集精度达纳秒级

Apache IoTDB

时序数据库 国产数据库

OpenHarmony如何切换横竖屏?

OpenHarmony开发者

OpenHarmony

Cloud Kernel SIG月度动态:建立社区第三方驱动研发流程、发布ANCK 4.19-027版本 | 龙蜥 SIG

OpenAnolis小助手

内核 龙蜥社区 sig 版本发布 anck

小场景解决大问题|明道云在京东方的落地实践

明道云

WorkPlus移动办公平台,助力企业随时随地“指尖办公”

WorkPlus

Appuploader内测版本详解

雪奈椰子

iOS上架 ios打包

华为云云原生数据库:A Philosophy about “less”

华为云开发者联盟

数据库 后端 华为云 企业号 1 月 PK 榜

适配 Laravel 多版本的开源项目版本号规划

ModStart

特定领域知识图谱(DKG)融合方案:技术知识前置【一】-文本匹配算法、知识融合学术界方案、知识融合业界落地方案、算法测评KG生产质量保障

汀丶人工智能

自然语言处理 知识图谱 实体对齐

技术型创业公司如何把握发展与管理的节奏感?

石云升

极客时间 1月月更 技术领导力实战笔记

玖章算术CEO叶正盛:程序员必须掌握的数据库原理

NineData

数据库 程序员 开发 dba 数据库技术

被面试官问住了,MySQL两阶段提交是什么鬼?

程序员拾山

MySQL 面试

TDengine 时序数据特色查询语法详解,助力时序场景下的应用开发

TDengine

数据库 tdengine 时序数据库

MySQL性能优化浅析及线上案例

京东科技开发者

MySQL 性能优化 索引性能 数据库· 企业号 1 月 PK 榜

服务网格领域的百花齐放,是否存在一个更优解?

API7.ai 技术团队

Service Mesh 服务网格 APISIX

Databend v0.9.0 版本发布

Databend

一次偶然机会发现的MySQL“负优化”

骑牛上青山

MySQL 数据库 索引

Sealer 0.9 :帮助集群和分布式应用实现 Build、 Share、Run

阿里巴巴云原生

阿里云 开源 云原生 sealer

致 Tapdata 开源贡献者:聊聊 2022 年的进展和新一年的共建计划

tapdata

为什么MySQL数据删除了一半,表文件体积却没变小?

程序员拾山

MySQL

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