最新发布《数智时代的AI人才粮仓模型解读白皮书(2024版)》,立即领取! 了解详情
写点什么

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

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

关注

评论

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

程序性能优化全能手册

快乐非自愿限量之名

php 性能优化

5G-A,一根蓄满能量的弹簧

脑极体

5G

一文读懂 $mash 通证 “Fair Launch” 规则(幸运池玩法解读篇)

EOSdreamer111

One Switch 让你的 Mac 也能拥有控制中心

Rose

One Switch下载 苹果Mac 必备 系统功能快速开关工具

MegEngine 11-12 双月报:新版本发布,开发者福利课程,MegEngine 使用技巧,精彩不容错过!

MegEngineBot

深度学习 开源 框架 寒武纪

软件测试/测试开发/全日制|Pytest参数化神器,pytest.mark.parametrize()使用

霍格沃兹测试开发学社

解读 $mash 通证 “Fair Launch” 规则,将公平发挥极致?(幸运池玩法)

石头财经

避免重复扣款:分布式支付系统的幂等性原理与实践

隐墨星辰

幂等性 支付系统设计与实现 全局幂等 防止重复扣款

软件测试/测试开发/全日制|Pytest都有哪些命名规则?

霍格沃兹测试开发学社

恭喜 Databend 上榜 2023 开源创新榜「优秀开源项目 」

Databend

文字处理软件推荐 Mellel 6 激活中文版

胖墩儿不胖y

Mac软件 文字处理软件

一图回顾 Apache IoTDB 的 2023!

Apache IoTDB

聚道云软件连接器助力某动漫行业公司实现财务自动化

聚道云软件连接器

案例分享

【分布式技术专题】「分析Web服务器架构」Tomcat服务器的运行架构和LVS负载均衡的运行机制(修订版)

洛神灬殇

tomcat 分布式 分布式技术 架构原理 2024年第三篇文章

一文读懂 $mash 通证 “Fair Launch” 规则(幸运池玩法解读篇)

西柚子

macOS 14 Sonoma(最新MacOS系统) pkg完整安装包 14.2正式版

Rose

K8S学习指南(3)-minikube的安装

俞兆鹏

【Redis技术专区】「原理分析」探讨Redis6.0为何需要启用多线程

洛神灬殇

redis 多线程 原理分析 并发机制 2024年第一篇文章

C 语言指针完全指南:创建、解除引用、指针与数组关系解析

小万哥

程序人生 编程语言 软件工程 C/C++ 后端开发

一文读懂 $mash 通证 “Fair Launch” 规则(幸运池玩法解读篇)

股市老人

通过聚道云软件连接器实现金蝶软件与客如云软件的无缝对接

聚道云软件连接器

案例分享

解读 $mash 通证 “Fair Launch” 规则(Staking 玩法解读篇)

BlockChain先知

IBM SPSS Statistics 默认工具栏缺失怎么办?

Rose

Scrum敏捷研发管理流程及示例

顿顿顿

Scrum 敏捷开发 Scrum Master 敏捷项目管理 敏捷研发管理工具

【Redis深度专题】「核心技术提升」从源码角度探究Redis服务的内存使用、清理以及逐出等底层实现原理

洛神灬殇

redis 底层原理 清理缓存 原理机制 功能实现 2024年第二篇文章

【Mac/win】Topaz Gigapixel AI v6.3.3 绿色便携版 自带激活补丁

Rose

图片放大工具 Topaz Gigapixel AI便携版 Topaz Gigapixel AI教程

支持M1/M2,Axure RP 9授权密钥激活可用 附 Axure RP 9激活教程

Rose

Axure RP 9汉化 原型设计软件 Axure RP 9破解版 Axure RP 9授权

软件测试/测试开发/全日制|pytest用例执行顺序,这篇文章就够了

霍格沃兹测试开发学社

TuGraph Analytics作业监控面板:运行时组件上的高效分析工具

TuGraphAnalytics

分布式 监控 图计算

一款便捷操作移动设备的PC工具

iofomo

android 开发工具 测试工具 PC ios 开发

解读 $mash 通证 “Fair Launch” 规则,将公平发挥极致?(Staking 玩法)

石头财经

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