天,请注意时效性
说明
有人问为什么不是 JavaScript,因为它是脚本语言,没有类型系统等,完全没法儿跟 Swift 比较。
本人前端开发,对 C、Java、Python 语言仅限入门了解水平,而对 TypeScript(以下简称 TS) 的特性和使用达到了高级水平(自认),因此有些让我震惊的点,可能在各位眼中感觉「只不过是平平无奇的语言特性罢了」,亦或者「莫非设计 TypeScript 的人是天才?」。
本人不一定会对所有 Swift 中与 TS 有差异的地方强行震惊,因为有些 Swift 跟 TS 的差异是因为 TS 的自身不足,且有些设计在计算机语言中司空见惯(如数字区分 Int 和 Double 而不是只有一种 Number 类型),只是 JS 的设计让人震惊(毕竟是一周内赶工设计出来的),而不是 Swift。
本人认为:如果一个语言规则太多、特例太多、保留字太多,那就不是一门好语言。
前言
本文按照 Swift 官方语言介绍的顺序进行有序震惊,省略了不震惊或者不懂的的内容,如 附加宏
。
基础知识
注释
震惊点:XCode 没有 `/** */` 块注释快捷键,只有 `cmd + /` 的行注释。
VSCode 的块注释快捷键也比较难按,是 Alt + Shift + A
,难道大家都不常用这个功能?
当然,VSCode 和 XCode 中,都可以通过按 /**
后回车,自动生成块注释。
可选绑定
震惊点:我调试的时候想在 `if` 写一个始终为 `true` 的值测试用都不行,`if` 语句的可选绑定,必须是一个可选类型的值。
1 |
|
有必要这样?
集合类型
震惊点:数组方法中,有个 `sort` 表示原地排序,还有一个 `sorted` 返回新数组。
一个语言,干了框架干的活儿,很好,很符合苹果的风格,诸如此类的还有很多,就不一一震惊了。
控制流
if 表达式
震惊点:为了解决 `if-else` 给同一个变量赋值的情况,它提供了 `if` 表达式的写法,同理,`switch` 也有类似的表达式形式。
Swift 还是做的太多了:
1 |
|
Switch 的牛逼判断
震惊点:switch 没有隐式贯穿
不过这倒可以理解而且更合理,正常人谁没事儿希望 case 1 如果不额外 break
会自动跑到 case 2 中去啊?
震惊点:switch 可以判断对象值!
这其实算是 feature
,而且直觉上更合理,简直内行。
在 TS 中,因为 switch
的 case
语句使用的是全等(===)判断,因此你的 switch
的括号内一般不会传入一个对象,因为对象判断的是引用,而在 TS 中很少有需要判断对象相等的情况,更别提使用 switch
语句来判断了。
但是在 Swift 中,你可以在 case
语句中「捕获」判定的值(正式称呼为「模式」):
1 |
|
需要注意的是,在 TS 中也支持「多个匹配」但跟 Swift 中的 case 多个匹配完全不同。
Swift 中的多个匹配,逗号分割的内容只要有一个匹配,就会执行 case 语句,但是 TS 的逗号分割的匹配,它的本质是只匹配最后一个项,因为 TS 中的逗号分割的语句只返回最后一个值:
1 |
|
函数
函数的标签参数
震惊点:形参(标签参数)可以同名???而且规定(限制又来了,服了):可变参数的后面的参数必须有参数标签(如果只有一个那么实参就是形参)不能省略。
1 |
|
闭包
闭包的N中简写形式
震惊点:闭包的语法糖太多这里就不一一列举,最离谱的是只需要一个 `>` 符号的形式。
可以这么写的本质是 String 有一个叫做 >
的函数,是的你没有看错,符号也可以是函数!这一点下面再震惊。
1 |
|
真的离谱。
省略 return 的返回
震惊点:也许你可以理解「单行返回可以省略 return」 ,但是你绝对无法理解「多行也可以省略 return」
TS 中的省略 return 的语句,跟 Swift 普通的写法一样,单行省略 return:
1 |
|
普通的 Swift 语法:
1 |
|
但是!SwiftUI 中的写法:
1 |
|
你没看错,这也是一个尾随闭包函数,其中 VStack
是一个函数,闭包参数作为最后且唯一的参数,可以省略 VStack
的括号,但是!它返回了两个 Text
函数调用,却没有写 return
,为什么?
因为它用了 ViewBuilder
,这个东西跟后面的 resultBuilder
一样的语法糖。
自动闭包
震惊点:本来一个平平无奇的闭包赋值没有什么,但是它的延迟计算能力让我不得不为之震惊。
刚看自动闭包的时候,是拿这个例子介绍的,说什么「延迟计算」能力:
1 |
|
我寻思这不就是一个平平无奇的闭包,跟 TS 一样只是 customerProvider 变量被赋值给了闭包而已,相同实现在 TS 中是这样的:
1 |
|
然后当然是在它调用的时候才会执行闭包的逻辑,这算什么延迟计算!
但是,它的延迟计算形式在闭包作为参数的时候才是真牛逼:
平平无奇的显式闭包调用,跟 TS 调用方式基本类似,闭包作为参数,写法还是写在一个大括号中:
1 |
|
但是一旦把 customerProvider
标记为 @autoclosuer
情况就不一样了,此时你的 serve
函数调用可以这么写:
1 |
|
注意最后的 serve
函数的调用,它的参数 customer 是一个语句 customersInLine.remove(at: 0)
,在 TS 中,无论什么情况,调用栈都会先求这个值然后再调用 serve 函数。但在 Swift 中,这种写法跟上面的形式仅仅是形式不同,逻辑是一样的,也就是说会先执行 serve
函数,然后再去内部执行这个语句(当 customerProvider
调用的时候)。这才是传说中的,延迟计算
吧。
这种情况如果你写多了,会遇到很多诸如下面的代码:
1 |
|
此时你难以区分,是先执行括号内的语句,还是外层的函数。因此,Swift 官方文档也做了说明:
过度使用自动闭包可能会使您的代码难以理解。上下文和函数名称应明确表示计算正在被推迟。
真的离谱,既然不推荐,就不要设计出来啊喂!
枚举
震惊点: 枚举在其他语言只是为了方便的一种可有可无的用来状态机判断的类型,但是在 Swift 中却是最常用的一等类型,可以在一定功能上取代 Struct 你敢信?
震惊点2: 枚举是值类型。
枚举在 TS 就是一个平平无奇的「枚举」罢了,仅仅是把值给列出来,大多数用来在状态机中描述状态:
1 |
|
当然,TS 整的花活,在运行时、编译时的一些差异就不说了。在 TS 中,你完全可以使用一个对象来代替枚举:
1 |
|
关联值
震惊点: Swift 你这么设计枚举,是要 干!什!么!
在 Swift 中,枚举的重要性是第一位的,你可以遍历它的所有 case(需要遵循 CaseIterable
协议)、可以将 case 视作一个函数,然后在调用的时候传值,以让枚举实例处理,此谓之「关联值」:
1 |
|
然后在用的时候可以传值:
1 |
|
枚举最常用的就是在 switch 语句中,结合震惊的 switch 同样震惊的 case,你可以这么做:
1 |
|
第一次看反正我是觉得挺抽象的,也想不明白有什么实际使用场景(毕竟我没这么干过)。
隐式赋值
震惊点: 枚举的类型声明,声明的不是枚举本身的类型(键值对),而是 case 的类型。
Swift 中的隐式赋值,倒是跟 TS 中或者其他类 C 语言的一致,都是第一个数字是 n,后面的就是 n + 1。
但 Swift 更近一步的,它会根据你声明的类型才隐式赋值,默认是不会的,比如:
1 |
|
TS 中,你甚至不能指定 Swift 中指定的那个 Int 类型(这个类型其实指的是 case 的类型),而只能指定 enum 的类型(一般是 Record
别处的键值对)。
结构体和类
震惊点:结构体居然是值类型???
这个浓眉大眼的结构体,居然是值类型(使用了 Immutable
的那一套东西来优化性能):
1 |
|
带着花括号的东西,怎么看也不像值啊!离谱!震惊!
属性
各种xx属性/包装器/观察器
震惊点:Swift 做的太多了,类似于计算属性、存储属性、属性包装器(TS 也有)、属性观察器(TS 也有,跟包装器一起的)这类概念,一般都是框架带的,比如 Vue 的 `computed` 、`watch` 等,Swift 自己在 Struct 和 类中实现了,离谱!
1 |
|
另:属性包装器对全局变量不可用。
下标
震惊点: 这个没什么好说的,有「下标」这个概念本身就已经让人震惊了,它提供了一种访问集合、列表、字典元素的快捷方式。甚至,下标可以传多个值,就像函数一样。
基本上,下标的调用方式可以认为是对数据结构(上述的集合、列表、字典)的函数调用来访问值,不同的是函数调用使用 ()
,而下标使用 []
。
继承
震惊点: 跟之前所有的震惊点都类似,Swift 为了实现各种目的、效果,似乎很随意的添加各种关键字,因此与继承相关的保留字(当然,也不能叫「保留字」,因为 Swift 中保留字都可以拿来用,使用反引号包裹即可,下面再震惊)多不胜数。如: `final`、`override`、`open`、`required`,等等。
构造过程
Struct 的逐一成员构造器
震惊点: 因为 Struct 是值类型,所以它跟传统意义的 Class 的构造器规则在初始化的时候有所差异。
本来,Struct 和 Class 一样,都是一种平平无常的数据结构,和 TS 中的 Class 没什么不同。但是,Swift 在这里又整出幺蛾子。
因为 Struct 是值类型,因此特殊一点,它有一个叫做「成员逐一构造器」的构造器形式,就是说当一个 Struct 没有任何 init
的时候,它就可以通过传入参数的形式来 init 它的属性,而不用显式写出来。
1 |
|
但是这个规则不适用于 Class,Class 必须显式的拥有 init 构造器方法来初始化属性。
类的指定构造器和便利构造器
震惊点: 什么?构造器还分两种???
类的指定构造器就是跟 TS 中的普通 construct
一样作用的 init
方法。但是它的便利构造器…是在 init 前加个 convenience
关键字(又来!):
1 |
|
其实这里 Swift 文档没有明确告诉读者,为什么需要便利构造器,而是直接讲起了类的构造、继承过程和构造器代理。我在这里给小白门解释一下为什么需要存在便利构造器这个东西的存在,很简单的原因:类中的多个指定构造器不允许互相调用。
就这么简单的原因,按耐不住想调用?想简化初始化过程?用便利构造器去吧你!
常规写法:
1 |
|
以上是不是感觉 self.b 这种的写了两遍,麻烦?所以想在第二个 init()
中,这样干:
1 |
|
抱歉,编译器报错:Designated initializer for 'A' cannot delegate (with 'self.init'); did you mean this to be a convenience initializer?
这种情况,必须使用便利构造器:
1 |
|
图的就是一个「便利」~
可失败构造器
震惊点: 构造器还能构造失败的?
其实所谓的可失败构造器,就是指在构造过程(init 函数调用过程)可能抛错。
因此,如果构造器抛错,你不用像 TS 一样,在外层 try-catch
,而是直接说明即可,方法就是在 init
后面加个问号变成 init?
,然后在可能抛错的地方返回 nil
,在实例化的时候判断是否为 nil
即可。
Swift 中正常构造器不返回值,这跟 TS 一样——但 TS 的构造器可以返回值,如果返回值是对象类型则会替代 this
对象,这似乎更让人震惊。
可选链式调用
震惊点: 问:在什么情况下,函数写了 () 但是却没有执行?答:可选链式调用的场景下。
话不多说,看例子:
1 |
|
错误处理
震惊点:跟枚举一样,Swfit 的 try-catch(实际是 do-catch)设计的很复杂。
catch 语句不但可以捕获任意错误,还可以捕获特定错误类型,使用 is 或者跟 switch 一样,有多个 catch 分支进行进行匹配,离谱:
1 |
|
并发
震惊点:看文档的时候,我一开始并未意识到 `withTaskGroup` 是一个内置的进行 Group Task 的方法。。。
至于 await
跟 TS 中的 await
完全一样的用法,爽!
扩展
震惊点:扩展应该算是 Swift 中最强的设计了,任何对象,无论内置还是三方,都可以随意的、低成本的、无需任何复杂声明任何复杂关键字的,进行扩展。
相比于 TS 中,你想扩展一个已有的类,你需要在原型链上做一些操作,然后再修改对象的 this 指向这个类。
但是在 Swift 中,你只需要一个 extension
然后就可以肆无忌惮的写任何你想写的方法、属性,他们的 this
都指向实例,或者扩展类型(也就是静态)方法、属性。
简直炫酷狂拽屌炸天。
协议
震惊点:我觉得协议的出现是为了解决 Struct 的抽象问题,因为任何语言中,类本身都是可以继承(可能说的绝对了),不需要再多余实现一个「协议」,使用抽象类即可。只是顺带的,Swift 限制了类只能继承一个父类的同时,让协议更多的在类和 Struct 和枚举上同时发挥作用。
不说了,想说的都在上面。
泛型
震惊点:泛型整了个「关联类型」,其实发挥的作用就是声明的时候带的泛型,但是更强大。
Swift 的泛型关联类型是这样子的:
1 |
|
至于为什么不设计成这样:
1 |
|
估计是因为这个 Item 可能会很长,不优雅吧,如:
1 |
|
写到名字的后面的话,那么泛型声明会经常超出屏幕,难顶。
不透明类型和封装协议
震惊点:你可以实现一个效果,即只让编译器知道你的类型是什么,但是调用端不知道具体类型,而只知道它遵守某项协议。
基本上,不透明类型和封装协议想实现的效果是,一方面对调用者隐藏实现细节,一方面可以允许重构代码的时候不用修改太多地方,举个例子:
1 |
|
这里,makeShape
返回的类型,只要遵守 Shape
协议类型的任意类型都可以,不用明确返回 Circle
类型。如此一来,后续添加新的遵守 Shape
类型的 Struct
,该函数都可以返回(这也是 SwiftUI 中所常用的一种方式,比如 some View
等)。
内存安全
震惊点:Swift 因为可以按引用传递值,所以会产生同时对一块内存区域的读写(这很合理对吧?)
1 |
|
上述运行会报错,但是编译器不会有提醒。诸如此类的还有很多,在这种情况下要特别注意。因为 TS 中不存在按引用传值的情况,所以不会有这种问题,easy~
高级运算符
震惊点:你可以重写/自定义实现一个以符号为函数名的函数,以此来实现相同类、结构体的实例之间的相互操作。
举个例子:
1 |
|
这个设计好,我怎么没想到呢?牛逼牛逼。
需要注意的是,运算符方法是有自身属性的,如 +
是二元操作符也是中缀运算符,所以接受两个函数,-
既可以是中缀运算符(二元操作符),也可以是前缀运算符(一元操作符,func
前需要加 prefix
),因此可以重载。
但是 Swift 又规定了赋值运算符=
不能被重载,三元条件运算符(a ? b : c
)也不能。
这个运算符是如此的常见,以至于你可以在任意的 Swift 内置对象/Foundation 对象中,看到诸如 ==
相等判断的运算符函数。
你甚至可以实现自己运算符!
1 |
|
真牛逼。
结果构建器
震惊点:Swift 为了「优雅」、「复用」,无所不用其极。
为了让下面这段代码看上去好看,Swfit 「发明」了 @resultBuilder
这个神奇的东西,如下:
普通写法:
1 |
|
Swift 说,上面的三元判断太麻烦,而且复杂判断的话会很长不好阅读,希望优雅点,于是,结果构建器诞生了(如果我没理解错的话):
1 |
|
然后在上面的 c
变量这么赋值:
1 |
|
复用,优雅,高效,绝!
词法结构
震惊点:尽管 Swift 中的保留字多不胜数,但是它允许你将保留字用作标识符。
方法就是,使用反引号扩住即可:
1 |
|
但除了关键字外,x
和 x 是同一个变量。