Swift -> Optional嵌套 探讨
准备运动:Optional 的介绍
王巍的《Swifter》一书中,介绍了一个有用的命令:在 LLDB 中输入 fr v -R foo
,可以查看foo
这个变量的内存构成。我们稍后的分析将用到这个命令。
在 Swift 的世界里,一切皆对象,包括 Int Float 这些基本数据类型,所以我们可以这么写:print(1.description)
。
而对象一般都是存储在指针中,Swift 也不例外,这就造成了一个问题,指针为空的情况需要处理。在 Objective-C 中,向一个 nil 的对象发消息是默认不产生任何效果的行为,但是在 Swift 中,这种行为被严格地禁止了。
Swift 是一个强类型语言,它希望在编译期做更多的安全检查,所以引入了类型推断。而类型推断上如果要做到足够的安全,避免空指针调用是一个最基本的要求。于是,Optional 这种类型出现了。Optional 在 Swift 语言中其实是一个枚举类型:
public enum Optional<Wrapped> : _Reflectable, NilLiteralConvertible {
case None
case Some(Wrapped)
}
Optional 的嵌套
Optional 类型的变量,在使用时,大多需要用if let
的方式来解包。如果你没有解包而直接使用,编辑器通过类型推断会提示你,所以看起来这套机制工作得很好。但是,如果 Optional 嵌套层次太多,就会造成一些麻烦,下面我们来看一个例子。
let a: Int? = 1
let b: Int?? = a
let c: Int??? = b
在这个机制中,1 这个 Int 值被层层 Optional 包裹,我们用刚刚提到的fr v -R
,可以很好的看出来内部结构。如下图:
(lldb) fr v -R a
(Swift.Optional<Swift.Int>) a = Some {
Some = {
value = 1
}
}
(lldb) fr v -R b
(Swift.Optional<Swift.Optional<Swift.Int>>) b = Some {
Some = Some {
Some = {
value = 1
}
}
}
(lldb) fr v -R c
(Swift.Optional<Swift.Optional<Swift.Optional<Swift.Int>>>) c = Some {
Some = Some {
Some = Some {
Some = {
value = 1
}
}
}
}
从这个示例代码中,我们能看出来多层嵌套的 Optional 的具体内存结构。这个内存结构其实是一个类似二叉树一样的形状,如下图所示:
- 第一层二叉树有两个可选的值,一个值是 .None,另一个值类型是
Optional<Optional<Int>>
。 - 第二层二叉树有两个可选的值,一个值是 .None,另一个值类型是
Optional<Int>
。 - 第三层二叉树有两个可选的值,一个值是 .None,另一个值类型是
Int
。
那么问题来了,看起来这个 Optional.None 可以出现在每一层,那么在每一层的效果一样吗?我做了如下实验:
let a: Int? = nil
let b: Int?? = a
let c: Int??? = b
let d: Int??? = nil
如果你在 playground 上看,它们的值都是 nil,但是它们的内存布局却不一样,特别是变量 c 和 变量 d:
(lldb) fr v -R a
(Swift.Optional<Swift.Int>) a = None {
Some = {
value = 0
}
}
(lldb) fr v -R b
(Swift.Optional<Swift.Optional<Swift.Int>>) b = Some {
Some = None {
Some = {
value = 0
}
}
}
(lldb) fr v -R c
(Swift.Optional<Swift.Optional<Swift.Optional<Swift.Int>>>) c = Some {
Some = Some {
Some = None {
Some = {
value = 0
}
}
}
}
(lldb) fr v -R d
(Swift.Optional<Swift.Optional<Swift.Optional<Swift.Int>>>) d = None {
Some = Some {
Some = Some {
Some = {
value = 0
}
}
}
}
- 变量 c 因为是多层嵌套的 nil,所以它在最外层的二叉树上的值,是一个
Optional<Optional<Int>>
。 - 变量 d 因为是直接赋值成 nil,所以它在最外层的二叉树上的值,是一个
Optional.None
。
麻烦的事情来了,以上原因会造成用 if let 来判断变量 c 是否为 nil 失效了。如下代码最终会输出 c is not none
。
let a: Int? = nil
let b: Int?? = a
let c: Int??? = b
let d: Int??? = nil
if let _ = c {
print("c is not none")
}
解释
在我看来,这个问题的根源是:一个 Optional 类型的变量可以接受一个非 Optional 的值。拿上面的代码举例,a 的类型是 Int?,b 的类型是 Int??,但是 a 的值却可以赋值给 b。所以,变量 b(类型为 Int??),它可以接受以下几种类型的赋值:
- nil 类型
- Int? 类型
- Int?? 类型
按理说,Swift 是强类型,等号左右两边的类型不完全一样,为什么能够赋值成功呢?我查了一下 Optional 的源码,原来是对于上面第 1,2 种类型不一样的情况,Optional 定义了构造函数来构造出一个 Int?? 类型的值,这样构造之后,等号左右两边就一样了。源码来自https://github.com/apple/swift/blob/master/stdlib/public/core/Optional.swift,我摘录如下:
public enum Optional<Wrapped> : _Reflectable, NilLiteralConvertible {
case None
case Some(Wrapped)
@available(*, unavailable, renamed="Wrapped")
public typealias T = Wrapped
/// Construct a `nil` instance.
@_transparent
public init() { self = .None }
/// Construct a non-`nil` instance that stores `some`.
@_transparent
public init(_ some: Wrapped) { self = .Some(some) }
}
以上代码中,Optional 提供了两种构造函数,完成了刚刚提到的类型转换工作。
烧脑体操
好了,说了这么多,我们下面开始烧脑了,以下代码来自傅若愚在不久前 Swift 大会上的一段分享:
var dict :[String:String?] = [:]
dict = ["key": "value"]
func justReturnNil() -> String? {
return nil
}
dict["key"] = justReturnNil()
dict
以下是代码执行结果:
我们可以看到,我们想通过给这个 Dictionary 设置一个 nil,来删除掉这个 key-value 对。但是从 playground 的执行结果上看,key 并没有被删掉。
为了测试到底设置什么样的值,才能正常地删掉这个 key-value 键值对,我做了如下实验:
var dict :[String:String?] = [:]
// first try
dict = ["key": "value"]
dict["key"] = Optional<Optional<String>>.None
dict
// second try
dict = ["key": "value"]
dict["key"] = Optional<String>.None
dict
// third try
dict = ["key": "value"]
dict["key"] = nil
dict
// forth try
dict = ["key": "value"]
let nilValue:String? = nil
dict["key"] = nilValue
dict
// fifth try
dict = ["key": "value"]
let nilValue2:String?? = nil
dict["key"] = nilValue2
dict
执行结果如下:
我们可以看到,以下三种方式可以成功删除 key-value 键值对:
dict["key"] = Optional<Optional<String>>.None
dict["key"] = nil
let nilValue2:String?? = nil; dict["key"] = nilValue2
所以,在这个烧脑之旅中,我们发现,一个 [String: String?] 的 Dictionary,可以接受以下类型的赋值:
- nil
- String
- String?
- String??
如果要删除这个 Dictionary 中的元素,必须传入 nil 或 Optional<Optional<String>>.None
,而如果传入Optional<String>.None
,则不能正常删除元素。
好吧,实验出现象了,那这种现象的原因是什么呢?
还好苹果把它的实现开源了,那我们来一起看看吧,源文件来自:https://github.com/apple/swift/blob/master/stdlib/public/core/HashedCollections.swift.gyb,以下是关键代码。
public subscript(key: Key) -> Value? {
get {
return _variantStorage.maybeGet(key)
}
set(newValue) {
if let x = newValue {
// FIXME(performance): this loads and discards the old value.
_variantStorage.updateValue(x, forKey: key)
}
else {
// FIXME(performance): this loads and discards the old value.
removeValueForKey(key)
}
}
}
所以,当 Dictionary 的 value 类型为 String 时,如果你要设置它的值,它接受的是一个 String? 类型的参数。而因为我们刚刚例子中的 value 类型为 String?,所以正常情况下它需要的是一个 String?? 类型的参数。在上面的失败的例子中,我们传递的是一个 String? 类型的值,具体值为Optional<String>.None
,于是在执行时就会按以下的步骤来进行:
- 我们传递一个值为
Optional<String>.None
,类型为 String? 的参数。 - 因为传的参数类型是 String?,而函数需要的是 String??,所以会执行 Optional 的构造函数,构造一个两层的 Optional。
- 这个两层 Optional 的值为
Optional.Some(<Optional<String>.None>)
- 进入到 Dictionary 的实现时,会用 if let 进行是否为 nil 的判断,因为两层的 Optional,所以 if let 判断它不是 nil。
- 所以代码执行到
_variantStorage.updateValue(x, forKey: key)
,把 Optional.None 当成值,设置给了相应的 key。
如果你没理解,可以再翻翻最初我们对多层嵌套 nil 变量的实验和分析。
我们再看看传递参数是 Optional<Optional<String>>.None
的情况,步骤如下:
- 我们传递一个值为
Optional<Optional<String>>.None
,类型为 String?? 的参数。 - 因为参数类型是 String??,函数需要的类型也是 String??,所以参数不经变换,直接进入函数调用中。
- 这个时候参数的值不变,还是
Optional<Optional<String>>.None
。 - 进入到 Dictionary 的实现时,会用 if let 进行是否为 nil 的判断,
Optional<Optional<String>>.None
用 if let 判断,得到它是 nil。 - 所以代码执行到
removeValueForKey(key)
,Dictionary 删除了相应的 key-value 键值对。
转载自:http://www.infoq.com/cn/articles/swift-brain-gym-optional?utm_campaign=rightbar_v2&utm_source=infoq&utm_medium=articles_link&utm_content=link_text
Swift -> Optional嵌套 探讨的更多相关文章
- Swift Optional
拆包和解包的原因: 其实所谓的 nil 就是 Optional.None, 非 nil 就是Optional.Some, 然后会通过Some(T)包装(wrap)原始值,这也是为什么在使用 Optio ...
- Swift optional详解
学习了一阵swift对于optional类型一直不是很了解,为此特意学习了一下,通过观看斯坦福大学的视频,将他所讲的进行了总结 optional 有两个值 1.not set (未设) 代表这个值没有 ...
- Swift 类型嵌套
1.类型嵌套 Swift 支持类型嵌套,把需要嵌套的类型的定义写在被嵌套的类型的 {} 中. Swift 中的枚举类型可以辅助实现特定的类或者结构体的功能. struct SchoolUniform ...
- SWIFT Optional Value
SWIFT中有一个类型定义叫可选值,在变量类型后面加一个?号即可定义一个类型为Optional Value的变量,当在使用变量时要用到强制解包!. 如在页面上有一个可选输入年龄的框,在接受数据的时间就 ...
- swift 之嵌套的理解 func chooseStepFunction(backwards: Bool) -> (Int) -> Int
http://blog.csdn.net/lzx_322/article/details/28861199 swift 函数使用前面需要添加 func 有返回值需要使用-> 后面添加返回类型 , ...
- Swift Optional Chaining
Optional Chaining介绍 关于「optional chaining」,<The Swift Programming Language>是这么描述的: Optional cha ...
- swift 闭包+嵌套函数+extension+单例+嵌套函数+??
//: Playground - noun: a place where people can play import UIKit //*******************嵌套函数********* ...
- Swift - 类型嵌套(以扑克牌结构体为例)
类型嵌套,简单来说实在一个类型中包含另外一个类型.我们拿一副扑克来说明. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 //类 ...
- [Swift]LeetCode339. 嵌套链表权重和 $ Nested List Weight Sum
Given a nested list of integers, return the sum of all integers in the list weighted by their depth. ...
随机推荐
- redis的常用命令
键值得相关命令: keys * 返回满足给定patternd的所有key exists 确认一个key是否存在 del 删除一个key expire 设置一个key的过期时间 ttl 查看过期时间还有 ...
- Spring Timer实现
定时器:继承java.util.TimerTask类实现run方法 package com.zbb.framework.util.timer; import java.util.TimerTask; ...
- ssh environment variable
1 down vote When you run a command as an argument to ssh, the command is run directly by sshd; the s ...
- iOS中的项目新特性页面的处理
一般项目中都会出现新特性页面,比如第一次使用应用的时候,或者在应用设置里查看新特性的时候会出现. 这里,选择新建一个专门处理项目新特性的控制器,来完成功能. 首先是 NewFeaturesViewCo ...
- 在VS中生成后拷贝文件
环境:win7_64旗舰版,VS2013 工作项目中,一般会使用第三方库,当修改并重新编译第三方库后,需要将DLL文件拷贝到工作项目下的生成目录中,每次手动拷贝比较繁琐,VS提供自定义生成事件,允许我 ...
- mongoDB7--游标cursor
之前我们学习了"增删改查"四中语法和查询表达式的深入学习,我们已经掌握了一定的操作mongodb数据的能力,那么接下来我们就要考虑我们的操作的效率问题了.(1)游标介绍如果我们查询 ...
- 《JavaScript高级程序设计》读书笔记 ---执行环境及作用域
执行环境及作用域 执行环境(execution context,为简单起见,有时也称为“环境”)是JavaScript 中最为重要的一个概念.执行环境定义了变量或函数有权访问的其他数据,决定了它们各自 ...
- HDU 5769 Substring
后缀数组. 然后按照排序完成之后的顺序,每个后缀统计贡献量. 统计第i个后缀的贡献的时候,如果这个后缀中没有X,贡献度为0. 有贡献的分3种情况考虑: 1.如果这个后缀height部分等于0(即与前一 ...
- .net core 时间戳转换
/// <summary> /// 时间戳和日期的转化 /// </summary> public class DateTimeUtility { /// <summar ...
- openwrt ramips随记
ar71xx / brcm47xx / brcm63xx / ramips是指cpu的系列,ramips是指ralink系列的