工作生活

Fu*king Closures

2019-07-03  本文已影响0人  Xtuphe

闭包能在其被定义的上下文中捕获和存储任何常量和变量的引用,这叫关闭这些常量和变量。

闭包有些特殊优化:

排序方法

Swift的标准库中提供一个sorted(by:),可以用来根据你提供的闭包的输出来对数组中元素进行排序。排序完成后该方法会返回一个新的排序后的数组,原数组没有被修改。

sorted(by:)方法接收一个闭包,这个闭包要传2个与数组元素类型相同的参数,并会返回一个Bool,为true时,第一个值会在第二个值的前面,为false时则反之。

例如下面,可以把普通的函数作为参数传过去

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"]

闭包表达式语法

通常如下:

{ (parameters) -> return type in
    statements
}

所以上面的排序可以简写为:

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

要注意的是这个闭包声明的参数与返回类型与 backward(::) 几乎一模一样,但,这是个包内表达式,参数与返回值是写在{}的里面。
闭包的包体被in关键字区分开来,in关键字代表了此闭包的定义(即参数与返回类型)结束了,包体要开始了。

从上下文中推断类型

因为排序闭包是作为一个参数传递给一个方法的,Swift可以推断出其参数的类型与返回类型。sorted(by:) 被一个含String元素的数组调用,所以其参数类型一定为(String, String) -> Bool,这意味着这些可以省去不写了。

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

单行闭包隐性返回

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

由于sorted(by:)方法的参数明确了需要一个Bool来作为闭包的返回值,并且闭包的包体包含的唯一一行表达式(s1 > s2)返回Bool,没有歧义,所以return可被省略。

速写参数名

Swift自动提供速写参数名(0,1, $2......)来指代闭包的参数,如果你在闭包中使用速写参数名,那么你可以省略定义参数,而参数的数量和类型会从函数类型中被推断出,此时in关键字也可以被省略。

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

运算符方法

Swift的String类型定义了针对String类型的运算符>作为一个方法,此方法有两个String类型的参数,并返回Bool。刚好符合sorted(by:)需要的类型。因此,可以继续简写为:

reversedNames = names.sorted(by: > )

尾部闭包

如果你需要把闭包作为一个函数的最后一个参数传递,且此闭包表达式很长,可以将其写为尾部闭包。一个尾部闭包,虽然是一个参数,可以写在方法()的后面。当你使用尾部闭包语法时,你不需要写参数标签。

func someFunctionThatTakesAClosure(closure: () -> Void) {
    // function body goes here
}

// Here's how you call this function without using a trailing closure:
someFunctionThatTakesAClosure(closure: {
    // closure's body goes here
})

// Here's how you call this function with a trailing closure instead:
someFunctionThatTakesAClosure() {
    // trailing closure's body goes here
}

所以前面写的sorted(by:)也可以写为:

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

如果一个闭包是这个方法的唯一参数,那不需要写()

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

map(_:)

Swift的Array类型有map(:) 方法,接收一个闭包最为其唯一参数。数组中的每个元素都会使map(:) 会被调用一次,并返回映射过的值(可以是其他类型)。映射的特性与返回值的类型则由闭包来实现。
在闭包对每个元素执行过后,map(_:) 方法返回一个新的array,包含所有映射过的值,且对应的顺序与原数组相同。

下面是如何把[16, 58, 510] 转换为新的数组["OneSix", "FiveEight", "FiveOneZero"]:

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"]

用numbers数组通过传map(:)作为尾部闭包来创建strings数组。map(:) 方法会为数组中的每个元素调用闭包。你不需指定闭包输入的参数类型,因为类型可以从数组的值中被推断出。

在这个例子中,变量number用闭包的number参数来初始化,所以number可以被修改(函数和闭包的参数永远是常量)。闭包表达式也声明了一个返回类型String,来表明最终被存储到映射后数组的值类型。

值捕获

一个闭包可以从其定义的上下文中捕获常量或变量。然后闭包可在包体内引用或修改这些常量或变量,即使在原领域内这些常量或变量已不存在了。

下面是一个叫makeIncrementer的函数,其包含一个叫incrementer的嵌套函数。incrementer()函数捕获2个值,runningTotal和amount,捕获这些值后,incrementer作为makeIncrementer的闭包返回,每次被调用时,runningTotal会增加amount。

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

makeIncrementer的返回类型是() -> Int。这意味着它返回一个函数,而不是简单的值。它还定义了一个变量runningTotal,来储存当前递增的总次数。

如果将上面嵌套的incrementer()函数抽出来看,似乎有点点奇怪

func incrementer() -> Int {
    runningTotal += amount
    return runningTotal
}

这个函数没有任何参数,然而却引用了runningTotal和amount。这是通过捕获他俩的引用来实现的。捕获引用确保了再makeIncrementer结束时,runningTotal和amount不会消失,并且在下一次incrementer被调用时runningTotal可用。

let incrementByTen = makeIncrementer(forIncrement: 10)

incrementByTen()
// returns a value of 10
incrementByTen()
// returns a value of 20
incrementByTen()
// returns a value of 30

如果你创建另外一个incrementer,他会有其自己新的,分开的runningTotal:

let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()
// returns a value of 7
incrementByTen()
// returns a value of 40
Note:
如果把闭包赋值给一个类实例的属性,那么这个闭包会捕获此实例或其成员,造成循环引用。Swift用capture lists来打破循环引用。

闭包是引用类型

在上面的例子中,incrementBySeven和incrementByTen为常量,但是这些常量引用的闭包仍然能够递增其捕获的runningTotal变量。这是因为函数与闭包属于引用类型。

每当你赋值一个函数或闭包给一个常量或变量,你是将常量或变量设置为函数或闭包的一个引用。上面例子中,incrementByTen常量引用的是要选择哪个闭包,而不是闭包的内容。
这也意味着当你把一个闭包赋值给两个不同的常量或变量,他们均将引用这个相同的闭包。

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

incrementByTen()
// returns a value of 60

闭包逃逸

当闭包被用作参数传递,并且在函数调用结束后被调用时, 被称作是逃逸。当你声明一个参数含有闭包的函数时,你可以在参数类型前写@escaping来表示此闭包允许逃逸。

闭包逃逸的一种方式是将其存储在函数外的一个变量中。函数在其开始运行后返回,但闭包会直到运行结束后被调用。

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

someFunctionWithEscapingClosure(_:) 函数接收一个闭包作为参数并将其添加到一个在函数外声明的数组。如果你没有用@escape标记参数,会得到一个编译期错误。

用@escape标记闭包意味着你需要在比包内显性引用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)
// Prints "200"

completionHandlers.first?()
print(instance.x)
// Prints "100"
上一篇下一篇

猜你喜欢

热点阅读