函数是现代编程最重要的基础之一。他们允许你将要执行的特定的任务逻辑封装在一个单元里,可实现封装重用。封装也可以是执行抽象层的封装,允许你和你的团队成员将其作为一个独立的“黑盒”而不用操心内部具体实现细节。
Swift支持全局的函数和方法,函数关联的类和其他的类型等。Swift还支持闭包,类似于匿名函数的表达式。
在这一章中,你将进一步探索函数,他们的语法以及基本类型等。你将看到输入输出参数对类和结构参数的影响。你还会看到Swift的方法和函数参数的命名一直受Object-C的影响。最后,你将了解到简洁且灵活的闭包表达式的语法,这也是为什么Swift会被称为是一种函数式语言的原因。
在这一章中,你将继续探索Swift函数的强大和多功能性。但在你晕头转向前,先来做点简单的小的例子。
生成一个新的playground并添加代码如下:
import Foundation
let a = 3.0, b = 4.0
let c = sqrt(a * a + b * b)
print(c)
你还能认出这是你在学校数学课里的哪个数学公式吗~~~~,勾三股四弦五,只能帮你这么多了。。。
你在playground中实现了一个函数,允许你在代码的最后使用开平方来计算代码。你定义了两个常数a和b,然后用勾股定理来计算c。控制台输出5.0.
Swift中没有自带计算数值平方的函数,所以我们自己来写一个,更新playground。
import Foundation
func square(number: Double) -> Double {
return number * number;
}
let a = 3.0, b = 4.0
let c = sqrt(square(a) + square(b))
print(c)
修改成这样后,输出的值依然是5.0。
上面的代码定义了一个叫square的函数,传入一个Double的参数并返回一个Double。
在Swift中,用关键词func来定义一个函数,后面跟函数的名字。函数可以包含有多个不同类型的输入参数但只能返回一种类型的数据。Swift的函数定义在全局范围,与此相反的是方法通常被定义在一个声明的类型范围里,比如一个类和结构。
除了语法的声明有些不同外,Swift和绝大多数的现代语言如Object-C都是相似的。让我们来试试一些新东西!
更新代码如下
func square(number: Double) -> Double {
return number * number
}
let operation = square
let a = 3.0, b = 4.0
let c = sqrt(operation(a) + operation(b))
print(c)
再次运行输出结果依然是5
毫无疑问,你肯定已经注意到了代码上某些有趣的事了,你定义了一个新的常数operation并将square作为值赋值给他。后面的代码用的也是operation而不是square了。
这是Swift语言的一个非常重要的特征。函数也可以看做是一个类,这意味着你可以将他分配给变量或常量,作为传递的参数或者是从其他函数返回的值。你可以使用采用了函数式编程的非常强大的编程技术,在第七章中还会深入的讲解函数式编程。
你要记住Swift是一种强类型语言,每个变量和常数都有一个类型和差不多相似的函数。比如下面这行代码没有声明常数的类型,所以编译器必须要自己推断类型。
let operation = square
但是square是作为什么类型被推断的呢?更新代码:
let operation:(Double) -> Double = square
代码正常运行。
square和operation都是(Double)->Double类型。这个函数类型说明了参数的类型和返回的类型(包括了参数的个数和类型)。这通常被称为函数的签名。和任何的函数一样,同样的签名都被认为是具有相同的类型。
技巧:当函数被作为另一个函数的参数时可能十分的难于理解,比如下面这种情况:
func doMath(operation:(Double)-> Double) -> Double { … }如果你发现你已经被绕晕了的话可以用别名来试试。允许你给一个非常复杂的类型赋予一个别名:
typealias OperationType = (Double) -> Double
func doMath(operation:OperationType) -> Double { … }
正如你看到的那样,Swift的函数语法非常的简单。包含了函数的关键字func以及紧随其后的函数名,括号里的输入参数,最后箭头后面的返回类型。
这还有几个简单的例子:
你可以定义没有参数的函数,比如,创建一个用于生产随机数的函数:
func generateRandomNumber() -> Double { ... }
函数中有多个参数时可用逗号分隔开:
func padString(string: String, length: Int) -> String { ... }
函数没有返回值或者返回的是void的,则稍有些许的有趣,在playground最后添加代码:
func logDouble(number:Double) -> Void {
print(String(format: "%.2f", number))
}
logDouble(c)
这创建了一个叫logDouble的函数,传入一个Double的参数并输入一个控制了小数位数为两位的字符串。然后调用这个函数并传入c。控制台输出:5.00
注意:这个代码用了NSString中的初始化方法initWithFormat。用Object-C的方法来弥补Swift中对字符串类型的初始化。
你可能注意到Void并不像Object-C那样是个关键词,而是一种类型。如果你打开Swift的头文件,你能发现Void是个()的别名:
typealias Void = ()
这和你使用没有元素的元组方法是一样的,你当然可以直接用(),虽然其实并没什么卵用。
技巧:你可以在xcode中按着common键鼠标点击对应的类型查看其在Swift的定义。
为了好玩,修改下函数,让其更紧凑些:
func logDouble(number:Double) -> () {
print(String(format: "%.2f", number))
}
再编译看看,是不是结果仍然一样。你甚至可以在方法中给将参数赋值给一个变量,虽然会有警告,且毛用没有。
还有个更简洁的定义方式。你可以完全去掉箭头和返回值,更新代码:
func logDouble(number:Double) {
print(String(format: "%.2f", number))
}
苹果设计的Swift是倾向于简洁紧凑的,允许你放弃掉不必要的结构和语法~~
然而需要提一下的是,当你抛弃了箭头和void时,你的logDouble类型依然是(Double)->()。
为了验证这一点,你可以更新你的代码并明确的定义一个有类型的变量。
func logDouble(number:Double) {
print(String(format: "%.2f", number))
}
var logger: (Double) -> () = logDouble
logger(c)
代码执行的情况和原来一模一样,然而一旦你省略了类型的声明中的返回值,如下:
var logger: (Double) = logDouble
然后编译器提示错误,需要你进行类型转换。因为(Double)是一个元组而不是一个函数。
现在你已经知道了函数的基本的内容,是时候去发现些更加有意思的东西了。现在是该了解泛型的时间了。
在一个新的playground上添加如下代码:
func checkAreEqual(value: Int, expected: Int, message: String) {
if expected != value {
print(message)
}
}
上面这个函数检查一个给定的整数是否和另一个期望的整数相等。如果两个值不相等,则函数输出一条信息在控制台。你可以用这个格式的方法用来验证或者作为前置判断条件。
在函数下面加几行代码测试下:
var input = 3
checkAreEqual(input, expected: 2, message: "An input value of '2' was expected")
input = 47
checkAreEqual(input, expected: 47, message: "An input value of '47' was expected")
第一个输出信息,第二个通过验证,控制台输出:
An input value of ‘2’ was expected
当前的函数只有少量的限定词所以只能用来检查你的整数是否匹配你期望的整数。如果你想要检查字符串string呢?或者是Double,或其他的类型呢?
好在,Swift允许你使用重载。重载即是用了相同的函数名字,但他们的参数类型,或者参数的个数是不同的。
在playground后面添加代码:
func checkAreEqual(value: String, expected: String, message: String) {
if expected != value {
print(message)
}
}
var inString = "cat"
checkAreEqual(inString, expected: "dog", message: "An input value of 'dog' was expected”)
控制台输出:
An input value of ‘dog’ was expected
通过重载checkAreEqual函数,你可以像判断整数值那样判断字符串是否相同了。
这个并不是一个可以自由扩展的解决方案。每一个新的类型都需要一个单独的函数,下面还有一种更好的方式。
删除checkAreEqusl的函数方法,替换为下面的内容:
func checkAreEqual(value: T, expected: T, message: String) {
if expected != value {
print(message)
}
}
正如你期待的那样,输出没有变化。
新添加的checkAreEqual是通用的,前两个指定参数用占位符T来表示。当函数被调用时,T会自动推断类型。另外,编译器也在下面两种情况下检查约束。
1.传递到此函数时的第一个和第二个参数必须是相同类型,因为两个参数都用了相同的类型T。
2.必须采用Equatable协议类型。这个约束利用了!=运算符,是实现checkAreEqual的必须条件。
为了验证先上面的条件我们试试用混合类型调用此函数,试试删除掉Equatable约束看看会怎么样,会有什么错误提示?
想了解更多和泛型相关的类型,请看第四章。
提示:Swift的类型推断非常强大。如果你将47和48.67分配给常量,且让编译器自己推断类型,则会分别选择Int和Double。所以很明显编译器会提示错误,第一个是Int,第二个是Double,无法执行。
所有到目前为止你写的大量的包含有输入参数和返回值的函数,都不会影响到任何参数的状态。但是如果你想要一个可以修改传递过来的参数的函数呢?这叫做函数的副作用,且行为必须要显示的声明,下面来举个例子!
打开一个新的playground,添加如下代码:
import Foundation
func square(number: Double) {
number = number * number
}
var a = 2.0
square(a)
print(a)
上面的代码定义了一个简单的函数square。将传递来的值修改为原来的平方。
明显发现编译器报错:
Cannot assign to ‘let’ value number.
这个错误提示告诉了你有关函数的一个非常重要的信息,默认情况下他们的参数都是常数。换句话说,他们的行为和用let关键字定义的常数是一样的。
编译器明确的指出这样的操作是禁止的。
提示:大多数的其他主流语言(Object-C,jav,c#等)都允许你在函数类修改函数的参数。然而,这些改变也仅仅是改变函数参数值在本函数内的本地副本,而不是调用者传入的实际变量。Swift是明确禁止修改函数参数的,因为他有一个非常好的常数概念,让你出现异常结果的可能性变得更小。
你可以修改函数参数的默认定义,先定义函数参数为变量inout就可以让编译器通过了,调传参数的时候前缀一个&,使用如下:
fun square(inout number: Double) {
number = number * number
}
var a = 2.0
square(&a)
print(a)
输入如预料的是4
在许多的其他语言,如Object-C中,在使用的参数前用一个&表明你传递的是值的引用,所以函数可以改值。
尽管palyground上的输入输出参数是Double类型,但事实是你可也以指定任何类型的参数作为输入输出,包括类和结构。
技巧:用in-out可以修改值,但是尽量少用,很容易造成调用时的混乱
在前面的内容中,你已经看到了用Int,Double,String作为函数参数时被拷贝一份用于使用,也看到了用inout关键字来修改传入的参数。
Swift在处理类上面有些不同,看下面的例子:
新建一个playground,并添加如下代码:
class Person {
var age = 34,
name = "Colin"
func growOlder() {
self.age+=1
}
}
func celebrateBirthday(cumpleanero: Person) {
print("Happy Birthday \(cumpleanero.name)")
cumpleanero.growOlder()
}
let person = Person()
celebrateBirthday(person)
print(person.age)
在playground定义了一个叫Person的类,里面包含了age,name两个属性,还有一个用于让年龄叠加的函数。 celebrateBirthday()是一个普通的函数,可以打印一条庆祝信息,并叠加对象的年龄。
控制台输出:
Happy Birthday Colin
35
你可以看到调用celebrateBirthday成功的让年龄增加了一岁。然而是怎么实现的呢?
当你将一个对象的实例作为一个参数传递给一个函数时,Swift传递的是这个类的引用。在你的playground上,person和cumpleanero都是指向同一个Person的实例。
将Person修改为结构看看情况如何
struct Person {
var age = 34, name = "Colin"
mutating func growOlder() {
self.age+=1
}
}
注意当你将声明class改为struct时,你还需要将growOlder()用mutating声明。任何结构中新的函数要改变他的属性状态值都需要用到mutating这个关键字。
更新代码如下
func celebrateBirthday(inout cumpleanero: Person) {
print("Happy Birthday \(cumpleanero.name)")
cumpleanero.growOlder()
}
var person = Person()
celebrateBirthday(&person)
print(person.age)
控制台输入:
Happy Birthday Colin
35
结构的使用和Int,Double之类的相同,当要修改结构中的值时必须要要用inout关键字
。
上面的代码你做了一连串的改变。首先person现在是常数而不是变量。你可以改变一个被分配给常数的类的属性,但不可以修改一个被分配给常数的一个结构,因为结构是值类型,要想修改就要用inout进行传递。第二处,正如前面的,你用&声明将结构对象作为参数传递。
正如你看到的,函数在处理类和结构作为参数时操作是完全不一样的。
注意:回忆下能想起数组和字典都是值类型的结构。如果你想要修改他们的值都必须先将他们标记为输入输出参数。
在这一部分,你将了解到可变长度参数。你可以使用变量的参数个数给函数。
你用一个省略号表示一个参数类型的个数是可变的,如下所示:
func longestWord(words: String...) -> String?
此函数接收一个字符串列表并返回一个最长的字符串或者nil。是时候创建一个新的playground了,此时不练练更待何时!
func longestWord(words: String...) -> String? {
var currentLongest: String?
for word in words {
if currentLongest != nil {
if word.characters.count > currentLongest!.characters.count {
currentLongest = word
}
} else {
currentLongest = word
}
}
return currentLongest
}
可变参数words变成在函数内的一个常数数组,允许你使用for-in 控制结构来遍历他的内容。这个算法的实现相当简单:函数遍历每个word,比较目前最长的字符串。
在函数下添加调用代码试试:
let long = longestWord("chick", "fish", "cat", "elephant")
print(long)
控制台输出
Optional(“elephant”)
输出表明,long是一个包含有“elephant”的可选类型值,尝试用不同的数量值的参数进行测试。
你看了第七章“Functional Programming”了吗?如果看过你可能会知道如何让这个函数更加简洁:
func longestWord(words: String...) -> String? {
return words.reduce(String?()) {
(longest, word) in
longest == nil || word.characters.count > longest!.characters.count
? word : longest
}
}
不用担心这个函数看不懂,后面的章节会详细介绍。是不是简单了不少!你可能不会经常使用到可变参数,但是当需要用时,这就是一个非常棒的解决方案。
目前为止你已经写了很多两个有着多个参数的函数了,比如:
checkAreEqual("cat", "dog", "Incorrect input")
有时候很难确认每个参数在上下文对应的作用。在上面的代码中,你没那么容易知道哪个是输入值,哪个是期望值。
外部参数名可以解决这个问题,在新的playground上试试:
func checkAreEqual(value val: String, expected exp: String, message msg: String) {
if exp != val {
print(msg)
}
}
这个函数的实现和前面是非常相似的。不过这一次中每个参数都有了两个名字。比如第一个参数有value,val两个名字。第一个名字是用来在外部调用时展示的外部参数名,这第二个名字是在函数内部使用的名字。
调用checkAreEqual时可以看到外部名称:
checkAreEqual(value: "cat", expected: "dog", message: "Incorrect input")
在定义checkAreEqual时提供了外部参数名,则你在调用这个函数的时候必须使用这个名字。你可以看到引入这个有多先进-他避免了每个参数在使用时造成的任何模糊不清的歧义。
当你使用了命名参数时,你的参数需要按照正确的函数顺序填写。尽管代码很清晰明确,但如果不按照正确的参数名传值,依然无法通过编译。
checkAreEqual(expected: "dog", value: "cat", message: "Incorrect input")
为了保持代码的简洁性,推荐你尽量少使用外部参数名。只有需要用到外部参数名来解决歧义时来使用比较合适。举一些例子看看:
下面是一个将字符串转变为日期的函数:
dateFromString("2014-03-14")
很明显该函数只接受一个单一的参数,所以再增加一个外部参数无异于画蛇添足。
与此相反,下面的函数获取一个cell上的坐标:
convertCellAt(42, 13)
一个参数是行,一个参数是列,但是哪个是行哪个是列呢?这时很明显就需要外部参数了。
convertCellAt(column: 42, row: 13)
Swift是一种面向对象的语言,因此,你写的应用的逻辑在方法中而不是在全局函数中。方法是与类型(如类,结构,枚举)相关联的一种特殊的函数。在本节中,你会发现一些在Swift中的一些特殊行为是受Object-C语法的大影响的。
关于方法和函数有两种理解方式:1.没有区别 2。函数是全局的,方法是类或结构里面的
实例方法将一个函数与一个特定类型的一个实例进行了关联。你可以在类,结构和枚举中定义一个和全局函数完全相同语法的方法。在一个新的playground中演练下:
class Cell: CustomStringConvertible {
private(set) var row = 0
private(set) var column = 0
func move(x: Int, y: Int) {
row += y
column += x }
func moveByX(x: Int) {
column += x
}
func moveByY(y: Int) {
column += y
}
var description: String {
get {
return "Cell [row=\(row), col=\(column)]" }
}
}
该类定义了一个包含有个行,列属性,一些可用于修改属性的方法,一个描述语句。并遵守CustomStringConvertible协议。
添加下面的代码执行这个类。
var cell = Cell()
cell.moveByX(4)
print(cell.description)
这个是Cell的一个实例,用了moveByX修改坐标并打印结果,控制台输出:
Cell [row=0, col=4]
这没什么奇怪的地方,继续更新代码:
var cell = Cell()
cell.moveByX(4)
cell.move(4, y: 7)
print(cell.description)
注意,在上面的代码中使用了move方法。你的第二个参数包含了名字。如果你删除了y:参数的前缀,编译器报错。
Swift方法和函数共享一个相同的内部和外部参数名概念相同。然而,他们的默认行为有点区别。一个函数除非你显示的提供了外部名称,否则所有的参数都无外部名称。但在方法中,第一个参数无外部名字,后续的参数默认使用和内部一样的外部名称。
当然你也可以通过自己命名外部名称自由的调整外部名称,操作方式和前面的完全相同。此外,你可以添加一个下划线表示删除默认的外部名称。
尝试修改下代码看看效果:
func move(x: Int, _ y: Int) {
row += y
column += x
}
现在方法的调用可以不使用外部参数了cell.move(4, 7)
虽然你可以修改默认的参数命名方式,但是我还是建议你不要用。苹果的api从Object-C开始就采用了这个标准,和moveBy,moveByY一样方法名指定了第一个参数的名称。
另外一个函数和方法屌炸天的功能是提供了给参数赋一个默认值的能力。更新move代码,代用0作为x和y的默认参数值
func move(x: Int = 0, y: Int = 0) {
row += y
column += x
}
相应的更新调用的代码
var cell = Cell()
cell.move(4, y: 7)
cell.move(2)
cell.move()
cell.move(y: 3)
print(cell.description)
从上面可以看出有了默认值后,可以不填参数,当只填一个参数时默认的代表第一个对应的参数,其他的参数需要写外部参数名。
早在前面的章节中,你就发现在Swift中函数可以当类用。你可以在函数中分配变量或常量并将值传递给其他的函数。方法也是一个类。
实践出真知,操练下:
var cell = Cell()
var instanceFunc = cell.moveByY
instanceFunc(34)
print(cell.description)
上面的代码生成了一个Cell的实例,然后分配moveByY实例方法给一个变量,然后通过这个变量的引用调用这个方法,控制台输出:
Cell [row=0, col=34]
你已经通过函数了解了实例方法这个概念,还有另一个可能比较有趣的功能,更新代码:
var cell = Cell()
var moveFunc = Cell.moveByY
moveFunc(cell)(34)
print(cell.description)
这一次,你是通过类方法来获取的moveByY方法而不是通过一个实例赋值。
当你第一次绑定一个对象的实例时返回对应实例下的函数,相当于在你早些时候的代码中就初始化了函数。
如果你对“当前函数”完全不了解也没事。等你看了第七章“函数式编程”再来理解吧!
闭包,和函数方法一样,都可以调用代码块,传递等。但是和函数方法不一样的是,闭包是匿名的,而且有“捕捉”的能力将值存在在定义他们的范围。在接下来的几节中,你会了解到闭包有多强。
在Swift的api中大量的函数和方法都使用了这个闭包。举个例子如下:
func sort(isOrderedBefore: (T, T) -> Bool) -> [T]
(Swift3.0已修改,自行在xcode中查看)
这种方法有一个参数:isOrderedBefore.这个参数本身就是一个函数,她接受两个T型参数(有这个数组的类型定义)并返回这两个值的相对顺序的布尔值。使用排序方法返回一个有序数组。
让我们看看这个方法,创建一个playground添加如下代码:
let animals = ["fish", "cat", "chicken", "dog"]
func isBefore(one: String, two: String) -> Bool {
return one > two
}
let sortedStrings = animals.sort(isBefore)
print(sortedStrings)
上面生成了一个常数数组animals和一个用来决定两个字符串相对顺序的函数isBefore。Swift定义了一个大于操作符用来比较字符串,使isBefore的实现很简单。
函数定义后,通过引用isBefore,创建一个排序后的数组:
[fish, dog, chicken, cat]
简而言之,你的playground告诉排序数组应该基于一项值是否大于另一项。唯一真正需要知道的排序内容如下:
one > two
在这种情况下,声明一个单独的isBefore函数用于排序显然有些多余。在接下来的几个步骤中,你可以去掉一些不必要的结构来创建一个更加简洁的实现。接下来的实现是非常神奇的,当然你在playground中的代码也是受益于这种语言的特性。让我们来去掉无用的函数。
更新代码
let animals = ["fish", "cat", "chicken", "dog"]
let sortedStrings = animals.sort({ (one: String, two: String) -> Bool in
return one > two
})
print(sortedStrings)
和刚刚一样,输出一个排序后的数组。
你使用了一个闭包函数而不是通过一个函数的引用来进行处理。在这例子中,你可以考虑用一个匿名函数闭包来实现。闭包的语法逻辑和刚刚的isBefore是一样的,只是没有额外的函数声明。isBefore的代码直接放在了sorted中。
你当前的闭包函数的语法和普通的函数语法非常的像。一个参数列表,一个箭头,一个返回类型,in关键字紧随在闭包的实现后面。
然而,一切有意思的事情才刚开始。你已经知道编译器可以根据上下文推断变量和常量的类型。所以其实在闭包中编译器也可以执行类型的方式。
在接下来的几个例子中,你会逐渐从闭包函数中删除代码,直到达到最简形式。每一次的改变,你的控制台输出都不会改变。
首先,参数的类型可以先删除了:
let sortedStrings = animals.sort({ (one, two) -> Bool in
return one > two
})
编译器能从sort中获取的数据推断参数的类型。
这个表达式只有一个语句块,作为一个结果,return关键字有些多余,所以可以删除:
let sortedStrings = animals.sort({
(one, two) -> Bool in
one > two
})
这个闭包函数的返回值也可以从sort需要的类型中推断,所以可以删除返回类型:
let sortedStrings = animals.sort({
(one, two) in
one > two })
虽然参数旁边的括号没几个字符,但毕竟是占用了几个字符,删掉,删掉,通通删掉。
let sortedStrings = animals.sort({
one, two in
one > two })
你已经几乎删除了50%的非空白字符,但这才刚刚开始!
后面这一步有点拽,你可以删除掉闭包函数中的参数声明,完全通过输入参数分配给本地常量:
let sortedStrings = animals.sort({ $0 > $1 })
正如你上面看到的,如果你不提供局部变量的参数名称,Swift提供了编号进行对应。这是闭包函数中一个非常好用且简单的参数集。
如果一个闭包作为最后一个参数传递给函数或方法,你可以将闭包写在括号外,使用所谓的后闭包。let sortedStrings = animals.sort() { $0 > $1 }
最后你再删除掉空的圆括号,最简结构如下:
let sortedStrings = animals.sort { $0 > $1 }
通过我的计算,你现在的闭包表达式至少比你一开始紧凑了3倍!理论分析结构已经是最紧凑了,但还不是最最紧凑的代码形式。sort需要的参数类型(String,String)-> Bool.大于操作符已经完全表明了这个关系,所以还可以直接说明:
let sortedStrings = animals.sort(>)
还记得你在这一节开始时定义的isBefore函数吗。你可以直接将闭包函数分配给一个变量:
var isBefore = {
(one: String, two: String) -> Bool in return one > two
}
let sortedStrings = animals.sort(isBefore)
因为编译器需要推断isBefore表达式的类型,所以,你不能够删除参数的类型,如果删除了,编译器则无法推断isBefore变量类型。编译器会报错:
var isBefore = {
(one, two) -> Bool in
return one > two
}
let sortedStrings = animals.sort(isBefore)
编译器无法根据你使用isBefore变量的方式来判断他的类型,直接将闭包传递给sorted:在sort的内部操作中编译器可以做更多的类型推断。
闭包最强的地方之一是可以从他周围的内容环境中捕获常量和变量。闭包可以使用一些原来的环境已经被破坏了的值!通过一个例子来理解这个概念是最简单的。
新的playground中添加代码:
typealias StateMachineType = () -> Int
typealias 定义了一个叫StateMachineType的函数类型。这个函数每次调用都返回一个Int。状态用intergers来表示,在每个调用中状态都在一个周期里循环。如,一个状态机有三个状态周期:
0, 1, 2, 0, 1, 2, 0, 1, 2, …
接下来添加一个函数用来创建一个基于周期状态数的状态机:
func makeStateMachine(maxState: Int) -> StateMachineType {
return {
currentState+=1
if currentState > maxState {
currentState = 0
}
return currentState
}
}
在详细了解这个函数内容前,先检查下是否能运行正常。添加一个测试如下:
let tristate = makeStateMachine(2)
print(tristate())
print(tristate())
print(tristate())
print(tristate())
print(tristate())
控制台输出:
1
2
0
1
2
看上去很棒!接着在代码下面继续添加一个状态机:
let bistate = makeStateMachine(1)
print(bistate());
print(bistate());
print(bistate());
print(bistate());
不出所料的,输出值在0和1之间循环
1
0
1
0
你已经确认了这个函数在创建一个状态值时需要一个基数,现在来看看是怎么具体实现的,看代码
func makeStateMachine(maxState: Int) -> StateMachineType {
var currentState: Int = 0
return {
currentState+=1
if currentState > maxState {
currentState = 0
}
return currentState
}
}
makeStateMachine的第一行定义了一个本地变量currentState,用来保存在当前结构中的状态值。第二行返回一个状态机本身状态的闭合表达式。这一行你省略了闭合表达式的结构()->Int,因为编译器能够从这种封闭的函数中推断出需要的类型。类型推断是非常聪明的。完整写法如下:
再更仔细的看看闭包的表达式,他利用了封闭的makeStateMachine函数的局部变量currentState。这是比较一个有趣的地方!
currentState是makeStateMachine中的本地变量,你希望这个函数在返回时即注销。
let tristate = makeStateMachine(2)
// currentState
变量在这个时候被注销了吗?!
print(tristate())
然而状态机正常工作,可以从makeStateMachine返回的闭包表达式中了解到currentState一定是还存在且有效的。
你当前在playground上的代码就是一个很好的用于示范值捕获的练习。因为闭合表达式利用了currentState变量,所以他可以在使用它的上下文周期中继续使用currentState。
此外你无须担心捕捉值的内存管理。Swift在捕获值应当销毁的时候自动处理。
很多现在的CocoaAPIs都是用的代理来处理异步执行的代码。比如,location的位置改变通知CLLocationManager通过CLLocationManagerDelegate协议声明处理。闭包的优秀表现,无疑会让苹果引入更多闭包的api,当你异步调用的时候调用一个闭包而不是一个委托对象。当这一切真的都发生的时候,你会发现捕获值是多重要的一个功能代码。
我相信你已经发现了闭包的强大之处,你应该会在你的代码中大量使用!不过在你这样做之前,还有一个重要的课题需要学习:如何避免因为使用闭包造成的内存泄漏。让我们来看一个简单例子:
Swift的playground不是一个用来了解内存管理的好地方。在playground的代码是反复运行的,所以,对象的释放不能预测。在这最后一小节,你需要创建一个完整的app。
打开Xcode选择File/New/Project… 并生成一个Single View Application命名为MemoryLeakTest。在这个项目中快速生成一个Person.swift文件,添加代码如下:
class Person {
let name: String
private var actionClosure: (() -> ())!
init(name: String) {
self.name = name
actionClosure = {
println("I am \(self.name)")
}
}
func performAction() {
actionClosure()
}
deinit {
print("\(name) is being deinitialized")
}
}
Person类非常简单,有一个不变的常数属性name,通过初始化设置。还有一个deinit在这个类被销毁时打印一条消息。
这个类还有一个performaAction方法用来打印name。你已经通过actionClosure的函数属性实现了。
是时候来用一下这个类了。
打开ViewController.swift,更新viewDidLoad代码如下:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let person = Person(name: "bob")
person.performAction()
}
简单的创建了一个Person的实例,并分配了一个属性值同时调用了performAction. 常数person是ViewDidLoad的本地值,所以,当这个方法退出时,person也应该被注销。为了能直接显示结果,你还希望deinitializer被调用,表明Person的实例已经被注销。
现在你可以测试下这个理论了。运行程序,监视控制台的输出:
I am bob
这证实了你的Person实例创建成功,且功能完全正常,但在deinit中打印语句呢?为什么这个方法没有执行?
答案很简单:在你的代码里有内存泄漏!如果你想仔细检查内存问题,你可以配置下你的应用检查内存的分配情况。这说明在ViewDidLoad退出后Person实例仍然在堆上存在。
闭包是引用类型,下面是对象和ViewDidLoad之间的关系图:
ViewController有个对本地常量Person实例的引用。Person有个加name常量属性的引用,有个被分配为私有的闭包函数actionClosure的引用。最后,这个闭包函数内部使用替换字符串的地方使用了self,造成了这个闭包函数对Person的引用。
当ViewDidLoad退出时,本地常量person被释放,删除ViewDidLoad和Person实例见的连接线。此时,你希望Person的实例的引用计算值应该下降为0并自动被销毁。不幸的是,这个期望并不会发生。因为闭包函数的有对Person实例的引用,如下图红色线所示:
这是一个典型的计数循环,在对象之间的循环引用造成的内存无法被释放。
笔记:如果你是从C#,Java,JavaScript或者其他采用垃圾回收机制语言转到Swift的,你可能会觉的有些奇怪。垃圾回收机制将引用追溯到已知的“根”,如果无法追溯到root根,则这个对象将从堆中释放销毁。用了这种技术,当检测到游离的循环引用时会在垃圾回收中移除。
Swift用的是自动引用计算。每次使用这个对象时都会增加一个对象引用的计数,当一个对象保留的计数为0时,这个对象立即被销毁。这解决了需要停止应用主线程来收集“垃圾”,但是你却需要在使用代码时避免出现循环引用。
这个问题并不是只在Swift中出现,在Object-C的代码块的block中也有这个问题。如果你熟悉Object-C的话,标准的解决方法是创建一个弱引用给self,然后创建一个强引用基于这个弱引用,在block的语句块中使用这个强引用:
__weak typeof(self)weakSelf = self;
[self.context performBlock:^{
__strong typeof(weakSelf)strongSelf = weakSelf;
// do something with strongSelf
}];
呵呵!既丑又容易出错!
还好,在解决这个同样问题时Swift要简单的多。打开Person.swift并在actionClosure的初始化代码里更新如下:
actionClosure = {
[unowned self] () -> () in
print("I am \(self.name)")
}
上述代码定义了一个闭包的捕获代码,详细的说明了这个闭包列表中常量和变量的所有权。在闭包使用前出现这个捕获列表,并将其用放扩报包含变量列表。
在这种情况下,unowned self表明这个闭包函数没有对自身的引用,不会增加引用计数。
编译并运行程序,确认Person的实例现在有被成功释放掉:
I am bob
bob is being deinitialized
很简单不是吗?
一般情况下你不太可能会写一个Person类,让他通过一个私有的闭包来实现方法,但你为了出现这种循环引用所以使用了这样的一个闭包。
循环引用一般在视图控制器中经常出现。你经常需要异步的更新一个视图控制器上的UI。如果你用闭包来实现更新,很有可能就会出现循环引用。如果你的视图控制器一直不能释放,说明出现了内存泄漏了!
在这一章中,你了解了下Swift中的函数和闭包,也学习了他的首要类first-class,语法和表达式。
最后你可能会发现,在大多数情况下,你的函数和闭包表达式是可以互换的,这是因为函数和闭包是一样的,函数是有名字的闭包而已。