Swift 烧脑体操一

2023-10-17 22:50
文章标签 swift 烧脑 体操

本文主要是介绍Swift 烧脑体操一,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

Swift 烧脑体操(一) - Optional 的嵌套

前言

Swift 其实比 Objective-C 复杂很多,相对于出生于上世纪 80 年代的 Objective-C 来说,Swift 融入了大量新特性。这也使得我们学习掌握这门语言变得相对来说更加困难。不过一切都是值得的,Swift 相比 Objective-C,写出来的程序更安全、更简洁,最终能够提高我们的工作效率和质量。

Swift 相关的学习资料已经很多,我想从另外一个角度来介绍它的一些特性,我把这个角度叫做「烧脑体操」。什么意思呢?就是我们专门挑一些比较费脑子的语言细节来学习。通过「烧脑」地思考,来达到对 Swift 语言的更加深入的理解。

这是本体操的第一节,练习前请做好准备运动,保持头脑清醒。

准备运动:Optional 的介绍

王巍的《Swifter》(http://swifter.tips/buy)一书中,介绍了一个有用的命令:在 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 Nonecase 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??? = nilif let _ = c {print("c is not none")
}

解释

在我看来,这个问题的根源是:一个 Optional 类型的变量可以接受一个非 Optional 的值。拿上面的代码举例,a 的类型是 Int?,b 的类型是 Int??,但是 a 的值却可以赋值给 b。所以,变量 b(类型为 Int??),它可以接受以下几种类型的赋值:

  1. nil 类型

  2. Int? 类型

  3. 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 Nonecase Some(Wrapped)@available(*, unavailable, renamed="Wrapped")public typealias T = Wrapped/// Construct a `nil` instance.@_transparentpublic init() { self = .None }/// Construct a non-`nil` instance that stores `some`.@_transparentpublic init(_ some: Wrapped) { self = .Some(some) }}

以上代码中,Optional 提供了两种构造函数,完成了刚刚提到的类型转换工作。

烧脑体操

好了,说了这么多,我们下面开始烧脑了,以下代码来自傅若愚(https://github.com/lingoer)在不久前 Swift 大会(http://atswift.io/#speaker)上的一段分享:

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,于是在执行时就会按以下的步骤来进行:

  1. 我们传递一个值为 Optional<String>.None,类型为 String? 的参数。

  2. 因为传的参数类型是 String?,而函数需要的是 String??,所以会执行 Optional 的构造函数,构造一个两层的 Optional。

  3. 这个两层 Optional 的值为 Optional.Some(<Optional<String>.None>)

  4. 进入到 Dictionary 的实现时,会用 if let 进行是否为 nil 的判断,因为两层的 Optional,所以 if let 判断它不是 nil。

  5. 所以代码执行到 _variantStorage.updateValue(x, forKey: key),把 Optional

    .None 当成值,设置给了相应的 key。

如果你没理解,可以再翻翻最初我们对多层嵌套 nil 变量的实验和分析。

我们再看看传递参数是 Optional<Optional<String>>.None 的情况,步骤如下:

  1. 我们传递一个值为 Optional<Optional<String>>.None,类型为 String?? 的参数。

  2. 因为参数类型是 String??,函数需要的类型也是 String??,所以参数不经变换,直接进入函数调用中。

  3. 这个时候参数的值不变,还是 Optional<Optional<String>>.None

  4. 进入到 Dictionary 的实现时,会用 if let 进行是否为 nil 的判断,Optional<Optional<String>>.None 用 if let 判断,得到它是 nil。

  5. 所以代码执行到 removeValueForKey(key),Dictionary 删除了相应的 key-value 键值对。

总结

好了,「烧脑体操」第一节就做完了,运动一下是不是感觉神清气爽?

总结一下本次烧脑锻炼到的脑细胞:

  • Optional 可以多层嵌套。

  • 因为 Optional 的构造函数支持,所以可以将一个类型为 T 的值,赋值给一个类型为 T? 的变量。

  • 因为 Optional 的构造函数支持,所以可以将 nil 赋值给一个任意嵌套层数的 Optional 变量。

  • 将 Optional 嵌套的内容是 nil 时,大家要小心 if let 操作失效问题。

  • 多层 Optional 嵌套容易烧脑细胞,尽量避免在工程中使用或触发。

  • 遇到问题可以翻翻苹果在 Github 开源的 Swift 源码。

愿大家玩得开心!

转载于:https://www.cnblogs.com/LiLihongqiang/p/5636270.html

这篇关于Swift 烧脑体操一的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/228482

相关文章

Swift知识点---RxSwift学习

1. 什么是RxSwift RxSwift是Swift函数响应式编程的一个开源库,由Github的ReactiveX组织开发、维护 RxSwift的目的是:让数据/事件流 和 异步任务能够更方便的序列化处理,能够使用Swift进行响应式编程 RxSwift本质上还是观察者模式,并且是一个响应式的,并且可以序列化的 观察者模式 观察者模式包括:KVO、通知等 爸爸妈妈照看观察宝宝 其中,

王立平-- Swift

Swift是供iOS和OS X应用编程的新编程语言,基于C和Objective-C,而却没有C的一些兼容约束。Swift采用了安全的编程模式和添加现代的功能来是的编程更加简单、灵活和有趣。界面则基于广受人民群众爱戴的Cocoa和Cocoa Touch框架,展示了软件开发的新方向。     Swift已经存在了多年。Apple基于已有的编译器、调试器、框架作为其基础架构。通过ARC(Au

The `XXXUITests [Debug]` target overrides the `ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES` build......

出现的警告: [!] The `ColorInHeartUITests [Debug]` target overrides the `ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES` build setting defined in `Pods/Target Support Files/Pods-ColorInHeart-ColorInHeartUITests/Po

swift快捷键大全

1. 文件 CMD + N: 新文件 CMD + SHIFT + N: 新项目 CMD + O: 打开 CMD + S: 保存 CMD + SHIFT + S: 另存为 CMD + W: 关闭窗口 CMD + SHIFT + W: 关闭文件 2. 编辑 CMD + [: 左缩进 CMD + ]: 右缩进 CMD + CTRL + LEFT: 折叠 CMD + CTRL + RIGHT:

自学Swift之打印输出

所有编程语言都有的函数打印输出: print()         // 不换行输出 println()      // 换行输出       print 和 println 函数可以直接打印基本类型 var num = 10 println("hello " + (String)num)   // 方式1: 其他类型同String输出的时候需要强转 printl

Swift项目#if DEBUG不生效

在新建的iOS项目中 代码里使用#if DEBUG不生效 解决办法: 在Xcode中给项目添加Debug标识符 点击“Build Setting”搜素Swift Compiler - CustomFlags打开Active Compilation Condition添加Debug标记为“DEBUG”

Swift 3.0 学习 -- 大写和小写字符串(Uppercase and Lowercase Strings)

在swift2.0的时候,您可以通过字符串的uppercaseString和lowercaseString属性来访问大写/小写版本的字符串。如下:

Swift 3.0 学习 -- 计算字符数量 (Counting Characters)

在swift2.0的时候,可以通过调用全局countElements函数,并将字符串作为参数进行传递,可以获取该字符串的字符数量。如下: let unusualMenagerie = "Koala, Snail, Penguin, Dromedary"print("unusualMenagerie has \(countElements(unusualMenagerie)) charact

swift 中“?”和“!”区别以及相关用法

最近也在学习swift的只是,已经是学完了闭包的用法,但回头想想之前学的一些知识感觉又望的差不多了。现在自己总结一些swift中?、!和as的相关用法。废话不多说了直接来吧! 一、swift语言使用var定义变量,但和别的语言不同,swift里不会自动给变量赋初始值,也就是说变量不会有默认值,所以要求使用变量之前必须要对其初始化。如果在使用之前不进行初始化的就会报错:           其

Module compiled with Swift 版本号 cannot be imported by the Swift 版本号 compiler

今天把xcode升级到12.5最新版,然后编译出现下面错误。。。记录下 XCTestWD:Module compiled with Swift 5.3.1 cannot be imported by the Swift 5.4.2 compiler: /Users/liyinchi/TestTool/node-v12.18.3-darwin-x64/lib/node_modules/app-in