Swift闭包完全指南:从入门到精通

Viewed 0

今天我们一起来学习闭包(Closure)。对于初学Swift的人来说,闭包可能是第一道坎,它难以理解但极其有用。即使一开始觉得困难,也值得花时间去掌握,因为它遍布Swift和SwiftUI的各个角落。

闭包可以被看作匿名的函数块。你可以创建它并直接赋值给变量,或者传递给其他函数以自定义其行为。没错,你可以将一个函数作为参数传递给另一个函数。

掌握闭包确实需要一些努力。它的语法起初可能令人望而生畏,但请记住,几乎所有Swift开发者都经历过这个阶段。如果你在看代码时感到不确定,回去再看一遍,慢慢理解。这就像骑自行车上山,一旦到达山顶,一切都会变得轻松。

如何创建和使用闭包

函数在Swift中是一等公民。你不仅可以调用它们,还可以将它们赋值给变量、作为参数传递,甚至从函数中返回函数。

例如,先定义一个函数,然后复制它:

func greetUser() {
    print(“Hi there!”)
}

greetUser()

var greetCopy = greetUser
greetCopy()

这段代码会打印两次“Hi there!”。注意,复制函数时不要加括号(),否则你将调用函数并存储其返回值,而不是函数本身。

更直接的方法是使用闭包表达式创建一个匿名函数块:

let sayHello = {
    print(“Hi there!”)
}

sayHello()

这个闭包没有参数,也不返回值。如果你想让它接受参数和返回值,语法略有不同:

let sayHello = { (name: String) -> String in
    “Hi \(name)!”
}

这里出现了in关键字,它用来分隔闭包的参数/返回类型定义和其主体代码。因为闭包的参数和返回值必须写在大括号{}内部,in起到了标记分界的作用。

你可能会问:为什么需要闭包?它们看起来复杂且晦涩。然而,闭包在Swift中无处不在,尤其是在SwiftUI中。理解闭包是编写现代Swift代码的关键。

要深入理解闭包,需要先了解函数类型。就像整数有Int类型,函数也有自己的类型。考虑之前的greetUser函数,它不接受参数,不返回值。它的类型是() -> Void

  • () 表示没有参数。
  • -> 指向返回类型。
  • Void 表示“无”,即不返回任何内容。有时也写作(),但为避免混淆,通常使用Void

每个函数的类型都由其接收和返回的数据类型决定。一个重要的细节是:函数的外部参数名不属于其类型的一部分。通过以下代码可以验证:

func getUserData(for id: Int) -> String {
    if id == 1989 {
        return “Taylor Swift”
    } else {
        return “Anonymous”
    }
}

let data: (Int) -> String = getUserData
let user = data(1989) // 注意,这里调用时没有使用外部参数名‘for:‘
print(user)

当我们复制getUserData函数时,复本data的类型是(Int) -> String,调用时只需传入Int,而不需要原来的外部参数名for:。这个规则同样适用于闭包的调用。

闭包的实用性在哪里?一个经典例子是数组的sorted()方法。默认情况下,它可以排序:

let team = [“Gloria”, “Suzanne”, “Piper”, “Tiffany”, “Tasha”]
let sortedTeam = team.sorted()
print(sortedTeam)

但如果你想自定义排序规则呢?比如,让队长“Suzanne”始终排在最前面,其他人按字母顺序排列。sorted(by:)方法允许你传入一个自定义排序函数。这个函数必须接受两个相同类型的元素(这里是String),并返回一个Bool,指示第一个元素是否应该排在第二个元素之前。

我们可以先定义一个这样的函数:

func captainFirstSorted(name1: String, name2: String) -> Bool {
    if name1 == “Suzanne” {
        return true
    } else if name2 == “Suzanne” {
        return false
    }
    return name1 < name2
}

然后将其传递给sorted(by:)

let captainFirstTeam = team.sorted(by: captainFirstSorted)
print(captainFirstTeam) // 输出: [“Suzanne”, “Gloria”, “Piper”, “Tasha”, “Tiffany”]

闭包的强大之处在于,你无需预先定义命名函数。你可以直接将一个闭包(匿名函数)传递给sorted(by:)

let captainFirstTeam = team.sorted(by: { (name1: String, name2: String) -> Bool in
    if name1 == “Suzanne” {
        return true
    } else if name2 == “Suzanne” {
        return false
    }
    return name1 < name2
})

虽然代码量看起来不少,但它的好处是:你可以直接将定制逻辑内联写入,而无需为了单一用途专门创建一个函数。接下来,我们将看到如何让这段代码变得更简洁。

为什么Swift那么喜欢使用闭包?

闭包是Swift最强大也最具挑战的特性之一。Swift(尤其是SwiftUI)广泛使用闭包的一个核心原因是其存储和延迟执行的能力。你可以说“在将来某个时刻执行这段代码”,例如:

  • 延迟一段时间后执行。
  • 动画完成后执行。
  • 网络下载完成后执行。
  • 用户选择菜单项后执行。

闭包让我们能将功能“打包”进一个变量,存储起来,并在需要时执行或传递。

为什么Swift的闭包参数在大括号内?

闭包和普通函数声明参数的位置不同。普通函数参数在括号内:

func pay(user: String, amount: Int) { }

而闭包将参数列表放在大括号内:

let payment = { (user: String, amount: Int) in
    // 代码
}

这样做主要是为了避免语法歧义。如果写成let payment = (user: String, amount: Int),看起来就像在创建一个元组。将参数放在大括号内,配合in关键字,清晰地定义了闭包这个完整的、可存储的代码块的结构。

如何从不带参数的闭包返回值?

声明闭包的返回类型时,如果闭包没有参数,必须使用空括号()

  • 无参数无返回值:
    let payment = { () -> Void in
        print(“Paying an anonymous person…”)
    }
    
  • 无参数但有返回值:
    let payment = { () -> Bool in
        print(“Paying an anonymous person…”)
        return true
    }
    

这与声明函数func payment() -> Bool的逻辑是一致的。

如何使用尾部闭包和速记语法

为了使闭包代码更简洁,Swift提供了多种语法糖。回顾之前的复杂闭包调用:

let captainFirstTeam = team.sorted(by: { (name1: String, name2: String) -> Bool in
    // … 排序逻辑
})

第一步:类型推断
由于sorted(by:)明确要求一个(String, String) -> Bool类型的函数,我们可以省略闭包内的参数类型和返回类型声明:

let captainFirstTeam = team.sorted(by: { name1, name2 in
    // … 排序逻辑
})

第二步:尾部闭包语法
如果函数的最后一个参数是闭包,你可以使用尾部闭包语法。将闭包写在函数调用的小括号()之后。

let captainFirstTeam = team.sorted { name1, name2 in
    if name1 == “Suzanne” {
        return true
    } else if name2 == “Suzanne” {
        return false
    }
    return name1 < name2
}

这移除了by:标签和参数列表外的括号,代码看起来更清爽。
第三步:速记参数名
Swift为闭包提供了自动生成的速记参数名:$0, $1, $2……分别代表第一个、第二个、第三个参数。使用它们可以进一步简化:

let captainFirstTeam = team.sorted {
    if $0 == “Suzanne” {
        return true
    } else if $1 == “Suzanne” {
        return false
    }
    return $0 < $1
}

当逻辑非常简单时,比如只是反转排序,代码可以精简到极致:

let reverseTeam = team.sorted { $0 > $1 }
// 甚至可以省略‘return‘,因为只有一行表达式
let reverseTeam = team.sorted { $0 > $1 }

何时使用速记语法? 没有硬性规定,但个人建议在以下情况避免使用:

  1. 闭包逻辑较长、较复杂。
  2. 速记参数名(如$0)被多次重复使用。
  3. 参数超过两个($2, $3),可读性会下降。

闭包的简洁语法使其在处理集合时非常强大,例如:

  • filter: 筛选数组中满足条件的元素。
    let tOnly = team.filter { $0.hasPrefix(“T”) } // [“Tiffany”, “Tasha”]
    
  • map: 将数组中的每个元素转换成另一种形式。
    let uppercaseTeam = team.map { $0.uppercased() } // [“GLORIA”, “SUZANNE”, …]
    

在SwiftUI中,闭包更是无处不在,用于定义视图内容、处理按钮动作、配置列表等。

Swift为什么使用尾部闭包语法?

尾部闭包语法旨在提升代码的可读性,特别是在闭包体较长时。对比以下两种写法:

// 传统写法
animate(duration: 3, animations: {
    print(“Fade out the image”)
})

// 尾部闭包写法
animate(duration: 3) {
    print(“Fade out the image”)
}

尾部闭包写法消除了结尾的}),使代码结构更清晰。当函数名(如animate)能清晰表达闭包所执行的操作时,这种语法显得非常自然。建议初学者积极尝试使用尾部闭包,以熟悉这种Swift中常见的编码风格。

如何接受函数作为参数

最后,我们来探讨如何编写接受函数作为参数的函数。这在创建高阶函数时非常有用。

首先,回忆函数类型注解:

var greetCopy: () -> Void = greetUser

编写一个生成数组的函数,它接受一个“生成器”函数作为参数:

func makeArray(size: Int, using generator: () -> Int) -> [Int] {
    var numbers = [Int]()
    for _ in 0..<size {
        let newNumber = generator() // 调用传入的函数
        numbers.append(newNumber)
    }
    return numbers
}

这个函数做了两件事:

  1. 它名为makeArray,接受一个Int类型的size参数和一个() -> Int类型的generator参数,返回一个[Int]
  2. 在函数体内,它循环size次,每次调用generator函数来获取一个整数,并将其添加到数组中。

调用这个函数时,你可以传入一个闭包:

let rolls = makeArray(size: 50) {
    Int.random(in: 1…20)
}

也可以传入一个预定义的函数:

func generateNumber() -> Int {
    Int.random(in: 1…20)
}

let newRolls = makeArray(size: 50, using: generateNumber)

多个尾部闭包
一个函数可以接受多个函数参数。在调用时,第一个闭包使用标准的尾部闭包语法,后续的闭包则需要带上参数标签。

func doImportantWork(first: () -> Void, second: () -> Void, third: () -> Void) {
    print(“About to start first work”)
    first()
    print(“About to start second work”)
    second()
    print(“About to start third work”)
    third()
    print(“Done!”)
}

// 调用
doImportantWork {
    print(“This is the first work”)
} second: {
    print(“This is the second work”)
} third: {
    print(“This is the third work”)
}

这种多尾部闭包的语法在SwiftUI中很常见,例如用于同时提供视图的主体、页眉和页脚。

为什么要使用闭包作为参数?

使用闭包作为参数的核心优势在于异步执行非阻塞。它允许你将“将来要做什么”这个任务打包,交给另一个系统或函数去安排执行,而当前代码可以继续运行,不被阻塞。

  • 系统集成(如Siri):当Siri需要你的应用处理一个请求时,它会在后台启动你的应用并传入一个闭包。你的应用可以在完成所有耗时工作(如网络请求、数据处理)后,调用这个闭包来回传结果。这样Siri的界面在整个过程中都能保持响应,不会因为等待你的应用而冻结。
  • 网络请求:从网络获取数据是典型的耗时操作。通过使用闭包回调,你可以发起请求后立即返回,让UI保持交互性。当网络数据返回时,再执行闭包中的代码来处理数据、更新界面。

闭包使得这种“发起请求 -> 立即返回 -> 未来处理结果”的模式变得非常优雅和易于管理。

总结:闭包

让我们回顾一下关于闭包的核心要点:

  • 函数副本:你可以复制函数,但复制体会丢失原始函数的外部参数名。
  • 函数类型:所有函数都有类型,格式为(参数类型) -> 返回类型Void表示无返回值。
  • 创建闭包:可以直接将闭包表达式赋值给常量或变量。
  • 闭包语法:带参数和返回值的闭包,其声明必须放在大括号内,并用in关键字与主体分隔。
  • 函数参数:函数可以接受其他函数作为参数。调用时,你可以传递一个命名函数,也可以直接内联一个闭包。
  • 类型推断:在闭包上下文中,Swift通常可以推断参数和返回值的类型,允许你省略它们的显式声明。
  • 尾部闭包:当函数的最后一个参数是闭包时,可以使用尾部闭包语法,让代码更清晰。
  • 速记参数:可以使用$0$1等速记参数名来指代闭包的参数,但应酌情使用以保证代码可读性。
  • 编写高阶函数:你可以创建自己的、接受函数作为参数的函数。

闭包无疑是Swift学习曲线中最陡峭的部分之一。它的语法和概念需要时间去适应。如果你在学完本章后感到有些头晕,那是完全正常的——这意味着你已经踏上了掌握Swift核心特性的正确道路。

0 Answers