我们为什么要使用 Swift


Overview

Swift 是苹果 2014 年推出的语言,首先应用于 mac 和 iOS 平台的开发,并且于 2015 年底开源,推出了 Linux 版本。它集合了许多编程语言最新的研究成果1,并且借鉴了 Objective-CRustHaskellRubyPythonC#CLU 等语言2

Swift 是一个多范式(Multi-paradigm)的编程语言。它基本涵盖了函数式编程(Functional Programming)语言和面向对象编程语言的所有特性。可以进行函数式编程,或者 Protocol-Oriented Programming 等,而不局限于某种特定编程的范式。

Swift 不是一个脚本语言,它的目标成为 C 系语言(C 、C++、ObjC)的代替品。它是一个强类型语言,变量和方法都有明确的类型,并且有比 OC 更严格的编译时静态类型检查。但是因为有类型推导机制,使得可以在很多的时候免去变量类型的声明,写起来更方便简洁。Swift 会使用 LLVM 编译成 native code。Swift 为了保持与 OC 的兼容性,在处理 NS 相关的东西时还依赖于 OC 运行时。

Swift 很注重安全性。它在语法层面上确保编译时可以检测出更多的错误。尽管使用 Swift 编程时可能会有一种约束感,但清晰明确的代码会在更长的时间维度上带来更多的便利。

与 OC 的兼容性

C 与 OC 代码不用任何更改,可以在 Swift 进行桥接调用。C 和 OC 代码里也可以调用部分 Swift 代码(一些 OC 没有的特性就不能被使用)。但 Swift 与 OC 代码不能写在同一个文件中。

全新的语言

第一眼看上去,OC 中有 class, protocol, struct, enum, switch 等语法结构,Swift 中也有同样的结构,一份 OC 代码基本上也可以无脑翻译成 Swift 的版本。似乎除了写法上的不一样之外,Swift 没有什么太不同的地方。但他们只是看似相同。

看似熟悉的概念,在 Swift 中有了更多的特性,甚至脱胎换骨。比如 Switch,除了可以 switch C 中的整数类型,还可以 switch 字符串,或者更加泛化的 pattern-match。而且引入了许多先进的语法概念,使得我们可以利用 Swift 写出更加简洁有力、思路新颖的代码。

Features

下面会介绍一些 Swift 相比于 OC 的新特性,和由此而带来的新的编程思路,并且说明在实际编程中的好处。

基本的现代化语言特性

  • 简洁的 class、extension 和 property 定义方式。 OC 定义一个 class 至少需要 4 行,而 swift 只需要两行。新建类的成本大大降低,可以避免因为书写成本高而没有使用类的情况,而此时新建类通常使代码更加整洁。
  • 命名空间。基于 module (Xcode target) 的命名空间,避免使用类前缀来防止命名冲突这种原始的方式。
  • 函数重载。避免了 numberWithInteger: numberWithDouble 这么啰嗦的命名了。所以 Swift 版 的 Masonry 的 equalTo 方法可以传对象和也可以传基本类型,而不需要使用宏。
  • override 修饰符。避免了无意间重写父类方法。
  • Tuple(元组)。低成本且使用方便的数据集合。它提供了函数多个返回值功能。
  • nested functions。在函数里面定义函数,直接在语法上限定只在局部使用的函数,使代码更清晰简洁。(虽然 OC 能使用 block 来代替实现,但写起来好难看)
  • 简洁的闭包写法。{ a in ... } 比 C/OC 中丑陋的 block 写法好很多。
  • 运算符重载。例如我们定义了一个<|> 运算符,它的输入时两个函数使得 ( f <|> g )(x) 等同于 f(g(x)) 3。这样多重的嵌套 f(g(h(x))) ,就可以写成 (f<|>g<|>h)(z) 这样的线性表达式了。
  • 不用写分号!

全新的枚举和结构体

枚举(Enum)

新的数据类型

C 语言中的枚举类型是等同于整数类型的。在 Swift 中的枚举是一种独立的数据类型,它不可以像整数一样进行运算,在要求为枚举类型的地方传入整数也是不允许的。

Swift 中的枚举可以类似 C 的枚举,有一个 rawValue,但也可以是一个「纯粹」的枚举类型 : 仅仅表达分类的概念,作为一个独立的实体存在,而不依托于其他的类型的数据。

它提供了「 . 」的简写方式,在明确上下文的情况下可以省略前缀。而在 OC 中为了使枚举中的不同情况有明了的表示,都会加一个统一的前缀,使用时会显得很啰嗦。Swift 中枚举可以有初始化方法、也可以添加函数。

Enumeration With Associated Values

枚举类型中可以携带值。一个具体的例子:

class Message {
    enum Content {
        case text (String)
        case image (UIImage, URL?)
        case audio (URL)
    }
    let content: Content 
    let date: Date
    // ...
}

有个 Message 类,它有三种类型,不同类型携带的值是不同的。对于不同的类型会有不同的操作,此时只需要一个 Switch :

switch (message.content) {
case .text(let string):
    print(string)
case .image(let imageObject , let url):
    // imageObject 类型是确定的: UIImage
    print(imageObject, url)
case .audio(let audioUrl) :
    print(audioUrl)
}

如果是 OC,就需要 Message 有一个枚举类型的 type 变量,和用来存储对应 type 的数据的变量。一种方式是设这个变量为 id 类型,使用的时候都需要强制类型转换。这样做不到类型安全,而且对上面 image 的情况不太好处理。另一种方式是设多个 property,这样做使用起来很混乱,不知道哪个变量存在哪个不存在,需要靠约定。而 swift 的处理方式就做到了类型安全和表意清晰。

另外的例子是网络返回的结果,在 OC 中它的参数应该由返回数据、错误信息等组成,在不同的情况下有些值为空。大在 Swift 中可以定义一个 ResultType,它有两种可能 Success 和 Error ,两者分别带有不同的该返回的数据类型(如 Success 中带有一个 JSON(NSDIctionary),而 Error 带有 NSError 对象)。一个二叉树也可以由枚举类型来实现,并且自带「value semantic」:

indirect enum BinaryTree<T>{
    case node(left:BinaryTree, value:T, Right:BinaryTree)
    case blank
}

这种特性使得枚举不只是传统意义上的枚举,大大提高了语言的表达能力。Swift 的枚举还可以实现代数数据类型,这里不再详述。

结构体(Struct)

Swift 中的结构体相比 C 语言的结构体,有许多与 class 相像的功能, 比如可以添加函数(包括 初始化方法 和 calculated property),实现 protocol,添加 extension。如同 C 中的结构体一样,它具有 value semantic,即是一种值传递(pass by value)的数据类型,这是它与 class 最大的区别。

Swift 中字符串、数组和字典均以结构体的形式实现。这意味它们被赋值给新的常量(或变量),或者被传入函数中时,都会以值传递(会发生拷贝行为),而不是引用传递。Swift 的 String、Array、Dictionary 的值传递有额外的编译器优化(copy-on-write),使得在只有在真正必要的时候,才会进行实际的拷贝。值传递的特性有助于 immutability 的实现。在 swift 的代码里,更多倡导使用 struct 而不是 class。

因为这些特性,CGRect 可以直接使用 frame.midX; frame.maxY 等属性(而不需要额外的函数)。也可以使用结构体来整理全局字符串常量,比如

struct PageName { 
	static let Feed = "PageNameFeed"
	static let Notification = "PageNameFeed"
} // 这样就字符串常量就不会凌乱放置了。

let key = PageName.Feed // 写起来很像枚举

函数式编程的特性

Immutability

Imumutability(不可变)是一种特性,具有这种特性的数据(变量、对象)在创建之后就不会再变化,如果想改变这个数据,唯一的办法是生成一份新的数据。一个不可变的数据是不存在「状态(State)」的概念。

比如 let p = CGPoint(x: 1, y:2) 是不可变的,但是 var p2 = CGPoint(x: 1, y:2)(p2 的 x,y 在创建之后都可以再改变)。一个 UIView 对象是可变的(view 的各种属性都可以变,即内部的状态都在变),但 ComponentKit 中一个 component 对象就是不变的 (虽然没有做到绝对不变)

Immutability 的好处

正如 Edsger Dijkstra 在那篇著名的论文 中写到:

… our intellectual powers are rather geared to master static relations and that our powers to visualize processes evolving in time are relatively poorly developed.(我们的智力善于理解静态的关系,但理清随时间演化的过程却是困难的)。

我们不善于处理时间的维度,逻辑过于复杂的时候会容易出现漏洞。不可变的数据,会降低逻辑的复杂性。比如一个不可变的数据 A,在使用的时候,我们只需关心它创建时候的值即可。因为它是不可变的,不用担心在哪一步是被隐性的改变或者改变之后对其他地方有所影响,也不用处理自身复杂的状态。全部是线性的过程,降低了思维的难度。

这引出了另外一个概念: Referentially Transparency4,也称为纯函数,它的结果只与输入参数有关系,即它不会引用外部的数据。纯函数与环境是松耦合的,更具有重用性和容易模块化,设计可靠并且易于测试。它把我们从繁杂的状态中解脱出来,只需要关心函数定义,并用它组合成想要得到的结果,而不用小心翼翼得操作特定的调用顺序。Immutability 可以帮助我们更容易写出 referentially transparenct function,从而达到我们的终极目标,解耦5

Swift 的机制

上面说到的概念,并不是编程语言的特性。但是在不同语言,实践起来的难易程度不同。在 Swift 中,有如下两个机制:

  • let 类型的声明

    它在编译器层面上保证了一个数据的不可变性。

    在 Swift 1.2 之后,声明为 var 但实际没有改变的变量,都会提示 warning,被建议改成 let。

    在 OC 中,虽然可以通过头文件和 readonly 修饰符来保证数据不被外部所改变,但是它无法保证内部不被更改。

  • 大部分的数据类型都是值传递

    Swift 中的 Array、Dictionay、String 都是结构体,传递时会进行拷贝,而不改变原有的值。这与 OC 有很大的区别。

  • 函数多返回值

    函数可以返回一个元组,元组可以包含多个元素,而且每个元素可以有单独命名。纯函数的输出是依赖于返回值的,多返回值可以方便我们构造函数。

虽然由于数据不可变,增加了创建和赋值的数量,但由于编译器获得了更多的信息,可以进行更多的优化。这相当于把复杂的工作交给了更可靠的编译器。

其他

在写代码的时候,如果数据都是不可变的,那么可变的状态怎么表示呢?创建新数据。

一个极端一点的例子,一个 view 要改变 backgroundColor,如果是不可变的,就需要使用新的 backgroundColor 来重新生成这个 view。这在实际的编程中显然是不现实的。为了处理 view 的繁杂的状态变化,ComponentKit 使用 component 代表 view,component 是不可变的,而实际 view 的变化交给了底层来处理。

对于 referentially transparenct function,如果它是一个对象的方法,并且这个方法的目的是改变对象的一个 property,此时应该怎么写呢:

// func removeSpace(_ text:String) -> String 
self.text = "this is a string"
self.text = removeSpace(self.text)

一味地追求不可变性,可能会带来麻烦。而适当地使用可变数据,会带来理解的直观性和灵活性。比如

// with mutable variables
func sum(_ array:[Int]) -> Int {
    var sum = 0
    for n in array {
        sum += n
    }
    return sum
}

// immutable. implemented by recursive
func sum2(_ array:[Int]) -> Int {
    if array.count == 0 {
        return 0
    } else {
        return (array.first ?? 0) + sum2( Array(array[1 ..< array.count]) )
    }
}

“Functions are values”

函数式的关键思想是函数即是值,与结构体、整数、布尔值没有任何区别6

支持函数式编程的语言需要在语言层面上支持「Functions are values」,所谓的函数是头等公民。函数可以传递和赋值。

// 返回一个函数
func inCircleRange(of distance:CGFloat) -> (CGPoint -> Bool) {
    return { p in p.x*p.x + p.y*p.y < distance*distance }
}

func offset(_ f :CGPoint->Bool, byValue:CGVector ) -> CGPoint -> Bool {
    return { p in
        let newP = p + byValue // 假如我们实现了运算符重载
        return f(newP)
    }
}

// inRange 是一个 CGPoint -> Bool 函数. 
// 它代表给定的点是否在以 (5,10) 为原点,半径为 10 的范围内。
let f = offset(inCircleRange(of: 10), byValue: CGVector(dx: 5, dy: 10)) 
let result = f(CGPoint(x:1, 10))  // true

OC 虽然可以使用函数指针和 block 实现,但是 Swift 提供了简洁的闭包写法,并且统一了函数和闭包,提供了一个完整的简洁的「函数即是值」的实现。

好处

这种特性提供的好处即是函数式编程的好处,在这里不再详细描述。一个额外的简单的例子如下:

let views: [UIView] = // ...
views.forEach( self.view.addSubview )

Map, Filter and Reduce

map, filter, reduce 是几个常用高阶函数。所谓高阶函数即函数的函数(函数的参数也是函数类型),这个概念在「函数即是值」的概念面前,没有特别之处。之所以详细叙述,是因为它给列表的操作带来了新思路。

map 正如名称所示,它把一组数据映射成另外一组数据,映射规则是我们传进去的函数。filter 则是根据规则过滤到部分数据,而 reduce 类似于一个「累加」的过程。

let numbers = (1...10).map({ i in
    return i*i
}).filter ({ i in
    return i % 3 == 0
}) // [9, 36, 81]

let result = numbers.reduce(0, combine: { (sum, i) in return sum + i}) // 126

// 生成 10 个 uiview
let views = (0..<10).map { _ in UIView() }
为什么要使用这些函数

其实这些函数没有那么神秘。不考虑优化,这些功能都可以使用简单的循环来实现。比如 map,可以新建一个可变数组,然后遍历输入数据,根据规则生成新的数据,然后加入数组中。循环有更多的灵活性,为什么还要使用这些函数?

使用 map 函数的时候我们只需要一个参数,即映射规则。我们的目的很明确,要把一组数据转换成另外一组数据,我们只需关心映射规则即可。这种方式使得「映射」这个动作变得很纯粹。如果使用循环,我们就需要考虑实现的各种细节,如遍历的方式、新建可变数据容器等。

高阶函数把我们从具体的实现从解脱出来,只关注行为本身。映射的实现变得很简单,映射行为可以作为一个基本操作来看待。从对数组中的单个元素进行操作,变成对整个数组进行操作,行为的抽象层次变高了。我们可以尽情思考要处理的逻辑,而不用考虑繁复的细节。这样,复杂的操作变得更加简单,代码也更清晰易懂。

Swift 的机制
  • Swift 原生对 SequenceType 、CollectionType(数组、字典都符合这个协议) 实现了 map, filter, reduce 函数。除了这三个常用的函数之外,还有许多其他的高阶函数 sort, indexOf, contain, minElement, elementEqual, split
  • 数组支持 nil,使得应用场景更多。
  • 简化的闭包写法和函数与闭包的统一,使得写起来更方便。
实际的例子
// 把一个消息的数组转变成一个名字的字符串
let namesString = messages.map({ m in
    return m.sender.name
}).filter({ name in
    return name.length < 4
}).reduce("", combine: { (sum , name) in
    return sum + name + " "
}) // 当然最后一步的 reduce 使用 array 的 joinWithSeparator 更好

Optional

Optional 是一个全新的概念,我们可以这样推导出 optional。

Swift 要求所有类型都不能为空(这是一个设定),比如 OC 中的 UIView *v = nil ,在 swift 是不允许的,因为它违反了所有类型的值不能为空的设定,会得到一个编译错误。当然,我们仍然需要表达「一个变量的值为空」的概念。有值和无值,是两种类别,枚举似乎是用来做这个事情的,我们定义如下结构:

Optional<T> {
  case some<T>
  case none
}

当要表示一个可以为空的 UIView 类,我们可以这样写 :

let v: Optional<UIView> = .none

即用一个 Optional 的枚举类型来表示。它可能有值 .some (并把真实的数据作为携带值保存在枚举中),也可能为空 .none。这样我们就解决了,在前面的设定下表示数据为空的概念。

而 Swift 的 optional 机制,即为 Optional 枚举类型的语法糖。下面一句完全等同于上面的代码:

let v: UIView? = nil 

所以,可以很明显地看出 UIView?UIView 是两种不同的数据类型。

Optional 有什么好处?
  • 更严格的编译检查
  • 增加了表达能力,更好得区分出「无」与「空」的概念。

optional 机制使得编译器可以在编译时对 nil 进行检查。如果一个函数的参数是非 optional 类型(比如 UIView ), 在使用时显式传入 nil(或者 UIVIew? 类型),会在编译过程中提示错误。正如静态语言相比动态语言可以在编译时发现类型错误,有了 optional,Swift 相比 OC 可以在编译时发现更多的错误。

在 OC 中,我们无法保证变量为非空,在函数的实现中就必须对参数做 nil 检查。有些时候,我们可以确定该场景中不会出现 nil,检查就会变得多余。但在 Swift 如果变量是非 optional 的类型,编译器会保证它非空,可以直接放心使用7。相反,一个 optional 类型的变量,由于我们显式地写出了它可能为空,在使用的时候就需要强制做 nil 检查 ( if-let?)。这从语言自身的角度,引导了使用者书写更规范的代码。

有了 Optional ,swift 中所有类型都可以为空,最显著的变化是基本类型也可以为空。这样数组的 ` indexOf: 函数,未找到的结果可以用 nil 表示,而不是用 -1 ,表达更加清晰。同样,枚举也可以用 nil 表示无值,而不需要专门设置一种 Undetermined 的 case。一个 Bool? 可以表示 真/假/无` 的概念,使得语言的表达能力更强。

使用的注解

在实际使用中,一个变量是否要设为 optional 类型,并且是用还是 ?应该优先使用非 optional (它提供了最多的信息),其次是 a?,而 a!要尽量避免使用。使用 a!意为着我们主动放弃编译检查。

其他

同 optional 一起,Swift 带来了许多语法糖。

? 语法糖提供了对于方法存在的检查,a.doSometing?() 先检查 a 是否可以响应 doSomething 方法,如果可以则执行,否则什么也不做。它可以简化 responseToSelector 的写法。

if-letas? 一起可以使下面的代码得到简化。

if ([a.class isKinkOfClass:[Message class]]) {
    Message *m = (Message *)a; // 存在强制类型转换
    if ([m.media.class isKinkOfClass:[UIImage class]]) {
        UIImage *image = (UIImage *)m.media;
        // do something with image
	}
}
if let message = a as? Message,
   let image = message.media as? UIImage {
    // do something with image
}

泛型 Generic

泛型,简单地理解,是把函数(或类、结构体等)中的类型作为参数提出来,在使用时指定。最简单的如:

func max<T: Comparable>(A:T, B:T) {
  return A > B ? A : B // 其中 `>` 是 `Comparable` protocol 中限定的方法
}
泛型的好处

泛型在确保了类型安全的同时,又保有灵活性。类型安全可以在编译时检查出更多的问题,而灵活性意味着更好的代码重用。

在 OC 中,面对一个函数的参数可能是多种类型的情况下,我们会使用公共父类,甚至是 id 类型。或者使用 id\<protocol\> 的方式,但很多情况没有相应的 protocol 存在,比如 map 函数的场景。这样我们就无法得知函数参数的具体类型,使用的时候就需要进行强制类型转换。泛型解决了这个问题。比如 Swift 中的 map 函数,我们在函数参数的 block 中可以准确地知道操作数据的类型,而又不需要针对每个类都实现一个 map 函数。

另一个例子, 可以使得在子类的 bindObject 函数中获得准确的类型。

class Message { var content: String?}
class AudioMessage: Message { }

class MessageCell<T: Message> :UITableViewCell{
    let label = UILabel()
    // 子类会重写
    func bindObject(_ object: T) {
        self.object = object
        self.label.text = object.content
    }
}

class AudioMessageCell: MessageCell<AudioMessage> {
    override func bindObject(_ object: AudioMessage) {
        super.bindObject(object)
        // the implementation
    }
}

模式匹配 Pattern Match

新的 Switch 语句

Swift 对 Switch 语句进行了升级,变得更加现代化。 每个 case 执行后默认结束 Switch 语句,不需要写冗余的 break 关键字。对于枚举的匹配可以在编译时检测出未覆盖的情况,而不用写 default 。Switch 的匹配的内容也不只限于整数,而可以传入任何类型,甚至更加复杂的模式。

什么是模式匹配

模式匹配可以理解为对表达式的抽象语法树的某些特点进行匹配,然后处理匹配结果的过程。这里的特点即为模式(parttern),可以理解为广义的「类型」。用 Switch 实现模式匹配的例子 :

switch A {
   case "hello world": // 具体的值
   case is Dictionary: // 一种类型
   case let d as Int?: // 一种类型
   case (2 ,_ , _): // 一种类型(三个元素的元组),并规定了部分的值
   case (2 ,_ , x) where x > 0: // 一种类型,并规定了第一个元素的值,和另外一个值的范围
   case .Audio(let url): // 枚举(有一个携带值的枚举)
   case let .Image(_?, type, url) where type == .Jpg: // 枚举,携带多值,并有条件限制
   case 1...10 : // 表达式
   case _ : // 任意值
}
// 实际上上面的语句是不能通过编译的,因为 A 的类型已知,编译器可以知道 A 一定不符合某些 case,直接在编译时抛出错误。
Swift 的机制

Swift 中的模式匹配主要由 switch 语句实现。匹配的模式的种类大致如上面代码所示。其中对于枚举的支持最佳。不同于 Scala 和 Clojure,Swift 不能对集合类型使用(比如数组)模式匹配,不能匹配如「第一个元素是整数的数组」这样的模式。

另外,还有 for case if/guard case 语句来使用模式匹配。这种语句中只能对枚举类型记性匹配,实际使用如同 switch 中的一个条目。

更详细的 Swift 模式匹配请看这里

例子与好处

模式匹配的一个好处是可以简化 if 分支的多重嵌套,书写更简洁,并且表意更清楚。同样是提高了抽象层次,可以直接目的而尽量少地考虑实现细节。

switch aGrade {
case 90...100: print("A")
case 80...90: print("B")
case 70...80: print("C")
case 60...70: print("D")
case 0...60: print("F")
default:
    print("Incorrect Grade")
}
switch (item["type"], item["department"], item["age"], item["name"]) {
    case let (sys as String, dep as String, age as Int, name as [String]) 
         where  age < 1980 && sys == "system":
         // 在一个语句中判断了四个变量的类型
         createUser(name, age)
    default:()
}
for url in enumerator {
    switch (url.pathComponents, url.pathExtension) {
    case (let f, "png") where f.contains("@3x") :
        // 目录中含有 `@3x` 的 png 文件
    case (let f, "png") where f.contains("@2x"): print(url)
        // 目录中含有 `@2x` 的 png 文件
    case (let f, "jpg"): 
        // 任意的 “jpeg“文件
    default: ()
    }
}

Protocol Extension

Protocol extension 是 Swift 2.0 中引入的机制。顾名思义,它可以为 Protocol 添加扩展,如同往常我们对类的操作一样。

protocol flyable {
    func fly() 
}
extension flyable {
    func fly() { // the implementation }
}
// useage
class Charizard: flyable {}

它给予了 protocol 一个默认的实现。实现 flyable 的类(或结构体等),会有默认实现的 fly 方法。Charizard 也可以重写该方法,调用时会被优先使用。

extension 也可以添加限定条件,使得只有特定情况实现 protocol 才能获得默认实现。

extension ForceTouchImagePreviewProtocol where Self: UIView  {
  // 只针对 UIView 的默认实现。在这个实现里,self 为 UIView 类型,可以得到相应参数和方法。
}
extension Array where Element :_ArrayType, Element.Generator.Element :Equatable  {
  // 对 数组的数组 类型的扩展,且数组中的元素是 Equatable
  // 其实 Array 不是 protocol。但可以看出对于 extension 的使用,已经不需要区分是 protocol 还是 class, struct, enum
}
Protocol extension 的好处

提高代码复用率。Array Dictionary 等类型都实现了 CollectionType protocol, CollectionType 中声明了 map 函数。如果没有 protocol extension,Array Dictionary在声明实现了 CollectionType 外,需要各自实现 map 函数,这会造成大量重复的代码,无法维护。而使用 protocol extension 只需为 CollectionType 实现一次即可。所以,Swift 1.x 中 map 方法是作为全局函数实现的,而 2.x 中是 CollectionType 的一个方法(进而可以用「.」的方式写在后面,并且利于链式调用,[1,2].map(...).filter(...))。

它强化了 protocol programming,使用组合代替继承。与继承共同父类相比,protocol 是一种更「组合化」的实现方式。但由于传统 protocol 只声明接口,实际使用时会存在上一段所描述的问题。protocol extension 会减小 protocol 的使用难度,进而促进了「组合化」。与 C# 中的 Abstract Class 对比,两者都能实现声明接口和默认实现的功能,但后者更灵活,可以服从多个 protocol。这种特点可以促进 protocol 变得更加细化和准确,进而可以组成更健壮且有灵活性的类型。

额外的一个功能: 它使得在纯 Swift 的范围内实现可选的 protocol。Swift 的 protocol 在使用的时候需要全部实现,如果想添加可选的方法,需要使用 @objc 修饰符。如果使用 protocol extension,就可以把可选的方法给予一个空的实现就可以了。(是否优于前者的方式需要再思考。)

实际例子

有一个实现搜索功能(搜索去重、延迟、loading indicator 显示控制)的组件叫 SearchContoller。它需要使用者实现一个 protocol:

protocol SearchControllerDelegate: class {
    func searchControllerWantToShowLoadingIndicator(_ show:Bool)

    func searchControllerWantToSearch(
        _ text:String,
        completion: (_ thisRequset:NetworkRequest, _ result:SearchResult)->Void
        ) -> NetworkRequest

    func searchControllerWantToShow(result:SearchResult)
    func searchControllerWantToShowInitialState()
}
// enum SearchResult {
//     case success(data: Any?)
//     case failure(error: Error, errorMessage: String)
// }

如果我们通常使用 BaseTableViewController 作为结果展示,而它有默认的 loading indicator,我们可以为 SearchControllerDelegate 的一个方法做一个默认的实现 :

extension SearchControllerDelegate where Self: BaseTableViewController {
	func searchControllerWantToShowLoadingIndicator(_ show:Bool) {
        if show {
            self.loadingEffectManager.showLoadingAnimation()
        } else {
            self.loadingEffectManager.stopLoadingAnimation()
        }
    }
    // 如果 BaseTableViewController 有更多的东西,我们甚至可以把所有方法都实现了。
}

从而对于 BaseTableViewController 极其子类来讲,SearchControllerDelegate 有了默认的实现,如果没有特殊要求,使用时只需声明服从协议即可。这样实现使得 SearchContoller 的接口很通用,但具体使用时又很方便。

严格的初始化方法

对于 OC,类的初始化方法是不安全8 :

  • 无法保证初始化方法只调用一次
  • 无法保证所有变量都被初始化(完整性)。
Swift 的机制

为了解决这个问题,Swift 初始化的方法有着严格的要求 : 引入了 designated initializer 和 convenience initializer 的概念,并强调了两者的依赖关系。

不加修饰符的初始化方法默认为 designated initializer(DI)。它要求所有变量都被初始化,否则会有编译错误。DI 的数量可以不唯一。子类的 DI 必须调用父类的某一个 DI。初始化的路径非常清晰,也保证了完整性,解决了上述两个问题。

convenience 修饰的初始化方法为 convenience initializer(CI)。如同名称所示,CI 的作用是为了方便使用,是「二等公民」,作为辅助的作用而存在。CI 不强调初始化好所有变量。为了不破坏 DI 所营造的清晰完整的初始化路径,CI 必须调用自己类中的 DI 或另一个 CI。

如果子类添加了需要在初始化方法中初始化的变量,则父类的 DI 和 CI 都不能直接使用。

一个例子,`init()` 不能在 RecipIngredient 中使用

相对 OC 的原始的初始化方法,Swift 在编译器的角度保证了初始化方法的安全性。

调用顺序

Swift 中的初始化方法强调了调用顺序。对于 OC 来说这是一个奇怪的顺序 :

class AA {
    let a:Int
    let foo:String = "Hello"
    init(a:Int){  self.a = a  }
}

class BB : AA {
    let b:Int
    init(a:Int , b:Int) {
    	// 1 新添加的变量需要在 init 之前初始化好
        self.b = b     
        // 2 调用 init
        super.init(a: a)
        // 3 修改父类中的变量
        self.foo = "hello world"
    }
    
    convenience init() {
        self.init(a: 1, b: 2)  // convenience 中需要先调 init
        self.foo = "God in his heavn, all right with the world"
    }
}

super.init 之前初始化好新增的变量仍然是为了安全性。比如,父类有一个方法 A() ,在初始化方法中被调用。子类重写了这个方法,并使用了新增的变量。如果在初始化方法中先调用 super.init ,就会使用到未初始化的变量。但这种方式也有不方便的时候,比如新增变量的初始化依赖于父类的某个变量9

其他

支持初始化方法返回 optional 类型,使用 init?() 即可。

required 关键字可以强制子类重写该初始化方法。比如 initWithCoder,在 UIView 的子类中必须重写,以保证在 xib 中可以正常使用。如果不想实现该方法,需要显式在其中调用 fatalError

Swift 可以直接在声明实例变量的地方给予它初始值,使用的时候非常方便,不需要再重写 init 方法。对于复杂的默认值,可以通过执行闭包来赋值。它也提供了 lazy 修饰符,使得实例变量的 lazy loading 变得更加方便实现。

class ViewController {
  	var useDarkTheme = true
  	lazy var inidicatorView: UIActivityIndicatorView = {[weak self] in let v = UIActivityIndicatorView(); self?.view.addSubview(v); return v }()
}

其他

try / catch, guard 修饰符

Swift 2.0 开始支持 try / catch 式的错误处理。它的好处是使得正常逻辑与错误处理逻辑分离开,使得代码更清晰易懂。

guard 修饰符其实与 if 没有本质的区别,但是使得语义更通顺。并且在 guard else 中有对提前返回有编译检查,防止了使用 if 做排除条件,却没有提前返回的问题。

类型推导

在能有足够信息推导出来类型的时候,变量、闭包等都不需要手动写明类型。在保证类型安全的同时,使得代码简介,且方便书写。

元组的一个好处
let views = [
    (v1, rect1, color1), // 可兼容 nil
    (v2, rect2, color2), 
    (v3, rect3, color3),
    ...
]
for (v, frame, color) in views {
    v.frame = frame
    v.backgroundColor = color
}
playground

playground 虽然不太适用于常规开发,但是做一些 demo 尝试却很方便。尤其是使用 liveView 快速开发简单 view,而不用运行整个程序10

import XCPlayground

let v = UIView(frame: CGRect(x: 0, y: 10, width: 100, height: 100))
XCPlaygroundPage.currentPage.liveView = v
v.backgroundColor = .red

Furture

WWDC 2016 的 session <What’s new in Swift> 中讲到11,Dock 的项目采用了 Swift。Dock 大概有 20 万行代码,其中 swift 有 2.5 万行。使用 Swift 进行部分重构,代码相比重构的部分减少 15%,同时又添加了额外的功能。

Swift 是一个快速发展的语言。它于 2014 年 6 月推出,9 月发布 1.0 版本,2015 年 4 月发布 1.2,是第一个比较稳定的版本。2.0 于 6 月推出,带来许多新变化,并于 2016 年 3 月推出 2.2。早期的版本存在很多问题,比如编译时间长、编译器有 bug,随着版本的更新已经改进许多,但仍没有达到一个很好的状态。

Swift 3 于 2016 9 月推出,有很多大改动,比如 Foundation 中的类去掉 NS 前缀12,并实现了许多原生的 Swift 类型。新的 API 设计准则,改变了从 OC 以来惯有的命名方式,更加清晰简洁。在实际使用中,几乎不会遇到 2.x 中偶尔会碰到的编译器问题。在笔者看来,3.0 版本标识着 swift 的成熟。它为实际 coding 带来了新的思想,更安全,更清晰。无需犹豫。

update for swift 3, 3-22-2017


  1. Swift is the result of the latest research on programming languages. https://developer.apple.com/swift/ 

  2. drawing ideas from Objective-C, Rust, Haskell, Ruby, Python, C#, CLU, and far too many others to list. http://www.nondot.org/sabre/ 

  3. func <|> <A,B,C> (a:(B)->C , b:(A)->B ) -> A->C { return { v in a( b(v) ) } } 

  4. https://en.wikipedia.org/wiki/Referential_transparency 

  5. chapter 7 of Functional Programming in Swift, Chris Eidhof, F.Kugler, W. Swierstra 

  6. chapter 2 of Functional Programming in Swift, Chris Eidhof, F.Kugler, W. Swierstra 

  7. 如果真的在运行时传入了一个 nil,会在调用处出错,函数内部仍没有问题。 

  8. http://swifter.tips/init-keywords/ 

  9. 这篇文章详细得叙述了这个问题,http://blog.scottlogic.com/2014/11/20/swift-initialisation.html。感觉还没有一个好的解决方案。个人倾向于用 var +  

  10. https://developer.apple.com/library/ios/recipes/Playground_Help/AddingLivesViews/AddingLivesViews.html 

  11. https://developer.apple.com/videos/play/wwdc2016-402/?time=318 

  12. https://github.com/apple/swift-evolution/blob/master/proposals/0086-drop-foundation-ns.md