【ArchSummit】如何通过AIOps推动可量化的业务价值增长和效率提升?>>> 了解详情
写点什么

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

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

关注

评论

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

linux服务器网络编程之线程模型

Linux服务器开发

reactor 线程模型 Linux服务器开发 Linux网络编程 服务器模型

HarmonyOS学习路之开发篇—— Java UI框架(组件与布局说明)

爱吃土豆丝的打工人

HarmonyOS 布局与组件 java UI

决定论:区块链的哲学

CECBC

极客星球|基于加权DBSCAN的工作地居住地计算方法

MobTech袤博科技

无服务框架-OpenFaas

QiLab

中国如何应对中美科技博弈?

石云升

科技革命 8月日更 启发 中美博弈

支持高性能计算场景,博云容器云打造智能算力引擎

BoCloud博云

容器 高性能计算

Dubbo No provider问题排查思路

捉虫大师

dubbo no provider

JavaScript 正则表达式的 5 个方法

devpoint

regex match 8月日更

智能合约系统开发|智能合约DAPP搭建

Geek_23f0c3

DeFi去中心化系统开发 DAPP智能合约交易系统开发

更智能更高效,区块链打造更美服装行业

CECBC

在云中进行灾难恢复的5种有效方式

浪潮云

云计算

mPaaS 月度小报|为采购而生,全新资源包上架;前端 2D 游戏化互动入门指南

蚂蚁集团移动开发平台 mPaaS

mPaaS 小程序市场

浅谈云上攻防——对象存储服务访问策略评估机制研究

腾讯安全云鼎实验室

云原生 对象存储 数据安全 云上安全攻防

为什么VR视频的清晰度不高?

爱奇艺技术产品团队

编码 vr 编解码 8k 解码

夯实基础,踏步云升 | 云原生 DevOps 入门必读

CODING DevOps

DevOps 云原生 CODING

信息过载,加班无度的时代,我们需要「洞穴空间」

非著名程序员

个人成长 提升认知 思维 8月日更

如何查看github和gitee评论消息

Albert

GitHub

教你用python爬虫下载1w+『ppt模板』,再也不用付费去购买啦!

Python研究者

8月日更

走出回归测试困境,爱奇艺精准测试体系建设

爱奇艺技术产品团队

测试 开发 精准测试 i技术会

JVM运行时数据区域

W🌥

Java JVM 8月日更

闭关修炼21天,“啃完”283页pdf,我终于4面拿下字节跳动offer

Java 程序员 面试 计算机

投资ipfs挖矿有风险吗?投资ipfs挖矿要多少钱?

投资ipfs挖矿有风险吗 投资ipfs挖矿要多少钱

实时音视频,是一门好的ToB生意吗?

ToB行业头条

面对物联网安全隐患高墙,熵核科技如何实现突围

熵核科技

物联网安全

基于 Web 端的屏幕共享实践

ZEGO即构

大前端 音视频 web端屏幕共享

开源应用中心 | 快来一键体验,这款仅有400KB的开源博客程序!

开源

Spring

ltc

spring

Drools 基础语法

LeifChen

drools 规则引擎 8月日更

云原生的数据云,下一个十年的数字化转型趋势

星环科技

数字化转型 云数据库

音频技术及行业的发展

声网

音频技术

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