MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Swift函数与闭包高级用法

2022-09-036.1k 阅读

一、Swift 函数的高级特性

1.1 函数类型作为参数

在 Swift 中,函数也有类型。函数类型由参数类型和返回类型组成。例如,一个接受两个 Int 类型参数并返回一个 Int 类型结果的函数,其类型为 (Int, Int) -> Int

这种特性使得我们可以将函数作为参数传递给其他函数。下面是一个简单的示例:

// 定义一个简单的加法函数
func add(_ a: Int, _ b: Int) -> Int {
    return a + b
}

// 定义一个函数,它接受一个函数类型的参数,并调用这个函数
func operate(_ a: Int, _ b: Int, operation: (Int, Int) -> Int) -> Int {
    return operation(a, b)
}

// 使用 operate 函数,传递 add 函数作为参数
let result = operate(3, 5, operation: add)
print(result) // 输出 8

在上述代码中,operate 函数接受两个 Int 类型的参数 ab,以及一个函数类型的参数 operationoperation 函数接受两个 Int 类型参数并返回一个 Int 类型结果。通过将 add 函数传递给 operate 函数,operate 函数可以调用 add 函数并返回结果。

1.2 函数类型作为返回值

不仅可以将函数作为参数传递,还可以将函数作为返回值。这在实现一些复杂的逻辑控制或延迟执行的场景中非常有用。

// 定义一个根据条件返回不同函数的函数
func chooseOperation(isAddition: Bool) -> (Int, Int) -> Int {
    if isAddition {
        return { a, b in a + b }
    } else {
        return { a, b in a - b }
    }
}

// 获取加法函数
let additionFunction = chooseOperation(isAddition: true)
let addResult = additionFunction(4, 2)
print(addResult) // 输出 6

// 获取减法函数
let subtractionFunction = chooseOperation(isAddition: false)
let subtractResult = subtractionFunction(4, 2)
print(subtractResult) // 输出 2

在这个例子中,chooseOperation 函数根据 isAddition 参数的值返回不同的函数。如果 isAdditiontrue,则返回一个加法闭包;否则返回一个减法闭包。通过这种方式,可以根据运行时的条件动态地选择要执行的操作。

1.3 嵌套函数

Swift 允许在函数内部定义其他函数,这些内部函数称为嵌套函数。嵌套函数在外部函数的作用域内,对外界是隐藏的,这有助于封装一些只在特定函数内部使用的逻辑。

func outerFunction() {
    func innerFunction() {
        print("这是内部函数")
    }
    innerFunction()
}

outerFunction() // 输出 "这是内部函数"

在上述代码中,innerFunction 定义在 outerFunction 内部,只能在 outerFunction 内部调用。这种封装可以提高代码的模块化和安全性,避免命名冲突。

1.4 可变参数

可变参数允许函数接受零个或多个指定类型的参数。在函数定义中,通过在参数类型后面加上 ... 来表示可变参数。

func sumOfNumbers(_ numbers: Int...) -> Int {
    var sum = 0
    for number in numbers {
        sum += number
    }
    return sum
}

let total = sumOfNumbers(1, 2, 3, 4, 5)
print(total) // 输出 15

sumOfNumbers 函数中,numbers 是一个可变参数,它可以接受任意数量的 Int 类型参数。在函数内部,numbers 被视为一个数组,可以通过遍历数组来计算总和。

1.5 输入输出参数

默认情况下,函数参数是常量。如果需要在函数内部修改参数的值并使其在函数调用后仍然有效,可以使用输入输出参数。在参数定义前加上 inout 关键字来表示输入输出参数。

func multiplyByTwo(_ number: inout Int) {
    number *= 2
}

var myNumber = 5
multiplyByTwo(&myNumber)
print(myNumber) // 输出 10

multiplyByTwo 函数中,number 是一个输入输出参数。在调用函数时,需要在参数前加上 & 符号,表示传递的是变量的引用而不是值的副本。这样,函数内部对 number 的修改会反映到外部的变量 myNumber 上。

二、Swift 闭包的高级用法

2.1 闭包表达式

闭包表达式是一种简洁的创建闭包的方式。它的语法形式如下:

{ (parameters) -> returnType in statements }

下面是一个使用闭包表达式作为排序函数的示例:

var numbers = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]
numbers.sort { (a, b) -> Bool in
    return a < b
}
print(numbers) // 输出 [1, 1, 2, 3, 3, 4, 5, 5, 5, 6, 9]

sort 函数的调用中,传递了一个闭包表达式作为参数。这个闭包表达式接受两个 Int 类型的参数 ab,并返回一个 Bool 类型的值,用于指示 a 是否应该排在 b 之前。

2.2 尾随闭包

当闭包表达式作为函数的最后一个参数时,可以使用尾随闭包的语法,将闭包写在函数调用的括号外面,使代码更加清晰易读。

let sortedNumbers = numbers.sorted { $0 < $1 }
print(sortedNumbers) // 输出 [1, 1, 2, 3, 3, 4, 5, 5, 5, 6, 9]

在这个例子中,sorted 函数的闭包参数写在了括号外面,使用了更简洁的尾随闭包语法。

2.3 捕获值

闭包可以捕获其定义时所在作用域中的常量和变量。即使这些常量和变量在闭包定义后离开了作用域,闭包仍然可以访问和修改它们的值。

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

let incrementByTen = makeIncrementer(forIncrement: 10)
print(incrementByTen()) // 输出 10
print(incrementByTen()) // 输出 20

makeIncrementer 函数中,incrementer 闭包捕获了 runningTotalamount。每次调用 incrementByTen 时,runningTotal 的值都会被更新并保持,尽管 runningTotal 定义在 makeIncrementer 函数内部,但由于闭包的捕获机制,它的状态得以延续。

2.4 自动闭包

自动闭包是一种自动创建的闭包,它不接受任何参数,返回一个特定类型的值。通常用于延迟执行代码,直到真正需要结果时才执行。

func logIfTrue(_ condition: @autoclosure () -> Bool) {
    if condition() {
        print("条件为真")
    }
}

let expensiveComputation: () -> Bool = {
    print("执行昂贵的计算")
    return true
}

logIfTrue(expensiveComputation())
// 输出 "执行昂贵的计算" "条件为真"

logIfTrue(expensiveComputation)
// 只有在条件判断时才输出 "执行昂贵的计算" "条件为真"

logIfTrue 函数中,condition 参数是一个自动闭包。在第一种调用方式中,expensiveComputation 被立即执行并传递结果。而在第二种调用方式中,由于 condition 是自动闭包,expensiveComputation 的执行被延迟到 if 条件判断时。

2.5 逃逸闭包

逃逸闭包是指闭包在函数返回后才被调用。如果一个闭包作为参数传递给函数,并且在函数返回后被调用,那么这个闭包就是逃逸闭包。在函数参数定义中,使用 @escaping 关键字来标记逃逸闭包。

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

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

let someString = "Hello, world!"
someFunctionWithEscapingClosure {
    print(someString)
}
someFunctionWithNonescapingClosure {
    print(someString)
}

completionHandlers.first?() // 输出 "Hello, world!"

someFunctionWithEscapingClosure 函数中,completionHandler 是一个逃逸闭包,它被添加到 completionHandlers 数组中,在函数返回后仍然可以被调用。而在 someFunctionWithNonescapingClosure 函数中,closure 是一个非逃逸闭包,它在函数内部被立即调用。

2.6 闭包的内存管理

闭包可能会导致循环引用,从而引发内存泄漏。例如,当一个类的实例持有一个闭包,而这个闭包又捕获了该类的实例时,就会形成循环引用。

class SomeClass {
    let someValue = 42
    var closure: (() -> Void)?
    deinit {
        print("SomeClass 实例被销毁")
    }
}

var instance: SomeClass? = SomeClass()
instance?.closure = {
    print(instance?.someValue ?? 0)
}
instance = nil
// 这里由于循环引用,SomeClass 实例不会被销毁,不会输出 "SomeClass 实例被销毁"

为了打破这种循环引用,可以使用 weakunowned 关键字来捕获实例。

class SomeClass {
    let someValue = 42
    var closure: (() -> Void)?
    deinit {
        print("SomeClass 实例被销毁")
    }
}

var instance: SomeClass? = SomeClass()
instance?.closure = { [weak instance] in
    guard let strongInstance = instance else { return }
    print(strongInstance.someValue)
}
instance = nil
// 这里使用 weak 捕获实例,打破了循环引用,SomeClass 实例会被销毁,输出 "SomeClass 实例被销毁"

在上述代码中,使用 [weak instance] 来捕获 instance,使得闭包对 instance 持有弱引用,从而打破了循环引用。当 instance 被设置为 nil 时,SomeClass 实例会被正确销毁。

三、函数与闭包的结合使用

3.1 用闭包实现回调

在许多异步操作中,闭包常被用作回调函数。例如,在使用 DispatchQueue 进行异步任务时,可以传递一个闭包作为任务完成后的回调。

DispatchQueue.global().async {
    // 执行一些耗时操作
    let result = 2 + 3
    DispatchQueue.main.async {
        // 回到主线程更新 UI
        print("结果是 \(result)")
    }
}

在这个例子中,首先在全局队列中异步执行一个任务(计算 2 + 3),然后在主线程队列中异步执行一个闭包,用于更新 UI 或处理结果。这里的闭包作为回调函数,确保在异步任务完成后执行特定的操作。

3.2 函数柯里化

函数柯里化是将一个多参数函数转换为一系列单参数函数的过程。在 Swift 中,可以通过闭包来实现函数柯里化。

func add(a: Int) -> (Int) -> Int {
    return { b in
        return a + b
    }
}

let addFive = add(a: 5)
let result = addFive(3)
print(result) // 输出 8

在上述代码中,add 函数接受一个 Int 类型的参数 a,并返回一个闭包。这个闭包又接受一个 Int 类型的参数 b,并返回 a + b 的结果。通过调用 add(a: 5) 得到一个新的函数 addFive,它固定了第一个参数为 5,可以直接传入第二个参数进行计算。

3.3 高阶函数与闭包组合

高阶函数是指接受其他函数作为参数或返回函数的函数。结合闭包,可以实现复杂的功能组合。

func compose<Input, Intermediate, Output>(_ f: @escaping (Intermediate) -> Output, _ g: @escaping (Input) -> Intermediate) -> (Input) -> Output {
    return { input in
        return f(g(input))
    }
}

func square(_ number: Int) -> Int {
    return number * number
}

func addOne(_ number: Int) -> Int {
    return number + 1
}

let addOneAndSquare = compose(square, addOne)
let resultValue = addOneAndSquare(3)
print(resultValue) // 输出 16,先执行 addOne(3) 得到 4,再执行 square(4) 得到 16

在这个例子中,compose 是一个高阶函数,它接受两个闭包 fg,并返回一个新的闭包。这个新闭包先调用 g 函数处理输入值,再将结果传递给 f 函数进行处理。通过这种方式,可以组合不同的函数逻辑,实现更灵活和强大的功能。

3.4 闭包作为属性

类或结构体可以将闭包作为属性来存储。这在需要根据不同的情况动态改变行为时非常有用。

class MyClass {
    var action: (() -> Void)?
    func performAction() {
        action?()
    }
}

let myObject = MyClass()
myObject.action = {
    print("执行自定义操作")
}
myObject.performAction() // 输出 "执行自定义操作"

MyClass 类中,action 是一个闭包类型的属性。可以在运行时为 action 属性赋值不同的闭包,然后通过调用 performAction 方法来执行相应的操作。这种方式使得对象的行为可以在运行时动态改变。

3.5 闭包与协议

在 Swift 中,协议可以定义闭包类型的属性或方法。这为不同类型之间的行为一致性提供了一种方式。

protocol Actionable {
    var performAction: () -> Void { get }
}

class FirstClass: Actionable {
    var performAction: () -> Void = {
        print("FirstClass 的操作")
    }
}

class SecondClass: Actionable {
    var performAction: () -> Void = {
        print("SecondClass 的操作")
    }
}

let firstInstance = FirstClass()
let secondInstance = SecondClass()

let instances: [Actionable] = [firstInstance, secondInstance]
for instance in instances {
    instance.performAction()
}
// 输出 "FirstClass 的操作" "SecondClass 的操作"

在这个例子中,Actionable 协议定义了一个 performAction 闭包类型的属性。FirstClassSecondClass 类都遵循这个协议,并为 performAction 属性提供了不同的实现。通过将不同类型的实例放入一个数组中,并遍历调用 performAction,可以实现统一的行为调用,尽管这些实例的具体类型不同。

通过深入理解和掌握 Swift 函数与闭包的这些高级用法,开发者可以编写出更加灵活、高效和可维护的代码。无论是在构建复杂的应用逻辑,还是处理异步任务和实现设计模式,这些特性都能发挥重要的作用。在实际开发中,需要根据具体的需求和场景,合理地运用函数和闭包的各种高级特性,以提升代码的质量和性能。