作者:Aaron Dean,日期:2020.05.06

思维导图

1. 函数

1.1 函数的声明和调用

函数(function)是执行特定任务的独立代码块,使用 func 关键字声明函数,确定一个函数名(function name),并使用 -> 分隔参数表列(arguments)和函数的返回类型(return type)。函数名描述函数能够执行的任务、参数表列说明函数期望接收参数个数和类型,返回类型表示当函数完成后应返回的内容。Swift中的每个函数都有一个类型,包括该函数的参数类型和返回类型。

调用函数的方式是:函数名( 参数)。将与该函数的参数类型匹配的输入值(称为自变量)传递给它时, 必须始终以与函数参数列表相同的顺序提供函数的参数。无论函数有无参数,这对圆括号 ( ) 都不能少。

//	函数的声明和调用
func myFavoriteBook(book: String, author: String) -> String {
    return "我最喜欢的书是\(book), 作者是\(author). \n那你最喜欢的书是什么?"
}
print(myFavoriteBook(book: "<<平凡的世界>>", author: "路遥"))

//我最喜欢的书是<<平凡的世界>>, 作者是路遥. 
//那你最喜欢的书是什么?

1.2 函数的参数和返回值

  • 无参数的函数
func sayHelloWorld() -> String {
    return "hello, world"
}
print(sayHelloWorld())
//  "hello, world"
  • 具有多个参数的函数
func greet(person: String, alreadyGreeted: Bool) -> String {
    if alreadyGreeted {
        return greetAgain(person: person)
    } else {
        return greet(person: person)
    }
}
print(greet(person: "Tim", alreadyGreeted: true))
// "Hello again, Tim!"
  • 无返回类型的函数
func greet(person: String) {
    print("Hello, \(person)!")
}
greet(person: "Dave")
// Prints "Hello, Dave!"
  • 具有多个返回值的函数

使用元组(tuple)可以将多个值作为一个单一的复合返回值从函数中返回。可以通过元组的元素名或序号引用元素。

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 a = minMax(array: [2,3,4,5])
print(a)
//(min: 2, max: 5)

可选的元组返回类型(Optional Tuple Return Types)

//为了安全地处理空数组

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)
}

你可以使用可选绑定来检查此版本的minMax(array :)函数是否返回实际的元组值或nil:

if let bounds = minMax(array: [8, -6, 2, 109, 3, 71]) {
    print("min is \(bounds.min) and max is \(bounds.max)")
}
//  "min is -6 and max is 109"
  • 隐式返回函数(Functions With an Implicit Return)

函数主体只有单个表达式,则该函数隐式返回该表达式,即不用显式地写上 return 关键字。

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.3 函数的实参标签和形参名

1.3.1 明确指定或省略实参标签

每个函数参数都具有实参标签(argument label)和形参名称(parameter name),如示例代码中的函数 by是实参标签,author 是形参名称。 实参标签在调用函数时使用; 每个实参都写在函数调用中,并在实参前写上其参数标签。 形参名称在功能的实现中使用。函数默认使用形参名(parameter names)作为实参标签。你也可以在形参名前指定一个实参标签,或者在形参名前写上”_”使用无标签实参。所有形参名称必须唯一。 另外,尽管实参标签可以相同,但不同的实参标签能使代码可读性更好。你可以:

  • 明确指定实参标签,如示例代码中的 author 前的 by。
  • 省略实参标签,使用下划线(_),如 book 前的实参标签。
func myFavoriteBook(_ book: String, by author: String) -> String {
    return "我最喜欢的书是\(book), 作者是\(author). \n你最喜欢的书是什么?"
}
print(myFavoriteBook("<<平凡的世界>>", by: "路遥"))

另外,你可以为函数中任何的形参指定默认值。

1.3.2 可变形参

可变参数(variadic parameter)接受零个或多个指定类型的值。 可以使用可变参数来指定在调用函数时可以向该参数传递不同数量的输入值。 通过在参数的类型名称后插入三个句点(…)来编写可变参数。

//求均值
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)
// 结果:3
arithmeticMean(3, 8.25, 18.75)
// 结果:10

1.3.3 输入-输出形参

函数形参默认为常量。 试图从函数主体内部更改函数参数的值会导致编译时错误。 这意味着你不能错误地更改参数的值。 如果你希望函数修改参数的值,并且希望这些更改在函数调用结束后仍然存在,请将该参数定义为输入输出形参(in-out parameter)

只能将变量作为输入输出参数的参数传递。 在将变量作为输入/输出参数的参数传递给变量名称时,可以在其名称前直接放置一个符号,以表明该函数可以对其进行修改。

输入输出形参不能指定默认值,可变参数不能标记为输入输出形参。

//交换两个整数
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"

1.4 函数类型

1.4.1 函数类型的概念

每个函数都有一个特定的函数类型(function type),包括参数类型和返回类型。比如,函数addTwoInts和multiplyTwoInts的类型均为:(Int, Int) -> Int,意思是这两个函数都是:具有两个参数(均为Int类型)和返回值为Int类型的值的函数。而函数printHelloWorld的类型为为() -> Void,既无参数,无也返回值。

//	(Int, Int) -> Int
func addTwoInts(_ a: Int, _ b: Int) -> Int {
    return a + b
}
func multiplyTwoInts(_ a: Int, _ b: Int) -> Int {
    return a * b
}

//	() -> Void:无参数,无返回值
func printHelloWorld() {
    print("hello, world")
}

1.4.2 函数类型的使用

就像 Swift 中其他类型一样使用。如下示例,定义了一个名为mathFunction的变量,其类型为“一个具有两个Int值的函数,并返回一个Int值。”将此新变量设置为引用名为addTwoInts的函数。

  • 可以使用与非函数类型相同的方式,将具有相同匹配类型的不同函数分配给同一变量。
  • 可以使用类型推断。
var mathFunction: (Int, Int) -> Int = addTwoInts
print("Result: \(mathFunction(2, 3))")
// Result: 5

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

let anotherMathFunction = addTwoInts
// anotherMathFunction 的类型推断为 (Int, Int) -> Int

1.4.3 函数类型作形参类型

您可以使用诸如(Int,Int)-> Int之类的函数类型作为另一个函数的参数类型。 这样,您就可以将函数实现的某些方面留给函数的调用方在调用函数时提供。

//函数类型作形参类型
func printMathResult(_ mathFunction: (Int, Int) -> Int, _ a: Int, _ b: Int) {
    print("Result: \(mathFunction(a, b))")
}
printMathResult(addTwoInts, 3, 5)
// Result: 8

1.4.4 函数类型作返回类型

您可以将一个函数类型用作另一个函数的返回类型。 为此,您可以在返回函数的返回箭头(->)之后立即编写完整的函数类型。

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!

1.5 函数嵌套

函数可以嵌套:嵌套函数可以访问在函数外声明的变量。使用嵌套函数的好处是,可以避免冗长的复杂代码,使代码结构更清晰。

func chooseStepFunction(backward: Bool) -> (Int) -> Int {
    func stepForward(input: Int) -> Int { return input + 1 }
    func stepBackward(input: Int) -> Int { return input - 1 }
    return backward ? stepBackward : stepForward
}
var currentValue = -4
let moveNearerToZero = chooseStepFunction(backward: currentValue > 0)
// moveNearerToZero now refers to the nested stepForward() function
while currentValue != 0 {
    print("\(currentValue)... ")
    currentValue = moveNearerToZero(currentValue)
}
print("zero!")
// -4...
// -3...
// -2...
// -1...
// zero!

函数是一等类型(first-class type),这意味着函数可以将另一个函数作为返回值返回。

函数可以使用另一个函数做它的参数。

2. 闭包(重点:闭包表达式)

2.1 闭包的概念

闭包是独立的功能块,可以在代码中传递和使用。Swift中的闭包类似于C和Objective-C中的块以及其他编程语言中的lambda。实际上,全局函数和嵌套函数是闭包(closures)的特殊情况

闭包的三种形式:

  • 全局函数(global functions):具有名称且不捕获任何值的闭包。
  • 嵌套函数(nested functions ):具有名称的闭包,可以从其闭包函数捕获值。
  • 闭包表达式(Closure expressions):用轻量级语法编写的未命名闭包,可以从其周围的上下文中(context)捕获值。

Swift的闭包表达式简洁明了,并做了优化:

  • 从上下文中推断参数和返回类型
  • 单闭包表达式的隐式返回(不用写 return关键字)
  • 速记实参名称
  • 尾随闭包语法

2.2 简化闭包表达式

//	闭包表达式

{(parameters) -> return type in
	statements
}

闭包表达式是一种以简短,集中的语法编写内联闭包的方法。有时并不需要完整的函数声明和名称,这时可以编写类似函数结构的简洁的闭包表达式。特别是当你打算使用以函数作为其一个或多个参数的函数或方法时很有用。

Swift 标准库提供了 sorted(by:) 方法,该方法根据你提供的排序闭包的输出对已知类型的值数组进行排序。

// sorted(by:)方法

let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

// 常规写法
func backward(_ s1: String, _ s2: String) -> Bool {
    return s1 > s2
}
var reversedNames = names.sorted(by: backward)
//reversedNames等于["Ewa", "Daniella", "Chris", "Barry", "Alex"]

但是,上面这段代码还可以更简单,即使用闭包表达式语法内联编写排序闭包。对于内联闭包(inline closure)表达式,参数和返回类型写在花括号内,而不是花括号外。闭包正文的开头由 in 关键字引进, 此关键字表示闭包的参数和返回类型的定义已完成,且闭包主体即将开始。闭包主体时很短时,可以写在一行。

var reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in 
		return s1 > s2 
})

从上下文中推断类型:需要在“省略”和代码的可读性之间做好平衡。

var reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )

单表达式闭包可以通过从声明中省略 return 关键字来隐式返回其单表达式的结果:

var reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )

速记实参名称(Shorthand Argument Names):Swift会自动为内联闭包提供速记实参名称,可使用$ 0,$ 1,$ 2等名称来引用闭包参数第 1、第 2、第 3等的值。

var reversedNames = names.sorted(by: { $0 > $1 } )

运算符方法(Operator Methods):这是一种更短的书写闭包表达式的语法。 Swift的String类型将大于运算符(>)的特定于字符串的实现定义为一种方法,该方法具有两个String类型的参数,并返回Bool类型的值。 这与sorted(by :)方法所需的方法类型完全匹配。 因此,您只需传递大于号运算符,Swift就会推断您要使用其特定于字符串的实现:

var reversedNames = names.sorted(by: >)

2.3 尾随闭包

若你需要将闭包表达式作为函数的最终参数传递给函数,而闭包足够长而无法在一行上内联写入时,则将其写为尾随闭包(Trailing Closures)最有用。 即使在函数调用的参数后面,也要在函数调用的括号后面加上结尾的闭包。 使用尾随闭包语法时,请勿在函数调用的过程中为闭包编写参数标签。

func someFunctionThatTakesAClosure(closure: () -> Void) {
    // 函数体
}

// 未使用尾随闭包时的函数调用

someFunctionThatTakesAClosure(closure: {
    // 闭包体
})

// 使用尾随闭包时的函数调用

someFunctionThatTakesAClosure() {
    // 尾随闭包体
}		

可以将之前的一条代码改写为使用尾随闭包的形式。如果将闭包表达式作为函数或方法的唯一参数提供,且将该表达式作为尾随闭包,则在调用函数时,无需在函数或方法的名称后写一对括号():

//未使用尾随闭包的形式
var reversedNames = names.sorted(by: { $0 > $1 } )

//使用尾随闭包改写
var reversedNames = names.sorted(){ $0 > $1 }

//省略函数或方法名后的括号()
var reversedNames = names.sorted { $0 > $1 }

闭包足够长而无法在一行上内联写入时,则将其写为尾随闭包(Trailing Closures)最有用。Swift 的 Array 类型的 map(_:) 方法将闭包表达式作为其单个参数。该闭包将为数组中的每个项目调用一次,并返回该项目的替代映射值(可能是其他类型的映射值)。 映射的性质和返回值的类型由闭包指定。

在将提供的闭包应用于每个数组元素之后,map(_ :)方法返回一个包含所有新映射值的新数组,其顺序与原始数组中相应值的顺序相同。示例如下:

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 的类型推断为 [String]
// 值为: ["OneSix", "FiveEight", "FiveOneZero"]

map(_ :)方法为数组中的每个项目调用一次闭包表达式。 您无需指定闭包的输入参数number的类型,因为可以从要映射的数组中的值推断出该类型。

在此示例中,变量数字使用闭包的number参数的值初始化,以便可以在闭包主体中修改该值。 (函数和闭包的参数始终是常量。)闭包表达式还指定String的返回类型,以指示将存储在映射的输出数组中的类型。

闭包表达式每次被调用时都会构建一个名为output的字符串。 它使用余数运算符(数字%10)计算数字的最后一位,并使用该数字在digitNames词典中查找适当的字符串。 闭包可用于创建任何大于零的整数的字符串表示形式。

2.4 值捕获

闭包能够从其周围的上下文捕获常量和变量,闭包能够在闭包主体中引用和修改这些常量和变量的值,即使定义常量和变量的原始范围不再存在。在 Swift 中,能捕获值的最简单的闭包就是嵌套函数。嵌套函数可以捕获其外部函数的任何参数,也可以捕获在外部函数中定义的任何常量和变量。

示例是一个名为 makeIncrementer 的函数,其中包含一个嵌套的函数,称为incrementer。 嵌套的 incrementer()函数从其周围的上下文中捕获两个值,runningTotal和amount。 捕获这些值之后,makeIncrementer将incrementer作为闭包返回,闭包将在每次调用时将runningTotal增加一个数量。

//makeIncrementer的返回类型是:() -> Int,即将一个函数作为返回值
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}

我们来单独看一下嵌套在内部的函数 incrementer , 它通过捕获对周围函数的runningTotal和amount的引用并在其自己的函数体内使用它们。 通过引用捕获可以确保在对 makeIncrementer 的调用结束时 runningTotal 和 amount 不会消失,并且还可以确保下次调用递增器函数时runningTotal可用。

let incrementByTen = makeIncrementer(forIncrement: 10)

incrementByTen()
// 返回 10
incrementByTen()
// 返回 20
incrementByTen()
// 返回 30

let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()
// 返回 7

incrementByTen()
// 返回 40

2.5 闭包是引用类型

闭包是引用类型(Closures Are Reference Types),即将某个闭包分配给两个不同的常量或变量,则这些常量或变量都引用同一个闭包,而不是复制值。

let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
// 返回 50

incrementByTen()
// 返回 60

2.6 转义闭包

当闭包作为函数的参数传递给闭包时,闭包被认为是对函数的转义,但是在函数返回后被调用。 声明将闭包作为其参数之一的函数时,可以在参数的类型前写@escaping,以指示允许转义闭包。

闭包可以逃脱的一种方法是将其存储在函数外部定义的变量中。 例如,许多启动异步操作的函数都将闭包参数用作完成处理程序。 该函数在开始操作后返回,但是直到操作完成后才调用闭包,闭包需要转义,稍后再调用。

var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
    completionHandlers.append(completionHandler)
}

someFunctionWithEscapingClosure(_ :)函数将闭包作为参数,并将其添加到在函数外部声明的数组中。 如果不使用@ecaping标记此函数的参数,则会出现编译时错误。

用@escaping标记闭包意味着您必须在闭包内显式引用self。 例如,在下面的代码中,传递给someFunctionWithEscapingClosure(_ :)的闭包是转义的闭包,这意味着它需要显式地引用self。 相比之下,传递给someFunctionWithNonescapingClosure(_ :)的闭包是不冒号的闭包,这意味着它可以隐式引用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)
// 输出 :200

completionHandlers.first?()
print(instance.x)
// 输出 :100

2.7 自动闭包(Autoclosures)

自动闭包(autoclosure)是一种自动创建的闭包,用于包装作为实参传递给函数的表达式。它不带任何参数,调用它时,它返回包装在其中的表达式的值。这种语法上的便利性使您可以通过编写正则表达式而不是显式闭包来省略函数参数的花括号。

调用采用自动闭包的函数很常见,但是实现这种函数并不常见。例如,assert(condition:message:file:line :)函数会自动关闭其条件和消息参数;仅在调试版本中评估其条件参数,并且仅在condition为false时评估其message参数。

自动闭包可让您延迟评估,因为在调用自动闭包之前,内部代码不会运行。延迟评估对于具有副作用或计算量大的代码很有用,因为它使您可以控制何时评估该代码。下面的代码显示了闭包如何延迟评估。

var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine.count)
// 输出: 5

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

print("Now serving \(customerProvider())!")
//  输出: Now serving Chris!
print(customersInLine.count)
// 输出: 4

即使customerInLine数组的第一个元素已由闭包中的代码删除,但只有在实际调用闭包时才删除array元素。 如果从不调用闭包,则闭包内部的表达式将永远不会求值,这意味着数组元素不会被删除。 请注意,customerProvider的类型不是String,而是()-> String-没有参数的函数,它返回字符串。

将闭包作为函数的参数传递时,您会得到延迟求值的相同行为。

// customersInLine 是 ["Alex", "Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: () -> String) {
    print("Now serving \(customerProvider())!")
}
serve(customer: { customersInLine.remove(at: 0) } )
// 输出: Now serving Alex!

上面清单中的serve(customer :)函数采用显式闭包,返回客户的姓名。 下面的serve(customer :)版本执行相同的操作,但是它采用了@autoclosure属性标记其参数类型,而不是进行显式关闭,而是采用了自动闭包。 现在,您可以像调用String参数而不是使用闭包一样调用该函数。 该参数会自动转换为闭包,因为customerProvider参数的类型已用@autoclosure属性标记。

// customersInLine 是 ["Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: @autoclosure () -> String) {
    print("Now serving \(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0))
// 输出: Now serving Ewa!

过度使用自动闭包会使您的代码难以理解。 上下文和函数名称应清楚表明评估被推迟。

如果要允许自动转义可以转义,请同时使用@autoclosure和@escaping属性。

// customersInLine 是 ["Barry", "Daniella"]
var customerProviders: [() -> String] = []
func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) {
    customerProviders.append(customerProvider)
}
collectCustomerProviders(customersInLine.remove(at: 0))
collectCustomerProviders(customersInLine.remove(at: 0))

print("Collected \(customerProviders.count) closures.")
// 输出: Collected 2 closures.
for customerProvider in customerProviders {
    print("Now serving \(customerProvider())!")
}
// 输出: Now serving Barry!
// 输出: Now serving Daniella!

参考资料:

  1. https://docs.swift.org/swift-book/LanguageGuide/Functions.html
  2. https://docs.swift.org/swift-book/LanguageGuide/Closures.html

文档协议:

CC-BY-4.0(共享-演绎-署名-不附加限制)