最近买了本王巍写的《Swifter 100个Swift2开发必备Tip》,之前看介绍以及看了此书序言后,确定此书应该是本不错的涉及Swift开发技巧的学习材料,故决定以此书讲述内容为题材,把自己对这些知识点的掌握以及由此引申出的知识及开发技巧归纳总结,一来便于自己巩固,二来也希望能帮到更多的喜欢Swift的开发者。
柯里化(Currying)
相信很多朋友都跟我一样,第一次听到这个名词,查了下关于该术语的解释:
在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。这个技术由 Christopher Strachey 以逻辑学家 Haskell Curry 命名的,尽管它是 Moses Schnfinkel 和 Gottlob Frege 发明的。
举个Swift中柯里化的实例,如下:
func addTwoNumbers(num: Int)(another: Int) -> Int {
return num + another
}
let addThree = addTwoNumbers(3)
let result = addThree(another: 6) //result is 9
从这个例子大家应该可以了解到柯里化在Swift中的用法,那么这个技巧在实际开发中有什么用呢?作者给我们举了个例子,就是在Swift中Selector只能通过字符串来生成,这个在编译期间是无法检查的,那为了控制风险,可以采用Ole Begemann提出的利用柯里化来对Target-Action模式进行封装,从而可以不用Selector方式来达到目的。
相信很多朋友和我一样,对这里所述内容不是很清楚,很有可能是因为大家接触Swift都相对比较晚,这时的Swift版本已经是2.2以后了。好,我就给大家讲一下Swift语言在这个知识点上的变化。在Swift2.2之前,Selector需要传入字符串,并且没有自动补全,这样开发者在输入时很容易出错。
let button = UIButton(type: .System)
button.addTarget(self, action: Selector(“buttonTapped:”), forControlEvents: .TouchUpInside)
...
func buttonTapped(sender: UIButton) { }
从Swift2.2开始,Selector写法更加安全了,有了自动补全,可以在编译阶段进行检查。写法如下:
button.addTarget(self, action: #selector(ViewController.buttonTapped(_:)), forControlEvents: .TouchUpInside)
好了,了解了Swift语言Selector语法的变化,我们再回来看Ole Begemann利用柯里化来对Target-Action模式进行的封装。首先给出了Swift中实例方法的本质
An instance method in Swift is just a type method that takes the instance as an argument and returns a function which will then be applied to the instance.
简单翻译就是:Swift中的实例方法就是一个类型方法,它以实例作为方法的第一个参数,并返回一个应用于该实例的函数。对这个概念也举了个例子如下:
class BankAccount {
var balance: Double = 0.0
func deposit(amount: Double) {
balance += amount
}
}
普通青年用法:
let account = BankAccount()
account.deposit(100) // balance is now 100
文艺青年用法:
let depositor = BankAccount.deposit
depositor(account)(100) // balance is now 200
depositor常量可以看做是类似C语言中实例方法的函数指针,也可以看做
let depositor: BankAccount -> (Double) -> ()
也就是说,这个函数有一个参数,就是BankAccount的实例,返回值为另一个函数,该函数接受一个Double类型的参数,没有返回值。
BankAccount.deposit(account)(100) // balance is now 300
我们如果连起来,再回头看开头介绍的实例方法的概念,就应该理解的更清楚了。
好明白了概念,我们再来看它是如何不依赖OC的消息分发机制(Selector方式),而用纯Swift的方式实现Target-Action模式的。
protocol TargetAction {
func performAction()
}
struct TargetActionWrapper<T: AnyObject> : TargetAction {
weak var target: T?
let action: (T) -> () -> ()
func performAction() -> () {
if let t = target {
action(t)()
}
}
}
enum ControlEvent {
case TouchUpInside
case ValueChanged
// ...
}
class Control {
var actions = [ControlEvent: TargetAction]()
func setTarget<T: AnyObject>(target: T, action: (T) -> () -> (), controlEvent: ControlEvent) {
actions[controlEvent] = TargetActionWrapper(target: target, action: action)
}
func removeTargetForControlEvent(controlEvent: ControlEvent) {
actions[controlEvent] = nil
}
func performActionForControlEvent(controlEvent: ControlEvent) {
actions[controlEvent]?.performAction()
}
}
具体用法如下,在这里就不多介绍了。
class MyViewController {
let button = Control()
func viewDidLoad() {
button.setTarget(self, action: MyViewController.onButtonTap, controlEvent: .TouchUpInside)
}
func onButtonTap() {
println("Button was tapped")
}
}
好了,介绍了这么多,大家应该对柯里化理解的比较深刻了,如果你准备利用这个特点在实际项目中大展拳脚,那我还是要给你泼一盆冷水。呵呵。原因如下:
Curried function declaration syntax func foo(x: Int)(y: Int) is of limited usefulness and creates a lot of language and implementation complexity. We should remove it.
在Swift3中柯里化特性已经被移除了,原因是用处有限且增加了语言实现的复杂性。不过大家学习柯里化这个概念还是值得的,说不定在今后的工作中还会遇到。
将protocol的方法声明为mutating
这个Tip说实在没有什么值得说的,看过Swift基本语法应该都知道,在protocol中声明的方法,class,struct,enum都可以实现,但是由于struct和enum是值类型,那么如果在方法中涉及成员变量修改的话,方法就需要被声明为mutating。而对于class来说,因为它是引用类型,无论是否修改其成员都不需要将方法声明为mutating,所以mutating对class来说相当于透明的。既然我们不知道定义的protocol方法将会被谁实现,为了避免错误,我们可以将protocol的方法都声明为mutating。
Sequence
Swift中的for...in语法可以用在所有实现了SequenceType的类型上,那么SequenceType是如何实现的,我们还是看下类型声明。首先我们可以看到
public protocol SequenceType {...}
SequenceType被声明为一个public的protocol,其中定义了一些方法,为了能更好的理解protocol的定义,我们先来熟悉一些会看到的编译器指令。
- @warn_unused_result:表示如果没有检查或者使用该方法的返回值,编译器就会报警告。
- @noescape:用在函数的闭包参数上,意味着这个参数是唯一可被调用的(或者用在函数调用时以参数的方式出现),其意思是它的生命周期比函数调用的周期短,这有助于一些小小的性能优化,但最重要的是它屏蔽了闭包中对self.的需求。这使得函数的控制流比其他更加透明。
- throws:可以抛出一个错误的函数或方法必需使用 throws 关键字标记。这些函数和方法被称为抛出异常函数(throwing functions)和抛出异常方法(throwing methods)。
- rethrows:一个函数或方法可以使用 rethrows 关键字来声明,从而表明仅当这个函数或方法的一个函数参数抛出错误时这个函数或方法才抛出错误。这些函数和方法被称为重抛出异常函数(rethrowing functions)和重抛出异常方法(rethrowing methods)。重抛出异常函数或方法必需有至少一个抛出异常函数参数。
好,了解了这些编译器指令后,我们来把主要精力放在protocol具体声明上。首先声明如下:
associatedtype Generator : GeneratorType
从Swift2.2之后,protocol声明中会把typealias改为associatedtype关键字。原因是在Swift2.2之前,都用typealias,它主要有两种用法:
- 为一个已经存在的类型取个别名
- 在协议中作为一个类型的占位名称
举个例子:
protocal port {
typealias Container: SequenceType
typealias Element: Container.Generator.Element
}
如上这是两种不同的用法,Swift2.2之后,将第一种用法改用associatedtype关键字。
再回到SequenceType的声明上,首先声明了一个Generator的占位名称,类型是GeneratorType。从GeneratorType的声明中,我们可以看到它封装了枚举状态及遍历Sequence的方法。去掉编译器指令及注释后,声明代码如下:
public protocol GeneratorType {
associatedtype Element
public mutating func next() -> Self.Element?
}
王巍给了一个逆向遍历Sequence的实现GeneratorType的例子作为参考:
class ReverseGenerator: GeneratorType {
typealias Element = Int //注意这里在实现时我们用typealias
var counter: Element
init(start: Element) {
self.counter = start
}
init<T>(array: [T]) {
self.counter = array.count - 1
}
func next() -> Element? {
return counter < 0 ? nil : counter-- //注意Swift3中 -- 语法会移除
}
}
明白了GeneratorType如何实现后,我们再回头看下SequenceType的声明,必要实现的方法是
public protocol SequenceType {
associatedtype Generator : GeneratorType
public func generate() -> Self.Generator
}
我们以刚才定义的ReverseGenerator,来定义一个ReverseSequence。
class ReverseSequence<T>: SequenceType {
typealias Generator = ReverseGenerator
var array: [T]
init(array: [T]) {
self.array = array
}
func generate() -> Generator {
return ReverseGenerator(array: array)
}
}
这样我们就可以把ReverseSequence用for...in语法来遍历了,写个例子来验证一下
let arr = [0,1,2,3,4]
for i in ReverseSequence(array: arr) {
print("Index \(i) is \(arr[i])")
}
结果如下:
Index 4 is 4
Index 3 is 3
Index 2 is 2
Index 1 is 1
Index 0 is 0
除了支持for...in语法,ReverseSequence还可以直接使用以下方法:
public func map<T>(@noescape transform: (Self.Generator.Element) throws -> T) rethrows -> [T]
public func filter(@noescape includeElement: (Self.Generator.Element) throws -> Bool) rethrows -> [Self.Generator.Element]
原因是这些方法已经在protocol extension中实现了。
多元组(Tuple)
这个Tip也很好理解,主要还是想介绍Swift新引入的Tuple的概念。对于方法返回值,利用元组这个概念,我们可以方便的返回多个不同类型的值。但是这样定义的方法适用于临时组织的数据,有时候定义类或者结构作为返回值会更适合。更多关于元组的语法,大家可以参考Apple官方Swift教程。
小结
本文按书中顺序介绍了4个Tips,有些概念可能比较陌生,所以本文做了很多延伸的介绍,并在下面给出了参考的链接。有些则展示了源码的声明,帮助更好理解Tip介绍的概念。 而其他一些Tips则相对简单,基本学习过Swift语法都应该是已经掌握的,故也不多做介绍。Swift语法的学习,国内已经有爱好者无偿做了高质量的翻译,下面也给出链接方便大家查阅。
本人也希望把此文做成一个系列,每篇介绍几个书中讲到的Tips,再加进一些本人的理解及相关知识的拓展延伸。希望对于没看过该书的朋友,可以学到书中所授知识,对于已读过的朋友,也能从本文中找到一些新的角度及解读。