Swift 3

Swift 3 正式发布已经 3 周了,大家 Swift 项目的代码迁移做的怎么样?Glow Baby 项目我花了近 3 天时间,12956 行增改,9817 行删减,360 个文件。迁移的过程是痛苦的,心很累,Xcode 8 的迁移工具也没有让我轻松多少。

不过待迁移完毕后,Swift 3 读起来、写起来都更舒服。Swift 1 确立了语言的基线:安全、快速、现代。Swift 2 展现了 Swift 应该是什么,未来怎么走:面向协议的编程、开源。而 Swift 3 更多是清扫和规范:新的 API 设计简洁干净,减少了歧义;移除了很多 C 风格语法使代码风格更加一致,可读性更高。总体上说,Swift 3 更加优秀。我们来看看 从语言使用的角度上,Swift 3 到底有哪些改变和新特性。

API 设计指南

先来看看可能是改动行数最多的一个变化:API 的重命名和规范。新的 API 设计强调了 API 调用 时的清晰,因为方法和属性只会声明一次,但反复调用。在保持语义、避免歧义的前提下,尽量简洁,减少冗余。

比如:

避免歧义

方法名中应该要包含所有需要的单词以减少阅读时的困惑,比如从一个 List 中移除指定位置的一个元素:

extension List {
        public mutating func remove(at position: Index) -> Element
}
employees.remove(at: x)

如果省略方法名中的 at,读代码的时候就不明确到底是删掉 x,还是删掉所在的位置 x 的元素。

删除协议名字的 Type 后缀

如原来的 CollectionType 变成 Collection;一些特殊的情况下,加上 Protocol 后缀避免歧义,如原来的 ErrorType 变为 ErrorProtocol

删除冗余的词

如:

Sequence.minElement() => Sequence.min()

UIColor.blackColor() => UIColor.black

根据 Side Effect 来决定命名

Mutating 方法的命名应该是一个动词,而 non-mutating 应该用 ed/ing 形式的词。如 array.sort() 是对 array 本身进行排序,而 array.sorted() 则是返回一个新的 Array。

很多方法的名字都大大缩短,比如原来 stringByAppendingString(aString: String) 变成 appending(_ aString: String)

还有很多条目这里不一一列举,感兴趣的可以官方的 Design Guidelines

引用 Objective-C 和 C 代码

Objective-C Constants

很多 Objective-C Constants 在 Swift 里成了 Enum。比如 HKQuantityTypeIdentifier

enum HKQuantityTypeIdentifier : String {
        case bodyMassIndex
        case bodyFatPercentage
        case height
        case bodyMass
        case leanBodyMass
}

这里顺便提一下,Swift 3 里的 Enum 都是小写字母开头。

C APIs

C 的 APIs 原来都是会全局的方法和变量,现在都引入为成员变量使用。如大家熟悉的 CGContext 在 Swift 3 里:

context.lineWidth = 1
context.strokeColor = CGColor(gray: 0.5, alpha: 1.0)
context.drawPath(mode: .Stroke)

同样 Dispatch 的 APIs 也更加 Swifty:

let queue = DispatchQueue(label: "com.example.imagetransform")
queue.async {
        let smallImage = image.resize(to: rect)
        DispatchQueue.main.async {
                imageView.image = smallImage
        }
}

let delay = DispatchTime.now() + .seconds(60)
queue.after(when: delay) {
        // ...
}

语法

C 风格的语法

自增 ++ 跟 自减 -- 运算符被移除了。

C 风格的 for 循环(for (int i = 0; i < array.count; i++) )也不能使用了。

函数

  • 函数参数中 let 的显式使用被移除了。
  • 函数参数中 var 被移除掉了。
  • inout 调整为类型修饰,而不是参数名修饰:func double(input: inout Int)
  • inout 类似,@noescape@autoclosure 也转变为类型修饰:func foo(f: @noescape () -> ()) {}
  • 移除了 Curry 函数的声明语法。像 func foo(x: Int)(y: Int) -> Int 这种语法应该用 func foo(x: Int) -> (y: Int) -> Int 这种语法代替。

Enum

Enum 有不少改动,最明显的改动是所有的成员都变成了小写。其次,在枚举当中,必须要在成员前加上左前缀点。之前只是在枚举外使用需要加,现在枚举内部对 self 进行 switch,也一定要加,确保代码的一致性。

enum Season {
        case spring
        case summer
        case fall
        case winter
        
        var foo: String {
                switch self {
                case .spring:  return ...
                case .summer: return ...
                ...
                }
        }
}

另一个改动是,有关联值的 enum,在 case 后可以声明多个成员:

enum MyEnum {
        case case1(Int, Float)
        case case2(Float, Int)
}

switch value {
case let .case(x, 2), let .case2(2, x):
        print(x)
case .case1, .case2:
        break
}

访问级别

在 Swift 3 之前,我们有三种访问级别:

  1. 公开(public)
  2. 内部(internal)
  3. 私有(private)

默认的是 internal,表示这个成员只能在 module 内可见。如果要在 module 外使用,就要将其设置为 public。private 表示该私有成员只能够在其所在文件内可用。

在 Swift 3 中,多了一种访问级别 fileprivate。fileprivate 的效果相同于之前的 private,而 private 表示只在其对应的作用域中可见。这让访问级别更加清晰。

Where

Generic 声明

在 generic 声明中,where 语句被移到了最后。Swift 3 之前,我们可能这么声明一个 generic 的方法:

func anyCommonElements<T : SequenceType, U : SequenceType where
        T.Generator.Element: Equatable,
        T.Generator.Element == U.Generator.Element>(lhs: T, _ rhs: U) -> Bool {
    ...
}

Swift 3 中,正确的语法应该是:

func anyCommonElements<T : SequenceType, U : SequenceType>(lhs: T, _ rhs: U) -> Bool
        where
                T.Generator.Element: Equatable,
                T.Generator.Element == U.Generator.Element {
        ...
}

条件判断

where 在条件判断语句中被删掉,取而代之的是逗号。原来这样的一个 guard 判断:

guard let x = opt1, y = opt2 where x == y else {}

在 Swift 3 中应该写成:

guard let x = opt1, y = opt2, x == y else {}

Implicitly Unwrapped Optional

原来隐式解析 Optional 类型没有了,但其语法还在。

let x: Int! = 5
let y = x
let z = x + 0

x 的声明后面有个 !,看起来像个 IUO,但其实是一个 optional 类型。x 赋值给 y,y 是 Int?,因为这里不需要进行强制的解析。但 z 是一个 Int,因为这里需要解析 x 里的值才能进行加法运算。

Tips

在我进行 Swift 3 升级的过程中,遇到不少坑,有的很难测试出来,甚至导致我们需要重新提交一个新的版本。

首先,Enum 的成员从大写变成小写。乍看起来没什么问题,但如果你的代码中有用到 String Raw 类型的 Enum,并且存储了它的 rawValue 的话,你代码升级后再把值取出来可能就有问题。比如 Season Enum 的例子:如果原来你把 Season.Spring.rawValue 存起来,比如 User Defaults 里。现在你再拿出来 Season(rawValue: {springRawValue}),会得到一个 nil,因为新的 rawValue 都是小写。

另外一个是 Json Dictionary 声明的变化,原来 Json Dictionary 的类型通常是 [String: AnyObject]。而在 Swift 3,则是用 [String: Any]。Any 包括 optional 类型,而当你要是用 NSJSONSerialization.dataWithJSONObject 对含有 Optional 类型的 Dictionary 序列化的话,就会有 runtime error 抛出。

以上是印象深刻的两个问题,其他问题 code 不同,可能会有不一样的表现,这里也不一一举例。

总结

升级 Swift 3 的过程虽然痛苦,也难以避免伴随着不少 bug 的出现,但熬过这个之后,Code 看起来写起来都会更加畅快。之后的 Swift 版本不大可能再出现这种规模的迁移。Swift 4 我们可能回看到完整泛型,并发,以及更多有趣,强大的特性。期待。