学习Swift语言(四)函数和闭包

594 阅读8分钟

一、函数

Swift 的函数函数语法既可以适配 C 语言只有 argument label 的风格,也可以适配 Objective-C 既有 parameter name 也有 argument label 的风格。Swift 函数支持指定参数默认值、输入输出参数(in-out parameter)。

Swift 的函数也具有类型属性,包含了函数参数值类型和返回值类型,因此很容易实现函数作为函数参数进行传递,或者作为函数的返回值。Swift 函数支持内嵌函数,利于实现函数嵌套。

1.1 函数定义和调用

// 1. 函数定义
func greet(person: String) -> String {
    let greeting = "Hello, " + person + "!"
    return greeting
}

// 2. 函数调用
print(greet(person: "Anna"))
// Prints "Hello, Anna!"
print(greet(person: "Brian"))
// Prints "Hello, Brian!"

1.2 函数参数和返回值

// 1. 没有参数的函数
func sayHelloWorld() -> String {
    return "hello, world"
}
print(sayHelloWorld())
// Prints "hello, world"

// 2. 带多个参数的函数
func greet(person: String, alreadyGreeted: Bool) -> String {
    if alreadyGreeted {
        return greetAgain(person: person)
    } else {
        return greet(person: person)
    }
}
print(greet(person: "Tim", alreadyGreeted: true))
// Prints "Hello again, Tim!"

// 3. 没有返回值的函数
func greet(person: String) {
    print("Hello, \(person)!")
}
greet(person: "Dave")
// Prints "Hello, Dave!"

// 4. 忽略返回值(函数有返回值,但是调用者不使用该返回值)
func printAndCount(string: String) -> Int {
    print(string)
    return string.count
}
func printWithoutCounting(string: String) {
    let _ = printAndCount(string: string)
}
printAndCount(string: "hello, world")
// prints "hello, world" and returns a value of 12
printWithoutCounting(string: "hello, world")
// prints "hello, world" but does not return a value

// 5. 带多个返回值的函数(按元组返回)
func minMax(array: [Int]) -> (min: Int, max: Int) {
    var currentMin = array[0]
    var currentMax = array[0]
    for value in array[1..<array.count] {
        if value < currentMin {
            currentMin = value
        } else if value > currentMax {
            currentMax = value
        }
    }
    return (currentMin, currentMax)
}

let bounds = minMax(array: [8, -6, 2, 109, 3, 71])
print("min is \(bounds.min) and max is \(bounds.max)")
// Prints "min is -6 and max is 109"

// 6. 返回可选元组
func minMax(array: [Int]) -> (min: Int, max: Int)? {
    if array.isEmpty { return nil }
    var currentMin = array[0]
    var currentMax = array[0]
    for value in array[1..<array.count] {
        if value < currentMin {
            currentMin = value
        } else if value > currentMax {
            currentMax = value
        }
    }
    return (currentMin, currentMax)
}

// 返回可选元组需要进行可选类型解包
if let bounds = minMax(array: [8, -6, 2, 109, 3, 71]) {
    print("min is \(bounds.min) and max is \(bounds.max)")
}
// Prints "min is -6 and max is 109"

注意:没有返回值的函数严格上说是有返回值的,其返回的类型是VoidVoid的本质则是一个空元组()。另外,(int, int)?类型和(int?, int?)类型是不一样的类型,差异则从字面上可以理解,前者要么是有效的元组,要么是空,后者元组的两个成员都有可能为空。

1.3 函数隐式返回值

接下来开始 Swift 的骚表演。若函数体仅包含一个表达式,则函数可以直接使用该表达式隐式返回。也就是说,若函数仅包含return ${some expression}一条语句,则函数体可以精简为${some expression}

func greeting(for person: String) -> String {
    "Hello, " + person + "!"
}
print(greeting(for: "Dave"))
// Prints "Hello, Dave!"

func anotherGreeting(for person: String) -> String {
    return "Hello, " + person + "!"
}
print(anotherGreeting(for: "Dave"))
// Prints "Hello, Dave!"

1.4 函数的Argument Labels 和 Parameter Names

Swift 函数的参数名由两大部分 Argument Labels 和 Parameter Names 组成。前者在调用函数时使用,后者在函数内部使用。默认情况下,参数将 Argument Label 用作 Parameter Name,因此前面看到的 Swift 函数参数名都只有一个。以下是 Swift 函数参数名的完全形态

// 1. 指定 Argument Labels 和 Parameter Names
func someFunction(argumentLabel parameterName: Int) {
    // In the function body, parameterName refers to the argument value
    // for that parameter.
}

// 2. 混合使用
func greet(person: String, from hometown: String) -> String {
    return "Hello \(person)!  Glad you could visit from \(hometown)."
}
print(greet(person: "Bill", from: "Cupertino"))
// Prints "Hello Bill!  Glad you could visit from Cupertino."

// 3. 有时函数名已经足以表述部分参数,可以不使用 Argument Labels
func someFunction(_ firstParameterName: Int, secondParameterName: Int) {
    // In the function body, firstParameterName and secondParameterName
    // refer to the argument values for the first and second parameters.
}
someFunction(1, secondParameterName: 2)

// 4. Swift 支持指定函数参数默认值
func someFunction(parameterWithoutDefault: Int, parameterWithDefault: Int = 12) {
    // If you omit the second argument when calling this function, then
    // the value of parameterWithDefault is 12 inside the function body.
}
someFunction(parameterWithoutDefault: 3, parameterWithDefault: 6) // parameterWithDefault is 6
someFunction(parameterWithoutDefault: 4) // parameterWithDefault is 12

// 5. 变长参数
func arithmeticMean(_ numbers: Double...) -> Double {
    var total: Double = 0
    for number in numbers {
        total += number
    }
    return total / Double(numbers.count)
}
arithmeticMean(1, 2, 3, 4, 5)
// returns 3.0, which is the arithmetic mean of these five numbers
arithmeticMean(3, 8.25, 18.75)
// returns 10.0, which is the arithmetic mean of these three numbers

// 6. 输入输出参数
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
    let temporaryA = a
    a = b
    b = temporaryA
}

// 调用带输入输出参数的函数的语法
var someInt = 3
var anotherInt = 107
swapTwoInts(&someInt, &anotherInt)
print("someInt is now \(someInt), and anotherInt is now \(anotherInt)")
// Prints "someInt is now 107, and anotherInt is now 3"

注意:通常将没有默认值的参数放前面,有默认值的参数放后面。函数变长参数只能有一个,放在参数列表最后面。变长参数传入到函数体内的类型实际是数组,例如Double...类型的变长参数传入到函数体内则是[Double]类型(不可空类型),可以使用for循环进行遍历。输入输出参数不可以指定默认值。

1.5 函数的类型

函数是在 Swift 语言体系中是一等公民,具有类型,既可以表示操作也可以作为数据传递,函数的类型用函数的参数类型和返回值类型表示。例如,以下两个函数的类型都是(Int, Int) -> Int

// 1. 声明两个函数类型 (Int, Int) -> Int 的函数
func addTwoInts(_ a: Int, _ b: Int) -> Int {
    return a + b
}
func multiplyTwoInts(_ a: Int, _ b: Int) -> Int {
    return a * b
}

// 2. 用函数类型声明一个变量。
var mathFunction: (Int, Int) -> Int = addTwoInts

// 3. 使用函数类型的变量
print("Result: \(mathFunction(2, 3))")
// Prints "Result: 5"

mathFunction = multiplyTwoInts
print("Result: \(mathFunction(2, 3))")
// Prints "Result: 6"

// 4. 函数类型也支持类型推断
let anotherMathFunction = addTwoInts
// anotherMathFunction is inferred to be of type (Int, Int) -> Int

// 5. 函数类型值作为函数的参数进行传递
func printMathResult(_ mathFunction: (Int, Int) -> Int, _ a: Int, _ b: Int) {
    print("Result: \(mathFunction(a, b))")
}
printMathResult(addTwoInts, 3, 5)
// Prints "Result: 8"

// 6. 函数类型值作为返回值
func stepForward(_ input: Int) -> Int {
    return input + 1
}
func stepBackward(_ input: Int) -> Int {
    return input - 1
}

// 定义返回函数类型的函数
func chooseStepFunction(backward: Bool) -> (Int) -> Int {
    return backward ? stepBackward : stepForward
}

// 将返回的函数值赋值给常量
var currentValue = 3
let moveNearerToZero = chooseStepFunction(backward: currentValue > 0)
// moveNearerToZero now refers to the stepBackward() function

// 使用该函数类型的常量
print("Counting to zero:")
// Counting to zero:
while currentValue != 0 {
    print("\(currentValue)... ")
    currentValue = moveNearerToZero(currentValue)
}
print("zero!")
// 3...
// 2...
// 1...
// zero!

二、闭包

闭包是自包含的功能代码块,可以在代码间传递和使用。Swift 的闭包类似于 C 语言和 Objective-C 语言中的 block,以及其他语言的 lumbda 表达式。

闭包可以捕获(capture)并保存定义闭包的代码的上下文中的任意常量或变量的引用。无需考虑捕获的常量或变量的内存管理问题,Swift 内置了对这些内存的管理机制。

全局函数(global function)和嵌套函数(nested function)是特殊形式的闭包:

  • 全局函数是命名闭包,不捕获任何值;
  • 嵌套函数是命名闭包,可以捕获外层函数(enclosing function)函数体中的值;
  • 闭包表达式是匿名闭包,以非常轻量的语法定义,可以捕获上下文中的值;

Swift 的闭包语法风格简洁。Swift 鼓励在日常开发过程中使用精炼、简洁的优化语法,包括:

  • 参数和返回值类型使用类型推断;
  • 单表达式的闭包使用隐式返回;
  • 使用参数名称缩写($0$1);
  • 使用尾随闭包语法;

2.1 闭包表达式

嵌套函数可以视为完全语法形态的闭包,而闭包表达式则具有更简洁的语法。闭包表达式的基本语法如下:

{ ($parameters) -> $return_type in
    $statements
}

但是 Swift 并不建议,在使用闭包的所有场景都使用完整的语法形式,Swift 鼓励使用精炼的优化语法。以下闭包语法的例程,可以看出 Swift 是如何执着于追求语法精炼的。总结其中优化语法有以下几种:

  • 闭包表达式本身就是对函数式闭包的优化,省略了闭包名称;
  • 类型推断:闭包表达式支持类型推断,从而可以省略闭包头部对参数类型和返回类型的声明;
  • 隐式返回:当闭包的函数体中只包含一条表达式,可以省略return关键字;
  • 参数缩写:闭包函数体中,可以用$0表示第一个参数,$1表示第二个参数,以此类推,因此可以省略闭包头部的整个参数列表;
  • 运算符有时可以表示闭包:下面的例子中,由于String类型重载了>运算符,而>运算符方法恰好是(String, String) -> Bool类型,在 Swift 强大的类型推断的支持下,因此只要在参数中传入>,Swift 就能推断出用String>运算符方法的实现作为传入参数;
// 1. 使用函数作为参数进行排序
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
func backward(_ s1: String, _ s2: String) -> Bool {
    return s1 > s2
}
var reversedNames = names.sorted(by: backward)
// reversedNames is equal to ["Ewa", "Daniella", "Chris", "Barry", "Alex"]

// 2. 使用闭包表达式实现,代码更加简洁了
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
    return s1 > s2
})

// 3. 再使用类型推断后,代码更更加简洁了
reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )

// 4. 再使用隐式返回后,代码更更更加简洁了
reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )

// 5. 再使用参数名称缩写后,代码更更更更加简洁了
reversedNames = names.sorted(by: { $0 > $1 } )

// 6. 究极进化。。。使用运算符表示闭包
reversedNames = names.sorted(by: >)

注意:Swift 官方文档中不止一次提到语法简洁这个问题。可见,Swift 这门语言是十分重视语法精炼的。日常开发过程中也应该使用尽量简洁的语法,当时也是建立在不影响代码可读性的前提上。

2.2 尾随闭包

如果函数的最后一个参数是闭包类型,而且该闭包的实现较长,此时尾随闭包的用法就可以派上用场了。上面排序的例程,可以用尾随闭包的形式调用,而且可以进一步省略()符号

// 0. 原始状态
reversedNames = names.sorted(by: { $0 > $1 } )

// 1. 使用尾随闭包作为函数最后一个参数
reversedNames = names.sorted() { $0 > $1 }

// 2. 尾随闭包省略圆括号
reversedNames = names.sorted { $0 > $1 }

// 3. 函数参数的实现体较长时,更能体现尾随闭包语法的强大
let digitNames = [
    0: "Zero", 1: "One", 2: "Two",   3: "Three", 4: "Four",
    5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine"
]
let numbers = [16, 58, 510]

let strings = numbers.map { (number) -> String in
    var number = number
    var output = ""
    repeat {
        output = digitNames[number % 10]! + output
        number /= 10
    } while number > 0
    return output
}
// strings is inferred to be of type [String]
// its value is ["OneSix", "FiveEight", "FiveOneZero"]

2.3 闭包捕获上下文

闭包可以捕获定义闭包的代码的上下文中的值,可以在闭包的实现体中使用或修改所捕获的上下文中的值,即使是超出该值的作用域。

最简单的闭包捕获上下文的场景是嵌套函数,嵌套函数可以捕获外层函数的传入参数,或者外层函数中定义的常量或变量。例如:

func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementer() -> Int {
        // 捕获到 runningTotal 的值为 0
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}

使用makeIncrementer构建函数闭包的实现:

let incrementByTen = makeIncrementer(forIncrement: 10)
incrementByTen()
// returns a value of 10
incrementByTen()
// returns a value of 20
incrementByTen()
// returns a value of 30

若紧接着开始第二轮递增操作,此时闭包捕获的runningTotal变量的初始值还是0,而不是上面递增完的30。这是因为每次调用makeIncrementer(forIncrement:)构建函数闭包时,都会使用独立的内存空间存储捕获值。也就是说,前后两次构建的闭包的捕获值存储在两块互补影响的内存空间。因此,紧接着再调用一次incrementByTen,返回值将会是40

let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()
// returns a value of 7

incrementByTen()
// returns a value of 40

从上面的例程发现 Swift 的变量值捕获机制和 Objective-C 的__block变量很相似,我们用一个例程验证一下。果然内嵌函数外层函数的捕获变量值变更,和闭包内部对不或变量的修改是会相互影响的。

func makeIncrementerTest(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    runningTotal = 100

    // 1. 闭包内部修改捕获变量值会影响外层函数的原始变量
    let incrementTest = incrementer
    print(incrementTest())
    // runningTotal = 110

    return incrementer
}

// 2. 外层函数修改原始变量值值会影响闭包的捕获变量值
let incrementTest = makeIncrementerTest(forIncrement: 10)
print(incrementTest())
// runningTotal = 120

注意:实际上 Swift 的var类型捕获变量相当于 Objective-C 的__block变量;let类型捕获常量相当于 Objective-C 的普通捕捉变量。连行为都是相同的。Swift 闭包捕获对象的引用时,闭包也会强引用该对象,此时若将闭包设置为对象的某个属性,则会形成循环引用,这个问题通过 capture lists 解决。后面看完类和对象才能介绍。

2.4 闭包是引用类型

也就是说 Swift 中的闭包也是一个对象,将闭包赋值给某个变量,该变量则是闭包对象的引用,此时将该变量赋值给另一个变量,则只是将另一个变量指向相同的闭包对象。这一点上和 Objective-C 的 block 也没什么区别。以下例程紧接

let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
// returns a value of 50

incrementByTen()
// returns a value of 60

2.5 逃逸闭包

当闭包作为函数的参数传入到函数,但是该闭包参数会在函数返回后调用,这种闭包称为逃逸闭包(escaping closure),这种行为称为闭包从函数逃逸(escape a function)。声明函数时,对闭包类型参数添加@escape,表示该闭包参数允许从函数中逃逸。

提问:为什么 Swift 需要声明逃逸闭包?这个应该是为了区分存在循环引用风险的闭包类型的参数。如果闭包类型参数只是在函数返回前调用,则闭包是不存在循环引用风险的,因为这种闭包不应被函数的上下文(例如self或者self强持有的某个成员变量)所持有。

// 1. 逃逸闭包定义
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
    completionHandlers.append(completionHandler)
}

// 2. 下列代码会报编译错误:Using non-escaping parameter 'opt' in a context expecting an @escaping closure
func biOpt(opt:(Int, Int) -> Int) -> (Int, Int) -> Int{
    return opt
}

当把闭包声明为逃逸闭包时,逃逸闭包内使用对象的成员时,必须显示地调用self,因为逃逸闭包需要显式地持有self对象,而且可以防止闭包捕获的成员与逃逸闭包真正调用时与所在上下文存在同名变量歧义,最重要的一点,时刻提醒你提防循环引用的可能性。

func someFunctionWithNonescapingClosure(closure: () -> Void) {
    closure()
}

class SomeClass {
    var x = 10
    func doSomething() {
        someFunctionWithEscapingClosure { self.x = 100 }
        someFunctionWithNonescapingClosure { x = 200 }
    }
}

let instance = SomeClass()
instance.doSomething()
print(instance.x)
// Prints "200"

completionHandlers.first?()
print(instance.x)
// Prints "100"

2.6 自动闭包

自动闭包(automatic closure)是用作函数的参数,仅封装了一个表达式的闭包。自动闭包不包含任何参数,当调用自动闭包时,直接返回闭包中的表达式的值。

自动闭包语法有利于在调用函数时,省略闭包类型参数的{}而直接传入表达式,又是为了更精炼的代码。另外,由于函数的闭包参数内的表达式,要等到正式调用该函数时,才会真正执行,因此该语法有利于延迟参数值的计算。延迟参数值计算(delay evaluation)在代码具有副作用、计算花销比较大的场景中具有重要意义。

声明自动闭包使用@autoclosure关键字修饰函数的闭包参数类型,在调用该函数时,在自动闭包类型的参数位置传入一个表达式。其实本质就是实现函数参数值的懒加载。

// 1. 数值需要延迟计算的例子
var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine.count)
// Prints "5"

let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count)
// Prints "5"

print("Now serving \(customerProvider())!")
// Prints "Now serving Chris!"
print(customersInLine.count)
// Prints "4"

// 2. 普通闭包实现数值延迟计算。调用函数时,参数传入一个闭包
// customersInLine is ["Alex", "Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: () -> String) {
    print("Now serving \(customerProvider())!")
}
serve(customer: { customersInLine.remove(at: 0) } )
// Prints "Now serving Alex!"

// 3. 自动闭包实现数值延迟计算。调用函数时,参数传入一个表达式
// customersInLine is ["Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: @autoclosure () -> String) {
    print("Now serving \(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0))
// Prints "Now serving Ewa!"

关于类型推断:Swift 语言具有强大类型推断,但是刚开始写 Swift 代码时总是在类型推断上碰壁,写代码总是等抛出编译错误。个人总结 Swift 的类型推断大致是按从外到内,自顶向下,从右向左的优先级。举三个例子:闭包的类型什么时候能省略,是要在调用闭包的地方,根据上下文推断闭包类型,调用闭包的代码上下文明显是“外部”;上面声明了let a:Int = 10,那么 Swift 就可以推断出下面let b = ab的类型等同于a的类型;对语句let a = 10,Swift 可以从右边的字面量10直接推断出a的类型是Int

三、总结

  • 函数在 Swift 中是一等公民,既是操作,也是数据;可以被调用,也可以作为数据在模块或函数间传递:
    • 函数也具有类型,用参数类型和返回值类型联合表示,也支持类型推断;
    • 支持按元组返回;
    • 支持内嵌函数;
    • 支持隐式返回;
    • 函数的参数包含 argument label 和 parameter name 两个部分,分别供调用者和函数体内部使用,默认是两者相同只需提供一个参数名,也可以用_符号忽略 argument label;
    • 函数调用时,也可以用_符号忽略返回值;
    • 函数支持变长参数,放参数列表最后,传到函数体内为[${SomeType}]类型;
    • 函数支持参数默认值指定,通常不带默认值的参数放参数列表前面,带默认值的放后面;
    • 函数参数和返回值类型都支持可空类型;
  • 闭包是自包含的,可以捕获上下文的功能代码块:
    • 全局函数是不能捕捉上下文的,有名称的特殊闭包;
    • 内嵌函数是可以捕捉上下文的,有名称的特殊闭包;
    • 闭包类型支持类型推断;
    • 闭包支持隐式返回;
    • 闭包支持参数缩写;
    • 闭包支持使用运算符表示;
    • 闭包捕获上下文值的行为取决于值的可读写属性,若声明为let则与 Objective-C 的普通变量捕获基本一致,若声明为var则与 Objective-C 的__block变量捕获基本一致;
    • 尾随闭包是,当函数的最后一个参数是闭包时,调用该函数时可以以尾随闭包的形式传入该闭包参数;
    • 逃逸闭包是,当函数传入闭包类型的参数,却没有在函数体中调用该闭包,例如闭包被当成返回值返回,或者在函数体中指定了闭包被其他对象持有,待返回后才会被调用,则需要在函数签名中,用@escape将该闭包参数的声明为逃逸闭包,否则会抛编译错误。逃逸闭包必须强调显式捕获self,以提醒开发者提防循环引用;
    • 自动闭包是,当函数传入的闭包类型参数,仅包含一个表达式,此时可以在函数签名中,用@autoclosure将该闭包参数的声明为自动闭包,则在调用函数时在该参数位置可以直接传入原闭包中的唯一的那条表达式。使用自动闭包通常是为了延迟参数值的计算,自动闭包是语法优化,目的只是为了代码更简练;