第六章——函数(序)

293 阅读5分钟

本文系阅读阅读原章节后总结概括得出。由于需要我进行一定的概括提炼,如有不当之处欢迎读者斧正。如果你对内容有任何疑问,欢迎共同交流讨论。

本章主要是介绍函数、闭包相关的基础知识。如果你对此已有了解可以直接跳过

关于函数,主要有三个重要的概念:

  1. 函数也是一种类型,和IntString类型等类似,函数可以被赋值给变量,也可以作为参数传入、传出别的函数。
  2. 函数体内,不仅可以使用它的参数,还可以使用函数外部被截获的变量
  3. 创建函数有两种方法:用func关键字或闭包表达式{ }

接下来详细解释这三个概念:

1. 函数可以赋值给变量,也可以作为参数传入、传出别的函数

在Swift和很多现代语言中,函数也是“一等公民”。它可以被赋值给变量,也可以作为参数传入、传出别的函数。理解这一点很重要,这是函数式编程的基础,就像指针在C语言中那么重要。我们通过一段代码来演示函数赋值给变量:

//定义了一个函数
func printInt(i: Int) {
print("you passed \(i)")
}

//函数赋值给变量,函数名后不用加括号
let funVar = printInt

//通过函数名调用函数,需要传入参数
funVar(2)   //输出结果:“you passed 2”

函数也可以作为别的函数的参数来用:

// 这个函数的参数是一个 (Int) -> ()类型 的函数
func useFunction(funcParam: (Int) -> ()) {
funcParam(3)
}

//把之前的函数传入,作为参数。输出结果相同,都是:“you passed 3”
useFunction(funVar)
useFunction(printInt)

掌握了这一点,我们就可以写出很多有用的高阶函数,这在第三章——“集合”中有所体现。

2. 函数体内可以使用它截获的,位于函数外部的变量

我们先看一个例子,把函数作为另外一个函数的返回值:

// 返回的函数类型是:(Int) -> String
func returnFunc() -> (Int) -> String {
func innerFunc(i: Int) -> String {
return "you passed \(i) to the returned func"
}
return innerFunc
}

let myFnc = returnFunc()
print(myFnc(4))

//输出结果:“you passed 4 to the returned func”

如果函数引用了函数体外部的变量,我们说这个变量被函数“截获”了。变量在超出自己的作用域后不会被释放,因为还有这个函数引用着它。我们把刚刚的函数修改一下,新增一个count变量:

func returnFunc() -> (Int) -> () {
var count = 0
func innerFunc(i: Int) {
count += i
print("running total is now \(i)")
}
return innerFunc
}

每次调用returnFunc函数都会获得一个不同的函数:

let f = returnFunc()
f(3)	// 会输出:“running total is now 3”
f(4)	// 会输出:“running total is now 7”

let g = returnFunc()
g(2)	// 会输出:“running total is now 2”
g(2)	// 会输出:“running total is now 4”

f(2)	// 会输出:“running total is now 9”

你可以把截获了变量的函数理解为一个类,这个类有一个方法(就是这个函数自身)和多个成员变量(被截获的变量)

3. 闭包表达式的{ }语法

除了用func关键字创建函数之外,还可以用{ }语法创建函数。先看一个普通的函数:

func doubler(i: Int) -> Int { return i * 2 }
let a = [1, 2, 3, 4].map(doubler)  //a = [2, 4, 6, 8]

等价的写法是:

let doubler = { (i: Int) -> Int in return i * 3 }
let a = [1, 2, 3, 4].map(doubler)  //a = [3, 6, 9, 12]

用闭包表达式定义的函数,可以理解为函数的“字面量”。就像1"hello"分别是IntString类型的字面量一样。与用func关键字不同的是,用闭包表达式定义的函数是匿名的。如果想使用它,可以把它赋值给某个变量。

这里我写了return i * 3并非写错,而是表示新定义的doubler会替换之前的doubler,所以输出结果中数组的每个元素都是之前的三倍。这也说明了用func关键字和闭包表达式创建的函数是完全等价的。

闭包表达式的作用主要是在于简化定义函数的方式。比如之前的double可以做如下简化:

let a = [1, 2, 3, 4].map { $0 * 2 }  //a = [2, 4, 6, 8]

如果使用闭包表达式,根本没有必要创建doubler函数,就可以完成map。接下来我们以原来复杂的代码为例,一步一步详细描述简化过程:

let doubler = { (i: Int) -> Int in return i * 2 }
let a = [1, 2, 3, 4].map(doubler)  //a = [3, 6, 9, 12]
  1. 函数作为参数传入别的函数中时,没有必要把这个函数先赋值给一个临时变量,再把临时变量传入到别的函数中。我们可以直接把函数写在别的函数中。简化结果:
let a = [1, 2, 3, 4].map({ (i: Int) -> Int in return i * 2 })
  1. 不必指定编译器能根据上下文推导出的类型。比如在这个例子中,数组的元素是Int类型,所以传入到map方法中的函数的参数类型也是Int,由于函数中进行了乘法运算,所以返回值类型必然也是Int。简化结果:
let a = [1, 2, 3, 4].map({ i in return i * 2 })
  1. 如果闭包内只有一条语句,那么显然返回值就是这条语句的值,return关键字就可以省略。简化结果:
let a = [1, 2, 3, 4].map({ i in i * 2 })
  1. 对于传入的参数,可以不指定具体的名字。默认就是$0表示第一个参数,$1表示第二个参数……简化结果:
let a = [1, 2, 3, 4].map({ $0 * 2 })
  1. 如果作为参数的函数是别的函数的最后一个参数,它可以写在函数的括号外面。这就是“尾闭包”语法。简化结果:
let a = [1, 2, 3, 4].map() { $0 * 2 }
  1. 因为map函数没有别的参数,所以可以省略括号。简化结果:
let a = [1, 2, 3, 4].map { $0 * 2 }

这样的写法一开始可能不太熟悉,但是习惯之后就会发现它在不损害代码可读性的前提下,极大的简化了代码量。新手在简化闭包表达式时,可能会遇到各种错误。这时候最好先写出它的完整表达式,然后按照上述规则一步一步简化,直到遇到错误为止。这时再好好考虑当时是哪里犯了错。

标注函数类型

Swift会自动推导闭包表达式的类型,但有时我们也需要显式的标出。比如这个函数:

let isEven = { $0 % 2 == 0 }

它会被自动推导为Int -> Bool类型的。如果我们需要人为指定它的类型,可以这么写:

let isEven = { (i: Int8) -> Bool in i % 2 == 0 }

不过,在闭包内标注类型不是一个好方法,更优雅的解决方案是在闭包外标注类型:

var isEven: Int8 -> Bool = { $0 % 2 == 0 }
// 或者
let isEven = { $0 % 2 == 0 } as Int8 -> Bool

最好的,也是比较复杂的解决方案是定义一个范型的isEven方法:

func isEven<T: IntegerType>(i: T) -> Bool {
return i % 2 == 0
}

let int8isEven: Int8 -> Bool = isEven

总结

{ }闭包表达式和func关键字创建的都是函数,它们是等价的。而闭包是指截获了外部变量的函数。因此,严格意义上来说闭包和闭包表达式是两个不同的概念。闭包表达式就是函数,而闭包是可以截获外部变量的函数。