0%

Advanced Swift系列(四): Functions

总览

        在打开本章之前,让我们回顾一下有关功能的要点。如果你已经熟悉一流的功能,请随时跳到下一部分。但是,如果你甚至不确定它们,请通读以下内容。

        要了解 Swift中的函数闭包 ,你实际上需要理解三件事,大致按此重要性顺序排列:

  • 1.可以将函数分配给变量,并传入和传出其他函数作为参数,就像 IntString 一样。
  • 2.函数可以捕获存在于其本地范围之外的变量。
  • 3.有两种创建功能的功能–使用 关键字{}Swift调用后一个闭包表达式。

        有时,刚接触闭包主题的人会以相反的顺序来讲,可能会错过这些要点之一,或者他们将 闭包闭包表达 这两个术语混为一谈,这可能会引起很多混乱。这是三足凳,如果你错过了以上三点之一,当你尝试坐下时会跌倒。

可以将函数分配给变量,并可以将其他函数作为参数传入和传出

        与许多现代语言一样,在 Swift 中,功能被称为 “一流的对象” 。 你可以将函数分配给变量,也可以将它们传入和传出其他函数,以供以后调用。

        这是最重要的了解。 对于函数式编程,“获取”类似于在C语言中的“获取”指针。如果你不太了解这一部分,那么其他所有内容都将成为噪音。

        让我们从一个简单地输出整数的函数开始:

1
2
3
func printInt(i: Int) { 
print("You passed \(i).")
}

        要将函数分配给变量 funVar ,我们仅使用函数名称作为值。 请注意,函数名称后没有括号:

1
let funVar = printInt

        现在,我们可以使用 funVar变量 调用 printInt函数 。 注意使用变量名称后的括号:

1
funVar(2) // You passed 2.

        同样值得注意的是,我们不能在 funVar 调用中包含参数标签,而 printInt 调用则需要参数标签,就像 printInt(i:2) 一样。 Swift 仅允许在函数声明中使用参数标签; 标签未包含在函数的类型中。 这意味着你目前无法将参数标签分配给函数类型的变量,尽管在以后的 Swift版本 中这可能会发生变化。

        我们还可以编写一个以函数作为参数的函数:

1
2
3
4
5
func useFunction(function: (Int) -> () ) { 
function(3)
}
useFunction(function: printInt) // You passed 3.
useFunction(function: funVar) // You passed 3.

        为什么能够处理这么大的功能呢? 因为它使你能够轻松编写 “高阶”函数 ,这些函数将函数作为参数并以有用的方式应用它们,如我们在“内置集合” 一章中所见。

        函数还可以返回其他函数:

1
2
3
4
5
6
7
func returnFunc() -> (Int) -> String { 
func innerFunc(i: Int) -> String {
return "You passed \(i)."
}
return innerFunc
}
let myFunc = returnFunc() myFunc(3) // You passed 3.

函数可以捕获存在于其局部范围之外的变量

        当函数引用超出其范围的变量时,这些变量将被捕获并留在它们周围,否则它们将超出范围并被销毁。

        为了看到这一点,让我们重新访问 returnFunc函数 ,但是添加一个每次调用时都会增加的计数器:

1
2
3
4
5
6
7
8
func counterFunc() -> (Int) -> String { 
var counter = 0
func innerFunc(i: Int) -> String {
counter += i // counter is captured
return "Running total: \(counter)"
}
return innerFunc
}

        通常,counter(作为 counterFunc 的局部变量)会在 return语句 之后超出范围,并被销毁。 相反,由于它是由 innerFunc 捕获的,因此Swift运行时将使其保持活动状态,直到捕获它的函数被销毁为止。 我们可以多次调用内部函数,并且看到运行总数增加了:

1
2
3
let f = counterFunc() 
f(3) // Running total: 3
f(4) // Running total: 7

        如果再次调用 counterFunc() ,将创建并捕获一个新的 counter变量

1
2
3
let g = counterFunc() 
g(2) // Running total: 2
g(2) // Running total: 4

        这不会影响我们的第一个功能,该功能仍然具有自己捕获的counter版本:

1
f(2) // Running total: 9

        将这些函数及其捕获的变量组合起来,就像具有单个方法(函数)和某些成员变量(捕获的变量)的类实例一样。

        在编程术语中,函数和捕获变量的环境的组合称为闭包。 因此,上面的f和g是闭包的示例,因为它们捕获并使用在其外部声明的非局部变量(计数器)。

可以使用{}语法为闭包表达式声明函数

        在 Swift 中,你可以通过两种方式定义函数。 一种是使用 func关键字 。 另一种方法是使用 闭包表达式 。 考虑使用以下简单函数将数字加倍:

1
2
3
4
func doubler(i: Int) -> Int { 
return i*2
}
[1, 2, 3, 4].map(doubler) // [2, 4, 6, 8]

        这是使用闭包表达式语法编写的相同函数。 和以前一样,我们可以将其传递给 map

1
2
let doublerAlt = { (i: Int) -> Int in return i*2 } 
[1, 2, 3, 4].map(doublerAlt) // [2, 4, 6, 8]

        可以将声明为 闭包表达式的函数 视为 函数文字 ,就像 1“hello” 是整数和字符串文字一样。 它们也是匿名的-它们没有命名,与func关键字不同。 可以使用它们的唯一方法是在创建它们时将它们分配给变量(就像我们在doubler中所做的那样),或者将它们传递给另一个函数或方法。

        可以使用匿名函数的第三种方式:你可以直接在内部调用函数,作为定义该函数的同一表达式的一部分。 这对于定义初始化需要多行的属性很有用。 我们将在下面的惰性属性部分中看到一个示例。

        使用 闭包表达式 声明的 doubler 和之前使用 func关键字声明的doubler完全等效 ,除了上面提到的参数标签处理方式不同。 与某些语言不同,它们甚至存在于相同的“命名空间”中。

        为什么 {} 语法有用呢? 为什么不每次都使用func? 好吧,它可以紧凑得多,尤其是在编写快速函数以传递给其他函数(例如map)时。 这是我们的doubler map示例,其格式简短得多:

1
[1, 2, 3].map { $0 * 2 } // [2, 4, 6]

        这看起来非常不同,因为我们利用了 Swift 的多项功能来制作代码更简洁。 在这里,它们一一对应:

  • 1.如果你要传递的闭包参数不正确,则无需先将其存储在本地变量中。可以认为这就像将数字表达式(例如5 * i)传递给以Int作为参数的函数一样。

  • 2.如果编译器可以从上下文中推断出类型,则无需指定它。在我们的示例中,传递给map的函数采用一个Int(根据数组元素的类型推断)并返回一个Int(根据乘法表达式的类型推断)。

  • 3.如果closureexpression的主体包含与leexpression相同的值,它将自动返回该表达式的值,你可以不使用该返回值。

  • 4.Swift自动为该函数的参数提供缩写名称-第一个为 $0 ,第二个为 $1 ,依此类推。

  • 5.如果无法执行该函数的闭包表达式,则可以将表达式移到函数调用括号之外。如果你有多行闭包表达式,那么这种尾随的闭包语法非常有用,因为它更类似于常规函数定义或其他块语句,例如 if expr {}。

  • 6.最后,如果功能没有其他说法,则可以关闭在函数名称后的所有括号之间都不要加上括号。

        使用这些规则,我们可以将下面的表达式简化为上面所示的形式:

1
2
3
4
5
6
[1, 2, 3].map( { (i: Int) -> Int in return i * 2 } ) 
[1, 2, 3].map( { i in return i * 2 } )
[1, 2, 3].map( { i in i * 2 } )
[1, 2, 3].map( { $0 * 2 } )
[1, 2, 3].map() { $0 * 2 }
[1, 2, 3].map{$0*2}

        如果你不熟悉 Swift 的语法以及一般的 函数式编程 ,那么这些紧凑的函数声明乍一看似乎令人生畏。但是随着你对语法和函数式编程风格的逐渐熟悉,它们会开始变得更加自然,并且你将欣赏消除混乱的功能,从而可以更清楚地看到代码在做什么。一旦你习惯于阅读这样编写的代码,与使用常规for循环编写的等效代码相比,一目了然地进行解析。

        有时, Swift 需要帮助来 推断类型 。有时,你可能会出错,并且类型不是你认为应该的。如果你在尝试提供 闭包表达式 时遇到神秘的错误,最好写出完整的表格(上面的第一个版本)并附上类型。在许多情况下,这将有助于弄清问题出在哪里。完成长格式编译后,再次将类型一一取出,直到编译器抱怨为止。如果错误是你的错误,则将在此过程中修复你的代码。

        Swift 也会坚持让你有时更露骨。例如,你不能完全忽略输入参数。假设你想要一个随机数数组。一种快速的方法是使用仅生成随机数的函数映射范围。但是,你必须提供一个参数。在这种情况下,你可以使用 _ 来指示编译器你确认有一个参数,但是你并不关心它是什么:

1
(0..<3).map { _ in Int.random(in: 1..<100) } // [3, 63, 60]

        当你需要显式键入变量时,不必在闭包内进行操作 closure表达。 例如,尝试定义不带任何类型的 isEven

1
let isEven = { $0 % 2 == 0 }

        上面,将 isEven 的类型推断为 (Int)-> Bool ,就像让 i = 1 推断为 Int 一样,因为 Int 是整数文字的默认类型。

        这是因为标准库中的类型别名 IntegerLiteralType

1
2
3
4
5
protocol ExpressibleByIntegerLiteral { associatedtype IntegerLiteralType
/// Create an instance initialized to `value`. init(integerLiteral value: IntegerLiteralType)
}
/// The default type for an otherwise-unconstrained integer literal.
typealias IntegerLiteralType = Int

        如果要定义自己的类型别名,它将覆盖默认的别名,然后更改此行为:

1
2
change this behavior:
typealias IntegerLiteralType = UInt32 let i = 1 // i will be of type UInt32.

        这几乎肯定是一个坏主意。

        但是,如果你需要另一版本的 isEven版本 ,则可以在闭包表达式中键入参数和返回值:

1
let isEvenAlt = { (i: Int8) -> Bool in i % 2 == 0 }

        但是你也可以从闭包外部提供上下文:

1
2
3

let isEvenAlt2: (Int8) -> Bool = { $0 % 2 == 0 }
let isEvenAlt3 = { $0 % 2 == 0 } as (Int8) -> Bool

        由于闭包表达式最常用于现有输入或输出类型的某些上下文中,因此通常不需要添加显式类型,但是知道你可以执行此操作很有用。

        当然,最好将isEven的通用版本定义为适用于任何整数的计算属性:

1
2
3
4
5
extension BinaryInteger {
var isEven: Bool {
return self % 2 == 0
}
}

        另外,我们可以选择将所有Integer类型的isEven变量定义为自由函数:

1
2
func isEven<T: BinaryInteger>(_ i: T) -> Bool { returni%2==0
}

        如果你想将该自由函数分配给变量,那么这也就意味着你必须锁定它正在操作的特定类型。 变量不能包含通用函数-只能是特定的一个:

1
let int8IsEven: (Int8) -> Bool = isEven

        关于命名的最后一点。 请务必记住,用 func 声明的函数可以是闭包,就像用 {} 声明的函数一样。 请记住,闭包是与任何捕获的变量组合的函数。 虽然用**{}创建的函数称为闭包表达式,但人们通常将此语法称为闭包**。 但是请不要感到困惑,并认为使用闭包表达式语法声明的函数与其他函数不同-没什么不同。 它们既是函数,也可以是闭包。

Flexibility through Functions

        在“内置集合”一章中,我们讨论了通过将函数作为参数传递来对行为进行参数化。 让我们来看另一个例子:排序。

        在Swift中对集合进行排序很简单:

1
2
let myArray = [3, 1, 2] 
myArray.sorted() // [1, 2, 3]

        共有四种排序方法:非变异变体sorted(by :)变异sor(by :) ,对于默认不以升序对可比较事物进行排序且不带参数的版本,将其乘以2。 对于最常见的情况,你只需要 sorted() 。 而且,如果你想以其他顺序排序,只需提供一个函数即可:

1
myArray.sorted(by: >) // [3, 2, 1]

        如果你的元素不符合 Comparable , 但你还可以提供函数像元组一样有一个 <运算符

1
2
3
var numberStrings = [(2, "two"), (1, "one"), (3, "three")] 
numberStrings.sort(by: <)
numberStrings // [(1, "one"), (2, "two"), (3, "three")]

        或者,如果你想按一些任意条件排序,则可以提供更复杂的功能:

1
2
3
4
5
6
7

let animals = ["elephant", "zebra", "dog"] animals.sorted { lhs, rhs in
let l = lhs.reversed()
let r = rhs.reversed()
return l.lexicographicallyPrecedes(r)
}
// ["zebra", "dog", "elephant"]

        这是最后一项功能-使用任何比较功能对集合进行排序的能力-使Swift排序如此强大。

        将此与Objective-C中的排序方式进行比较。如果要使用Foundation对数组进行排序,则会遇到一长串不同的选项:有些排序方法将选择器,块或函数指针作为比较谓词,或者可以传入NSSortDescriptors数组定义排序标准。所有这些功能都提供了很大的灵活性和功能,但是却以复杂性为代价-无法选择 “仅根据默认顺序进行常规排序”Foundation中的某些变体(例如,将块作为比较谓词的方法)与Swiftsorted(by :)方法基本相同;其他版本(例如带有排序描述符数组的版本)则充分利用了Objective-C的动态特性,从而获得了一种非常灵活和强大的(如果是弱类型的)API,该API无法直接移植到Swift

        Swift中仍然支持选择器动态调度,但是Swift标准库倾向于使用基于函数的方法。在本节中,我们将演示如何使用函数作为参数以及将函数作为数据如何以完全类型安全的方式复制相同的功能。让我们看一个受苹果文档中“排序描述符编程主题”指南启发的复杂示例。

        我们首先定义一个Person类型。因为我们想展示Objective-C强大的运行时系统是如何工作的,所以我们必须使该对象成为NSObject子类(在纯Swift中,结构可能是更好的选择)。我们还使用@objcMembers对该类进行注释,以使所有成员对Objective-C可见:

1
2
3
4
5
6
7
8
9
10
11
12
13
@objcMembers
final class Person: NSObject {
let first: String
let last: String
let yearOfBirth: Int

init(first: String, last: String, yearOfBirth: Int) {
self.first = first
self.last = last
self.yearOfBirth = yearOfBirth
// super.init() implicitly called here
}
}

        我们还定义了一系列具有不同姓名和出生年月的人:

1
2
3
4
5
6
7
8
let people = [
Person(first: "Emily", last: "Young", yearOfBirth: 2002),
Person(first: "David", last: "Gray", yearOfBirth: 1991),
Person(first: "Robert", last: "Barnes", yearOfBirth: 1985),
Person(first: "Ava", last: "Barnes", yearOfBirth: 2000),
Person(first: "Joanne", last: "Miller", yearOfBirth: 1994),
Person(first: "Ava", last: "Barnes", yearOfBirth: 1998),
]

        我们要首先按姓氏,然后按名字,最后按出生年份对这个数组进行排序。 订购应遵守用户的语言环境设置。 NSSortDescriptor对象描述了如何对对象进行排序,我们可以使用它们来表示各个排序标准(使用localizedStandardCompare作为符合区域设置的比较器方法):

1
2
3
4
5
6
7
let lastDescriptor = NSSortDescriptor(key: #keyPath(Person.last), ascending: true,
selector: #selector(NSString.localizedStandardCompare(_:)))

let firstDescriptor = NSSortDescriptor(key: #keyPath(Person.first), ascending: true,
selector: #selector(NSString.localizedStandardCompare(_:)))

let yearDescriptor = NSSortDescriptor(key: #keyPath(Person.yearOfBirth), ascending: true)

        要对数组进行排序,我们可以在NSArray上使用sortedArray(using :)方法。 这需要一个排序描述符列表。 为了确定两个元素的顺序,该方法首先使用第一个排序描述符并使用该结果。 但是,如果根据第一个描述符,两个元素相等,则使用第二个描述符,依此类推:

1
2
3
4
5
let descriptors = [lastDescriptor, firstDescriptor, yearDescriptor]
(people as NSArray).sortedArray(using: descriptors)
/*
[Ava Barnes (1998), Ava Barnes (2000), Robert Barnes (1985),
David Gray (1991), Joanne Miller (1994), Emily Young (2002)] */

        排序描述符使用了Objective-C的两个运行时功能:首先,键是一个Objective-C的键路径,它实际上只是一个包含属性名称链表的字符串。不要将这些与Swift 4中引入的Swift的本机(强类型)键路径混淆。我们将在下文中对后者进行更多说明。

        Objective-C运行时的第二个功能是键值编码,它可以在运行时查找键的值。选择器参数带有一个选择器(它实际上也只是描述方法名称的字符串)。在运行时,选择器用于查找比较功能,当比较两个对象时,使用该比较功能比较键的值。

        这是运行时编程的一种很酷的用法,尤其是当你意识到可以例如在用户单击列标题的情况下在运行时构建排序描述符数组时。

        我们如何使用Swift的排序功能来复制此功能?复制排序的各个部分很简单-例如,如果你想使用localizedStandardCompare对数组进行排序:

1
2
var strings = ["Hello", "hallo", "Hallo", "hello"]
strings.sort { $0.localizedStandardCompare($1) == .orderedAscending } strings // ["hallo", "Hallo", "hello", "Hello"]

        如果你只想使用一个对象的单个属性进行排序,那也很简单:

1
2
3
4
people.sorted { $0.yearOfBirth < $1.yearOfBirth }
/*
[Robert Barnes (1985), David Gray (1991), Joanne Miller (1994),
Ava Barnes (1998), Ava Barnes (2000), Emily Young (2002)] */

        但是,当将可选属性与localizedStandardCompare之类的方法结合使用时,这种方法效果不佳–丑陋得很快。 例如,考虑按文件扩展名对文件名数组进行排序(使用“可选”一章中的fileExtension属性):

1
2
3
4
5
var files = ["one", "file.h", "file.c", "test.h"] 
files.sort { l, r in r.fileExtension.flatMap {
l.fileExtension?.localizedStandardCompare($0) } == .orderedAscending
}
files // ["one", "file.c", "file.h", "test.h"]

        这很丑。 稍后,我们将使排序时使用可选选项更加容易。 但是,到目前为止,我们甚至都没有尝试过按多个属性进行排序。 要按姓氏,然后按名字排序,我们可以使用标准库的lexicographicallyPrecedes方法。 这需要两个序列,并通过遍历每一对元素直到找到一个不相等的元素来执行电话簿样式的比较。 因此,我们可以构建元素的两个数组并使用lexicographicallyPrecedes进行比较。 此方法还需要一个函数来执行比较,因此我们将对localizedStandardCompare的使用放在函数中:

1
2
3
4
5
6
7
8
9
10
11

people.sorted { p0, p1 in
let left = [p0.last, p0.first]
let right = [p1.last, p1.first]
return left.lexicographicallyPrecedes(right) {
$0.localizedStandardCompare($1) == .orderedAscending
}
}
/*
[Ava Barnes (2000), Ava Barnes (1998), Robert Barnes (1985),
David Gray (1991), Joanne Miller (1994), Emily Young (2002)] */

        至此,我们几乎以几乎相同的行数复制了原始排序的功能。 但是仍然有很多改进的余地:每次比较的数组构建效率都很低,比较是硬编码的,使用这种方法我们无法真正按照yearOfBirth进行排序。

Functions as Data

        让我们退后一步,而不是编写一个我们可以用来排序的更复杂的函数。 上面的排序描述符更加清晰,但是它们使用运行时编程。 我们编写的函数未使用运行时编程,但编写(和读取)起来并不那么容易。

        排序描述符是描述对象顺序的一种方式。 代替将信息存储为类,我们可以定义一个函数来描述对象的顺序。 最简单的定义是采用两个对象,如果顺序正确,则返回 true 。 这也正是标准库的sort(by :)sorted(by :)方法作为参数的类型。 让我们定义一个通用类型别名来描述排序描述符

1
2
3
/// A sorting predicate that returns `true` if the first
/// value should be ordered before the second.
typealias SortDescriptor<Root> = (Root, Root) -> Bool

        例如,我们可以定义一个排序描述符,该描述符按出生年份比较两个Person对象,或者一个按姓氏排序的排序描述符:

1
2
3
4
5
6
7
let sortByYear: SortDescriptor<Person> = { 
$0.yearOfBirth < $1.yearOfBirth
}

let sortByLastName: SortDescriptor<Person> = {
$0.last.localizedStandardCompare($1.last) == .orderedAscending
}

        无需手动编写排序描述符,我们可以编写一个生成它们的函数。 我们不得不两次写相同的属性是不好的:在sortByLastName中,我们很容易犯了一个错误,并且意外地将 $0.last$1.first 进行了比较。 另外,编写这些排序描述符也很麻烦; 要按名字排序,最简单的方法是复制并粘贴sortByLastName定义并进行修改。

        除了可以复制和粘贴外,我们还可以使用带有类似于NSSortDescriptor的接口定义函数,而无需进行运行时编程。 此函数将函数键作为第一个参数:给定要排序的数组元素,它返回排序描述符正在处理的属性的值。 然后使用areInIncreasingOrder函数比较两个值。 最后,即使类型别名稍微掩盖了这个事实,返回类型也是一个函数:

1
2
3
4
5
6
7
8
/// Builds a `SortDescriptor` function from a sorting predicate
/// and a `key` function that, given an element to compare, produces /// the value that should be used by the sorting predicate.
func sortDescriptor<Root, Value>(
key: @escaping (Root) -> Value,
by areInIncreasingOrder: @escaping (Value, Value) -> Bool) -> SortDescriptor<Root>
{
return { areInIncreasingOrder(key($0), key($1)) }
}

        关键功能描述了如何深入到Root类型的元素并提取与一个特定排序步骤相关的Value类型的值。 它与Swift 4中引入的Swift本机键路径有很多共通之处,这就是我们借用通用参数命名的原因—根和值—来自KeyPath类型。 在本章的后面,我们将讨论如何使用Swift的键路径重写排序描述符。

        这允许我们以不同的方式定义 sortByYear

1
2
3
4
5
let sortByYearAlt: SortDescriptor<Person> = sortDescriptor(key: { $0.yearOfBirth }, by: <)
people.sorted(by: sortByYearAlt)
/*
[Robert Barnes (1985), David Gray (1991), Joanne Miller (1994),
Ava Barnes (1998), Ava Barnes (2000), Emily Young (2002)] */

        我们甚至可以定义一个适用于所有Comparable类型的重载变量:

1
2
3
4
5
6
func sortDescriptor<Root, Value>(key: @escaping (Root) -> Value) -> SortDescriptor<Root> where Value: Comparable
{
return { key($0) < key($1) }
}
let sortByYearAlt2: SortDescriptor<Person> =
sortDescriptor(key: { $0.yearOfBirth })

        上面的两个sortDescriptor变体都可以使用返回布尔值的函数,因为这是标准库的比较谓词约定。 另一方面,像localizedStandardCompare这样的Foundation API则希望使用三向比较结果值(升序,降序或相等)。 添加对此的支持也很容易:

1
2
3
4
5
6
7
8
9
10
func sortDescriptor<Root, Value>( key: @escaping (Root) -> Value, ascending: Bool = true,
by comparator: @escaping (Value) -> (Value) -> ComparisonResult)
-> SortDescriptor<Root> {
return { lhs, rhs in
let order: ComparisonResult = ascending
? .orderedAscending
: .orderedDescending
return comparator(key(lhs))(key(rhs)) == order
}
}

        这使我们能够以更短更清楚的方式编写sortByFirstName

1
2
3
4
5
6
let sortByFirstName: SortDescriptor<Person> =
sortDescriptor(key: { $0.first }, by: String.localizedStandardCompare)
people.sorted(by: sortByFirstName)
/*
[Ava Barnes (2000), Ava Barnes (1998), David Gray (1991),
Emily Young (2002), Joanne Miller (1994), Robert Barnes (1985)] */

        此SortDescriptorNSSortDescriptor变体一样具有表现力,但类型安全,并且不依赖于运行时编程。

        当前,我们只能使用单个SortDescriptor函数对数组进行排序。 如果你回想起基于NSSortDescriptor的示例,我们使用了NSArray.sortedArray(using :)方法对具有多个比较运算符的数组进行排序。 我们可以轻松地向Array甚至Sequence协议添加类似的方法。 但是,我们必须添加两次:一次用于sort的变异变体,一次用于sort的非变异变体

        我们采用了不同的方法,因此我们不必编写更多扩展:我们编写了一个将多个排序描述符合并为一个排序描述符的函数。 它的工作方式类似于sortedArray(using :)方法:首先,它尝试第一个描述符并使用该比较结果。 但是,如果结果相等,它将使用第二个描述符,依此类推,直到描述符用完为止:

1
2
3
4
5
6
7
8
9
10
func combine<Root>
(sortDescriptors: [SortDescriptor<Root>]) -> SortDescriptor<Root> {
return { lhs, rhs in
for areInIncreasingOrder in sortDescriptors {
if areInIncreasingOrder(lhs, rhs) { return true }
if areInIncreasingOrder(rhs, lhs) { return false }
}
return false
}
}

        现在,我们终于可以复制初始示例:

1
2
3
4
5
6
7
let combined: SortDescriptor<Person> = combine(
sortDescriptors: [sortByLastName, sortByFirstName, sortByYear]
)
people.sorted(by: combined)
/*
[Ava Barnes (1998), Ava Barnes (2000), Robert Barnes (1985),
David Gray (1991), Joanne Miller (1994), Emily Young (2002)] */

        我们最终获得了与Foundation版本相同的行为和功能,但是我们的解决方案更安全,并且在Swift中更加惯用。由于Swift版本不依赖运行时编程,因此编译器还可以更好地对其进行优化。另外,我们可以将其与结构或非Objective-C对象一起使用。

        基于函数的方法的一个缺点是函数是不透明的。我们可以使用NSSortDescriptor并将其打印到控制台,然后获得有关排序描述符的一些信息:键路径,选择器名称和排序顺序。我们基于功能的方法无法做到这一点。如果需要这些信息很重要,我们可以将函数包装在结构或类中,并存储其他调试信息。

        这种将函数用作数据的方法(将它们存储在数组中并在运行时构建这些数组)开辟了新的动态行为水平,这是一种像Swift这样的面向静态类型的面向编译时的语言仍然可以复制其中一些内容的方法。诸如Objective-CRuby之类的语言的动态行为。

        我们还看到了编写结合其他功能的功能的有用性,这是功能编程的组成部分之一。例如,我们的Combine(sortDescriptors :)函数采用了一组排序描述符,并将它们组合为一个排序描述符。这是一项非常强大的技术,具有许多不同的应用程序。

        另外,我们甚至可以编写一个自定义运算符来组合两个排序函数:

1
2
3
4
5
6
7
8
9
10
11
infix operator <||> : LogicalDisjunctionPrecedence
func <||><A>(lhs: @escaping (A,A) -> Bool, rhs: @escaping (A,A) -> Bool)
-> (A,A) -> Bool {
return{ x,yin
if lhs(x, y) { return true }
if lhs(y, x) { return false }
// Otherwise they're the same, so we check for the second condition.
if rhs(x, y) { return true }
return false
}
}

        在大多数情况下,编写自定义运算符不是一个好主意。 自定义运算符通常比函数难读,因为运算符没有易于解释的名称。 但是,当少量使用它们时,它们可能会非常强大。 上面的运算符允许我们重写组合的排序示例,如下所示:

1
2
3
4
let combinedAlt = sortByLastName <||> sortByFirstName <||> sortByYear people.sorted(by: combinedAlt)
/*
[Ava Barnes (1998), Ava Barnes (2000), Robert Barnes (1985),
David Gray (1991), Joanne Miller (1994), Emily Young (2002)] */

        这读起来很清楚,也许也比替代方法更简洁地表达了代码的意图,但前提是你(和代码的所有其他阅读者)已经深深理解了操作员的含义。与自定义运算符相比,我们更喜欢Combine(sortDescriptors :)函数。在呼叫站点更清晰,最终使代码更具可读性。除非你编写的是特定于域的高度代码,否则自定义运算符可能会过分杀人。

        Foundation版本与我们的版本相比仍然具有一个功能优势:它可以处理可选内容,而无需编写更多代码。例如,如果我们将Person的最后一个属性设为可选字符串,则无需在使用NSSortDescriptor的排序代码中进行任何更改。

        基于函数的版本需要一些额外的代码。你可能会猜到接下来会发生什么:我们再一次编写一个接受一个函数并返回一个函数的函数。我们可以使用一个常规的比较函数(例如localizedStandardCompare),该函数对两个字符串起作用,然后将其转换为一个对两个字符串进行可选的函数。如果两个值均为零,则它们相等。如果左侧为零,但右侧不是,则它们在上升,反之亦然。最后,如果它们都不为零,我们可以使用compare函数对它们进行比较:

1
2
3
4
5
6
7
8
9
10
11
12
func lift<A>(_ compare: @escaping (A) -> (A) -> ComparisonResult) -> (A?) -> (A?) -> ComparisonResult
{
return { lhsin { rhs in
switch (lhs, rhs) {
case (nil, nil): return .orderedSame
case (nil, _): return .orderedAscending
case (_, nil): return .orderedDescending
case let (l?, r?): return compare(l)(r)
}
}
}
}

        这使我们可以将常规比较函数“提升”到可选对象的域中,并且可以与我们的sortDescriptor函数一起使用。 如果你从前回想起files数组,则通过fileExtension对其进行排序确实很丑陋,因为我们不得不处理可选参数。 但是,有了我们的新举升功能,它又可以干净了:

1
2
3
4
let compare = lift(String.localizedStandardCompare)
let result = files.sorted(by: sortDescriptor(key: { $0.fileExtension },
by: compare))
result // ["one", "file.c", "file.h", "test.h"]

        我们可以为返回布尔的函数编写类似版本的lift。 正如我们在“可选内容”一章中看到的那样,标准库不再提供诸如>的比较运算符作为可选内容。 删除它们是因为如果你不小心使用它们可能会导致令人惊讶的结果。 布尔值的lift变量使你可以轻松使用现有的运算符,并在需要功能时使其成为可选函数。

Functions as Delegates

        Delegates 他们无处不在。 这条消息深深打入了Objective-C(和Java)程序员的脑海:使用协议(接口)进行回调。 你定义一个协议,你的所有者实现该协议,然后将其注册为你的委托,以便获取回调。

        如果委托协议仅包含一个方法,则可以将存储委托对象的属性替换为直接存储回调函数的属性。 但是,要记住一些折衷方案。

Delegates, Cocoa Style

        首先,以与Cocoa定义其无数个委托协议相同的方式创建一个协议。 来自Objective-C的大多数程序员多次编写这样的代码:

1
2
3
protocol AlertViewDelegate: AnyObject { 
func buttonTapped(atIndex: Int)
}

        AlertViewDelegate被定义为仅类协议(通过从AnyObject继承),因为我们希望我们的AlertView类保留对委托的弱引用。 这样,我们就不必担心引用周期。 AlertView永远不会强烈保留其委托,因此,即使委托(直接或间接)对警报视图有很强的引用,一切都很好。 如果委托被取消初始化,则委托属性将自动变为nil:

1
2
3
4
5
6
7
8
9
10
class AlertView {
var buttons: [String]
weak var delegate: AlertViewDelegate?
init(buttons: [String] = ["OK", "Cancel"]) {
self.buttons = buttons
}
func fire() {
delegate?.buttonTapped(atIndex: 1)
}
}

        当我们处理课程时,这种模式非常有效。 例如,假设我们有一个ViewController类,该类初始化警报视图并将其自身设置为委托。 由于该代表被标记为弱者,因此我们无需担心循环引用:

1
2
3
4
5
6
7
8
9
10
class ViewController: AlertViewDelegate { 
let alert: AlertView
init() {
alert = AlertView(buttons: ["OK", "Cancel"])
alert.delegate = self
}
func buttonTapped(atIndex index: Int) {
print("Button tapped: \(index)")
}
}

        通常的做法是始终将代表属性标记为 ***weak***。 这个约定使内存管理的推理变得非常容易,因为实现委托协议的类不必担心创建循环引用

使用结构的Delegate

        有时我们可能想要一个由结构体实现的委托协议。 使用AlertViewDelegate的当前定义,这是不可能的,因为它是仅类的协议。

        我们可以通过不将AlertViewDelegate定义为仅类协议来放松其定义。 另外,我们将buttonTapped(atIndex :)方法标记为变异。 这样,当方法被调用时,结构可以自行变异:

1
2
3
protocol AlertViewDelegate {
mutating func buttonTapped(atIndex: Int)
}

        我们还必须更改AlertView,因为委托属性不能再弱了:

1
2
3
4
5
6
7
8
9
10
class AlertView {
var buttons: [String]
var delegate: AlertViewDelegate?
init(buttons: [String] = ["OK", "Cancel"]) {
self.buttons = buttons
}
func fire() {
delegate?.buttonTapped(atIndex: 1)
}
}

        如果我们为委托属性分配一个对象,则将强引用该对象。 特别是在与Delegate合作时, strong reference 意味着我们很有可能在某个时候引入循环引用。 但是,我们现在可以使用结构。 例如,我们可以创建一个记录所有按钮点击的结构:

1
2
3
4
5
6
struct TapLogger: AlertViewDelegate {
var taps: [Int] = []
mutating func buttonTapped(atIndex index: Int) {
taps.append(index)
}
}

        起初,似乎一切正常。 我们可以创建一个警报视图和一个记录器,然后将两者连接起来。如果我们在触发事件后查看logger.taps,则该数组仍为空:

1
2
3
4
5
let alert = AlertView() 
var logger = TapLogger()
alert.delegate = logger
alert.fire()
logger.taps // []

        当我们分配给 alert.delegate 时,Swift复制了该结构。 因此,这些 taps 不是记录在记录器中,而是记录在alert.delegate中。 更糟糕的是,当我们分配值时,我们失去了价值的类型。 为了获取信息,我们需要使用条件类型转换:

1
2
3
4
if let theLogger = alert.delegate as? TapLogger { 
print(theLogger.taps)
}
// [1]

        显然,这种方法行不通。 使用类时,创建引用循环很容易;使用结构时,原始值不会发生突变。 简而言之:使用结构时,委托协议没有多大意义。

用函数代替 Delegates

        如果委托协议仅定义了一个方法,我们可以简单地将委托属性替换为直接存储回调函数的属性。 在我们的情况下,这可以是一个可选的 buttonTapped属性 ,默认情况下为nil

1
2
3
4
5
6
7
8
9
10
class AlertView {
var buttons: [String]
var buttonTapped: ((_ buttonIndex: Int) -> ())?
init(buttons: [String] = ["OK", "Cancel"]) {
self.buttons = buttons
}
func fire() {
buttonTapped?(1)
}
}

        函数类型的(_ buttonIndex:Int)->()表示法有点奇怪,因为内部名称 buttonIndex 与代码中的其他地方无关。 上面我们提到,不幸的是,函数类型没有参数标签; 但是,它们可以具有显式的空白参数标签和内部参数名称。 这是经过官方批准的解决方法,可以在函数类型标签中提供参数以用于文档编制,直到Swift支持更好的方法为止。

        和以前一样,我们可以创建一个logger结构,然后创建一个警报视图实例和一个logger变量:

1
2
3
4
5
6
7
8
struct TapLogger { 
var taps: [Int] = []
mutating func logTap(index: Int) {
taps.append(index)
}
}
let alert = AlertView()
var logger = TapLogger()

        但是,我们不能简单地将 logTap方法 分配给 buttonTapped属性 。 Swift编译器告诉我们“不允许部分应用'mutating'方法”

1
alert.buttonTapped = logger.logTap // Error

        在上面的代码中,尚不清楚应在作业中执行什么操作。 记录器是否被复制? 还是应该buttonTapped改变原始变量(即记录器被捕获)?

        为了使这项工作有效,我们必须将赋值的右边包装在一个闭包中。 这样做的好处是非常清楚地表明,我们现在正在捕获原始的logger变量(而不是值),并且正在对其进行突变:

1
alert.buttonTapped = { logger.logTap(index: $0) }

        另一个好处是,现在取消了命名的耦合:回调属性称为buttonTapped,而实现该属性的函数称为logTap。 除了方法之外,我们还可以指定一个匿名函数:

1
alert.buttonTapped = { print("Button \($0) was tapped") }

        将回调与类结合使用时,有一些警告。 让我们回到我们的视图控制器示例。 在其初始值设定项中,视图控制器现在可以将其buttonTapped方法分配给警报视图的回调处理程序,而不必将自己分配为警报视图的委托:

1
2
3
4
5
6
7
8
9
10
class ViewController { 
let alert: AlertView
init() {
alert = AlertView(buttons: ["OK", "Cancel"])
alert.buttonTapped = self.buttonTapped(atIndex:)
}
func buttonTapped(atIndex index: Int) {
print("Button tapped: \(index)")
}
}

        alert.buttonTapped = self.buttonTapped(atIndex :)行看起来像是一个无辜的任务,但是请注意:我们刚刚创建了一个引用循环! 对对象的实例方法的每个引用(例如示例中的self.buttonTapped)都隐式捕获对象。 要了解为什么必须这样做,请考虑警报视图的角度:当警报视图调用存储在其buttonTapped属性中的回调函数时,该函数必须以某种方式“知道”它需要调用哪个对象的实例方法,即 仅存储对ViewController.buttonTapped(atIndex :)的引用而不知道实例是不够的。

        我们可以将self.buttonTapped(atIndex :)缩短为self.buttonTapped或仅仅为buttonTapped; 所有这三个都指同一个功能。 可以省略参数标签,只要这样做不会造成歧义。

        为了避免强引用,通常需要将方法调用包装在另一个弱捕获对象的闭包中:

1
2
3
alert.buttonTapped = { [weak self] index in 
self?.buttonTapped(atIndex: index)
}

        这样,警报视图就不会强烈引用视图控制器。 如果我们可以保证警报视图的生命周期与视图控制器相关联,那么另一种选择是使用无所有权而不是弱项。 弱,如果警报视图超出视图控制器,则在调用函数时self在闭包内为nil

        如果你查看了ViewController.buttonTapped表达式的类型,你会注意到它是(ViewController)->(Int)->()。 发生什么事了?在幕后,实例方法被建模为以下函数:
给定实例,返回另一个函数,然后对该函数进行操作实例。 someVC.buttonTapped实际上只是编写ViewController.buttonTapped(someVC)的另一种方式-这两个表达式都返回一个(Int)->()类型的函数,而该函数是一个已强烈捕获someVC实例的闭包。

        如我们所见,协议和回调函数之间存在一定的权衡。协议增加了一些冗长性,但是具有弱委托的纯类协议无需担心引入引用循环。用函数替换委托可以增加很多灵活性,并允许你使用结构和匿名函数。但是,在处理类时,需要注意不要引入引用循环。

        同样,当你需要多个紧密相关的回调函数时(例如,为表视图提供数据时),将它们按协议分组在一起而不是单独进行回调会很有帮助。另一方面,当使用协议时,单个类型必须实现所有方法。

        要取消注册委托或函数回调,我们可以简单地将其设置为nil。当我们的类型存储委托或回调数组时该怎么办?使用基于类的委托,我们可以简单地从委托列表中删除一个对象。使用回调函数,这并不是那么简单。我们无法添加功能,因此需要添加额外的基础架构以进行注销。

输入参数和突变方法

        在Swiftinout参数前面使用的“&”可能会给你印象,特别是如果你具有CC ++背景的话,inout参数本质上是通过引用。 但事实并非如此。 inout按值传递和复制回传,而不是按引用传递。 引用Swift编程语言:

        inout参数具有一个值,该值传递给函数,由函数修改,然后从函数传递回以替换原始值。

        为了了解可以将哪种表达式作为inout参数传递,我们需要区分左值和右值。 左值描述内存位置。 lvalue是“左值”的缩写,因为左值是可以出现在赋值左侧的表达式。 例如,array [0]是一个左值,因为它描述了数组中第一个元素的存储位置。 右值描述一个值。 2 + 2是一个值,它描述了值4。 你不能在赋值语句的左侧输入2 + 2或4。

        对于inout参数,你只能传递左值,因为突变右值没有意义。 在常规函数和方法中使用inout参数时,需要明确地传入它们:每个左值都必须以开头。 例如,当我们调用增量函数(需要一个inout Int)时,我们可以通过在变量前面加上一个&符号来传递变量:

1
2
3
4
5
func increment(value: inout Int) { 
value += 1
}
var i=0
increment(value: &i)

        如果使用let定义变量,则不能将其用作左值。 这是有道理的,因为我们不允许对let变量进行突变; 我们只能使用“可变”左值:

1
2
let y:Int = 0 
increment(value: &y) // Error

        除了变量之外,还有一些其他东西也是左值。 例如,我们还可以传入数组下标(如果使用var定义了数组):

1
2
3
var array = [0, 1, 2] 
increment(value: &array[0])
array // [1, 1, 2]

        实际上,这适用于每个下标(包括你自己的自定义下标),只要它们都定义了get和set即可。 同样,我们可以将属性用作左值,但前提是它们必须同时定义get和set:

1
2
3
4
5
6
7
struct Point { 
var x: Int
var y: Int
}
var point = Point(x: 0, y: 0)
increment(value: &point.x)
point // Point(x: 1, y: 0)

        如果属性是只读的(即,只有get可用),我们不能将其用作inout参数

1
2
3
4
5
6
extension Point {
var squaredDistance: Int {
return x*x + y*y
}
}
increment(value: &point.squaredDistance) // Error

        运算符也可以取inout值,但为简单起见,在调用时不需要“与”号; 我们只指定左值。 例如,让我们重新添加后缀增量运算符,该运算符在Swift 3中已删除:

1
2
3
4
5
postfix func ++(x: inout Int) { 
x+=1
}
point.x++
point // Point(x: 2, y: 0)

        变异算子甚至可以与可选链结合使用。 在这里,我们将增量操作链接到字典下标访问:

1
2
3
var dictionary = ["one": 1] 
dictionary["one"]?++
dictionary["one"] // Optional(2)

        请注意,如果键查询返回nil,则不会执行++运算符。

        编译器可以优化inout变量以通过引用传递,而不是复制进出。 但是,文档中明确指出我们不应该依赖此行为。

        我们将在下一章“结构和类”中回到inout,在这里我们将探讨采用inout参数的变异方法和函数之间的相似之处。

嵌套函数和inout

        你可以在嵌套函数内使用inout参数,但是Swift会确保你的用法是安全的。 例如,你可以定义一个嵌套函数(使用func或使用闭包表达式)并安全地更改inout参数

1
2
3
4
5
6
7
8
9
10
11
12
func incrementTenTimes(value: inout Int) { 
func inc() {
value += 1
}
for _ in 0..<10 {
inc()
}
}

var x=0
incrementTenTimes(value: &x)
x // 10

        但是,不允许你使该inout参数转义(在本章的最后,我们将详细讨论转义函数):

1
2
3
4
5
6
7
func escapeIncrement(value: inout Int) -> () -> () { 
func inc() {
value += 1
}
// Error: nested function cannot capture inout parameter // and escape.
return inc
}

        鉴于inout值在函数返回之前被复制回去,因此这是有道理的。 如果我们以后能以某种方式修改它,应该怎么办? 该值是否应该在某个时候复制回去? 如果源不存在怎么办? 让编译器验证这一点对安全至关重要。

When & Doesn’t Mean inout

        说到不安全的函数,你应该了解的其他含义:将函数参数转换为不安全的指针

        如果函数将UnsafeMutablePointer作为参数,则可以使用将变量传递给它,类似于使用inout参数的方式。 但是在这里,你实际上是通过引用传递的-实际上是通过指针传递的

        这是增量,写成采用不安全的可变指针而不是inout

1
2
3
4
5
6
7
func incref(pointer: UnsafeMutablePointer<Int>) -> () -> Int { 
// Store a copy of the pointer in a closure.
return {
pointer.pointee += 1
return pointer.pointee
}
}

        正如我们将在后面的章节中讨论的那样,Swift数组隐式地衰减到指针,以使C的互操作性变得轻松愉快。 现在,假设你传入一个超出范围的数组,然后再调用结果函数:

1
2
3
4
5
6
let fun: () -> Int 
do{
var array = [0]
fun = incref(pointer: &array)
}
fun()

        这打开了令人兴奋的未定义行为的世界。 在测试中,以上代码在每次运行时都打印不同的值:有时为0,有时为1,有时为140362397107840 —有时会导致运行时崩溃。

        这里的道义是:知道你要传递的内容。 附加时,你可能正在调用安全的 Swift inout语义,或者将可怜的变量转换为不安全指针的残酷世界。 在处理不安全的指针时,请特别注意变量的生命周期。 我们将在“互操作性”一章中对此进行更详细的介绍。

Properties

        有两种不同于常规方法的特殊方法:计算属性和下标。 计算属性看起来像常规属性,但是它
不使用任何内存来存储其值。 取而代之的是,每次访问属性时都会动态计算该值。 计算属性实际上只是一种具有异常定义和调用约定的方法。

        让我们看看定义属性的各种方法。 我们将从代表GPS轨迹的结构开始。 它将所有记录的点存储在称为record的数组中:

1
2
3
4
5
import CoreLocation

struct GPSTrack {
var record: [(CLLocation, Date)] = []
}

        如果我们想让record属性对外部只读,而对内部只读,则可以使用 private(set)fileprivate(set)修饰符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct GPSTrack {
private(set) var record: [(CLLocation, Date)] = []
}
```

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;要访问GPS轨迹中的所有时间戳,我们创建一个计算属性:

``` Swift
extension GPSTrack {
/// Returns all the timestamps for the GPS track.
/// - Complexity: O(*n*), where *n* is the number of points recorded.
var timestamps: [Date] {
return record.map { $0.1 }
}
}

        由于我们未指定设置器,因此timestamps属性为只读。 结果未缓存; 每次你访问该属性时,它都会计算结果。 《Swift API设计指南》建议你记录每个非 O(1) 的计算属性的复杂性,因为调用者可能会假设访问属性需要花费固定时间。

Change Observers

        我们还可以实现willSetdidSet处理程序,以使属性和变量在每次设置属性时都被调用(即使值不变)。 分别在存储新值之前和之后立即调用它们。 一种有用的情况是使用Interface Builder时:我们可以实现didSet来了解何时连接IBOutlet,然后可以在处理程序中执行其他配置。 例如,如果我们想在标签可用时设置标签的文本颜色,则可以执行以下操作:

1
2
3
4
5
6
7
class SettingsController: UIViewController { 
@IBOutlet weak var label: UILabel? {
didSet {
label?.textColor = .black
}
}
}

        必须在属性的声明位置定义观察者-你不能在扩展名中追溯添加观察者。 因此,它们是该类型设计人员的工具,而不是用户的工具。 willSet和didSet处理程序本质上是定义一对属性的简写:一个提供存储的私有存储属性,以及一个公共计算属性,其setter在将值存储到存储属性之前和/或之后执行附加工作。 这与Foundation中的键值观察机制有根本的区别,Foundation中的键值观察机制通常由对象的使用者用来观察内部变化,而无论类的设计者是否打算这样做。

        但是,你可以覆盖子类中的属性以添加观察者。 这是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Robot { 
enum State {
case stopped, movingForward, turningRight, turningLeft
}
var state = State.stopped
}

class ObservableRobot: Robot {
override var state: State {
willSet {
print("Transitioning from \(state) to \(newValue)")
}
}
}

var robot = ObservableRobot()
robot.state = .movingForward
// Transitioning from stopped to movingForward

        这仍然与变更观察者作为类型的内部特征的本质保持一致。 如果不允许,则子类可以通过使用执行其他工作的计算后的setter覆盖存储的属性来达到相同的效果。

        这些功能的实现反映了用法上的差异。 KVO使用Objective-C运行时将观察者动态添加到类的setter中,这在当前版本的Swift中是不可能实现的,尤其是对于值类型Swift中的属性观察是纯粹的编译时功能

惰性存储属性

        惰性地初始化值是一种常见的模式,Swift具有一个特殊的关键字lazy来定义惰性属性。 请注意,必须始终将惰性属性声明为var,因为直到 初始化完成后才可能设置其初始值Swift有一个严格的规则,即在实例的初始化完成之前,让常量必须具有一个值。 惰性修饰符是记忆的一种非常特定的形式。

        例如,如果我们有一个显示GPSTrack的视图控制器,则可能需要一个轨迹的预览图像。 通过使该属性变得懒惰,我们可以将昂贵的图像生成推迟到第一次访问该属性时:

1
2
3
4
5
6
7
8
9
class GPSTrackViewController: UIViewController { 
var track: GPSTrack = GPSTrack()

lazy var preview: UIImage = {
for point in track.record {
// Do some expensive computation.
}
return UIImage(/* ... */) }()
}

        请注意,我们是如何定义lazy属性的:它是一个闭包表达式,它返回要存储的值(在本例中为图像)。 首次访问该属性时,将执行闭包(请注意结尾处的括号),并将其返回值存储在该属性中。 这是懒惰属性的常见模式,该属性需要初始化多个衬垫。

        由于惰性变量需要存储,因此我们需要在GPSTrackViewController的定义中定义惰性属性。 与计算属性不同,扩展中无法定义存储属性和惰性属性

        如果track属性更改,则预览不会自动失效。 让我们看一个更简单的例子,看看发生了什么。 我们有一个Point结构,并将distanceFromOrigin存储为一个惰性计算属性:

1
2
3
4
5
6
7
8
9
10
11

struct Point {
var x: Double
var y: Double
private(set) lazy var distanceFromOrigin: Double
= (x*x + y*y).squareRoot()
init(x: Double, y: Double) {
self.x = x
self.y = y
}
}

        创建点时,我们可以访问distanceFromOrigin属性,它将计算值并将其存储以供重用。 但是,如果我们随后更改x值,则该值不会反映在distanceFromOrigin中:

1
2
3
4
var point = Point(x: 3, y: 4) 
point.distanceFromOrigin // 5.0
point.x += 10
point.distanceFromOrigin // 5.0

        请务必注意这一点。 解决该问题的一种方法是在xydidSet属性观察器中重新计算distanceFromOrigin,但是distanceFromOrigin不再是真正的懒惰了:它会在x或y每次更改时进行计算。 当然,在此示例中,解决方案很简单:我们应该从一开始就将distanceFromOrigin设置为常规的(非延迟)计算属性。

        访问惰性属性是一项变异操作,因为该属性的初始值是在首次访问时设置的。 当一个结构包含一个惰性属性时,访问该惰性属性的结构的任何所有者都必须因此将包含该结构的变量声明为var,因为访问该属性意味着有可能对其容器进行突变。 因此,这是不允许的:

1
2
3
let immutablePoint = Point(x: 3, y: 4) 
immutablePoint.distanceFromOrigin
// Error: Cannot use mutating getter on immutable value.

        强制所有想要访问惰性属性的Point类型用户使用var是一个巨大的不便,这通常会使惰性属性不适用于结构

        此外,请注意,lazy关键字不会执行任何线程同步。如果在计算该值之前多个线程同时访问一个惰性属性,则该计算可能会执行一次以上,同时还会产生任何副作用。

        在Swift的开源时代初期,Swift团队提出了一种使用“行为”注释属性的通用机制。 Property 行为从未得到实施,但是这个想法在2019年以所谓的 Property 代表提案的形式返回。如果实现,则可以将当前的原始语言功能(如属性观察器和惰性属性)从编译器移至标准库(或第三方库),从而使编译器的复杂性降低,并允许所有开发人员添加自己的属性实现模式。 Property 代表提案未通过其最初的Swift Evolution审查,但我们可能会在以后的Swift版本中看到此功能或类似功能。

下标

        我们已经在标准库中看到了下标。 例如,我们可以像这样执行字典查找:dictionary [key]。 这些下标在很大程度上是函数和计算属性的混合体,具有自己的特殊语法。 像函数一样,它们也接受参数。 像计算的属性一样,它们可以是只读的(使用get)或读写的(使用get set)。 就像普通函数一样,我们可以通过提供具有不同类型的多个变体(属性无法实现)来重载它们。 例如,数组默认具有两个下标-一个用于访问单个元素,另一个用于获取切片(准确地说,这些是在Collection协议中声明的):

1
2
3
4

let fibs = [0, 1, 1, 2, 3, 5]
let first = fibs[0] // 0
fibs[1..<3] // [1, 1]

自定义下标

        我们可以为自己的类型添加下标支持,也可以使用新的下标重载来扩展现有类型。 举例来说,让我们定义一个Collection下标,该下标接受一个索引列表并返回这些索引处所有元素的数组:

1
2
3
4
5
6
7
8
9
extension Collection {
subscript(indices indexList: Index...) -> [Element] {
var result: [Element] = []
for index in indexList {
result.append(self[index])
}
return result
}
}

        请注意,我们如何使用显式参数标签将下标与标准库中的下标区分开。 三个点表示indexList是可变参数。 调用方可以传递零个或多个指定类型(此处是集合的索引类型)的逗号分隔值。 在函数内部,参数可以作为数组使用。

我们可以这样使用新的下标:

1
Array("abcdefghijklmnopqrstuvwxyz")[indices: 7, 4, 11, 11, 14] // ["h", "e", "l", "l", "o"]

高级下标

        下标不限于单个参数。 我们已经看到了一个带多个参数的下标示例:带键和默认值的字典下标。 如果你有兴趣,请在Swift源代码中查看其实现。

        下标的参数或返回类型也可以是通用的。 考虑类型为[String:Any]的异构字典:

1
2
3
4
5
6
7
8
9
var japan: [String: Any] = [ 
"name": "Japan",
"capital": "Tokyo",
"population": 126_440_000,
"coordinates": [
"latitude": 35.0,
"longitude": 139.0
]
]

        如果你想在此字典中更改嵌套值,例如 坐标的纬度,你会发现这并不容易:

1
2
// Error: Type 'Any' has no subscript members.
japan["coordinates"]?["latitude"] = 36.0

        好的,这是可以理解的。 表达式japan [“coordinate”]的类型为 Any? ,因此
你可能会在应用嵌套下标之前尝试将其转换为字典:

1
2
// Error: Cannot assign to immutable expression.
(japan["coordinates"] as? [String: Double])?["latitude"] = 36.0

        这不仅变得很难看,而且也行不通。 问题是你无法通过类型转换(表达式)来对变量进行突变Japan [“coordinates”] as? [String:Double] 不再是左值。 你必须先将嵌套字典存储在本地变量中,然后对该变量进行突变,然后将本地变量分配回顶级关键字。

        我们可以通过使用通用下标扩展Dictionary来做得更好,该通用下标将所需的目标类型作为第二个参数并尝试在下标实现中进行强制转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
extension Dictionary {
subscript<Result>(key: Key, as type: Result.Type) -> Result? {
get {
return self[key] as? Result
}
set {
// Delete existing value if caller passed nil.
guard let value = newValue else {
self[key] = nil
return
}
// Ignore if types don't match.
guard let value2 = value as? Value else {
return
}
self[key] = value2
}
}
}

        由于我们不再需要向下转换下标返回的值,因此突变操作将转到顶级字典变量:

1
2
japan["coordinates", as: [String: Double].self]?["latitude"] = 36.0 
japan["coordinates"] // Optional(["longitude": 139.0, "latitude": 36.0])

        通用下标使这成为可能,但你会注意到此示例中的最终语法仍然很丑陋。 Swift通常不太适合处理像该字典这样的异构集合。 在大多数情况下,最好为数据定义自己的自定义类型(例如,此处为“国家/地区”结构),并将这些类型与Codable兼容,以将值与数据传输格式进行相互转换。

Key Paths

        Swift 4在语言中增加了关键路径的概念。key路径是对属性的未调用引用,类似于未应用的方法引用。关键路径在Swift的类型系统中填补了一个相当大的漏洞;以前,无法以引用方法(例如String.uppercased)的方式引用类型的属性(例如String.count)。尽管使用了相同的名称,但是Swift的键路径与Objective-CFoundation中使用的键路径有很大不同。稍后我们将有更多话要说。

        键路径表达式以反斜杠开头,例如 \String.count。必须使用反斜杠来区分键路径和可能存在的同名类型属性(假设String也具有静态count属性,那么String.count将返回该属性的值)。类型推断也可以在键路径表达式中使用:如果编译器可以从上下文中推断类型名称,则可以省略类型名称,这会导致 \.count

        鉴于键路径和函数类型引用是如此紧密地相关,因此不幸的是,Swift的语法不同。即使这样,Swift团队也表示有兴趣在将来的版本中为函数类型引用采用反斜杠语法。

        顾名思义,键路径描述了从根值开始的贯穿值层次结构的路径。例如,给定以下“人员”和“地址”类型,\Person.address.street是解析人员街道地址的关键路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Address { 
var street: String
var city: String
var zipCode: Int
}

struct Person {
let name: String
var address: Address
}

let streetKeyPath = \Person.address.street
// Swift.WritableKeyPath<Person, Swift.String>
let nameKeyPath = \Person.name // Swift.KeyPath<Person, Swift.String>

        key路径可以由存储和计算的属性的任意组合以及可选的链接运算符组成。 编译器会自动为所有类型生成一个新的[keyPath:]下标。 你可以使用此下标“调用”key路径,即在给定实例上访问其描述的属性。 因此,“Hello”[keyPath: \.count]等效于 “Hello” .count 。 或者,对于我们当前的示例:

1
2
3
let simpsonResidence = Address(street: "1094 Evergreen Terrace", city: "Springfield", zipCode: 97475)
var lisa = Person(name: "Lisa Simpson", address: simpsonResidence)
lisa[keyPath: nameKeyPath] // Lisa Simpson

        如果你看一下上面两个键路径变量的类型,你会注意到 nameKeyPath 的类型为 KeyPath <Person,String>(即可以应用于Person并产生String的强类型键路径) ,而streetKeyPath的类型为WritableKeyPath。 因为形成后一个键路径的所有属性都是可变的,所以键路径本身允许基础值的改变:

1
lisa[keyPath: streetKeyPath] = "742 Evergreen Terrace"

        对nameKeyPath进行相同操作会产生错误,因为底层property是不可变的。

        关键路径不仅可以描述属性,我们也可以使用它们来描述下标。 例如,以下语法可用于提取数组中第二个人值的名称:

1
2
3
var bart = Person(name: "Bart Simpson", address: simpsonResidence) 
let people = [lisa, bart]
people[keyPath: \.[1].name] // Bart Simpson

        相同的语法也可以用于在关键路径中包括字典下标。

关键路径可以用函数建模

        从基本类型Root映射到Value类型的属性的密钥路径与类型(Root)-> Value的函数非常相似,或者对于可写密钥路径,有一对用于获取和设置值的函数。 相对于此类功能(语法除外)的主要好处是,它们是值。 你可以测试键路径是否相等并将它们用作字典键(它们符合Hashable),并且可以确保键路径是无状态的,这与可能捕获可变状态的函数不同。 正常功能无法实现所有这些功能。

        通过将一个key路径附加到另一个key路径,也可以构成key路径。 请注意,类型必须匹配:如果你以从A到B的键路径开头,则附加的键路径必须具有B的根类型,然后生成的键路径将从A映射到附加键路径的值类型, 说C:

1
2
3
// KeyPath<Person, String> + KeyPath<String, Int> = KeyPath<Person, Int>
let nameCountKeyPath = nameKeyPath.appending(path: \.count)
// Swift.KeyPath<Person, Swift.Int>

        让我们重新编写本章前面的排序描述符,以使用键路径代替函数。 我们之前将sortDescriptor定义为采用函数,(根)->值:

1
2
3
4
5
6
7
8
typealias SortDescriptor<Root> = (Root, Root) -> Bool

func sortDescriptor<Root, Value>(key: @escaping (Root) -> Value)
-> SortDescriptor<Root> where Value: Comparable {
return { key($0) < key($1) }
}
// Usage
let streetSD: SortDescriptor<Person> = sortDescriptor { $0.address.street }

        我们可以添加一个变体,用于从键路径构造排序描述符。 我们使用下标以获取值的关键路径:

1
2
3
4
5
func sortDescriptor<Root, Value>(key: KeyPath<Root, Value>) -> SortDescriptor<Root> where Value: Comparable {
return { $0[keyPath: key] < $1[keyPath: key] }
}
// Usage
let streetSDKeyPath: SortDescriptor<Person> = sortDescriptor(key: \.address.street)

        虽然有一个采用关键路径的sortDescriptor构造函数很有用,但它并没有为我们提供灵活性功能。 关键路径依赖于可比值。 仅使用关键路径,我们就无法轻松地按不同的谓词进行排序(例如,执行不区分大小写的本地化比较)。

可写关键路径

        可写关键路径是特殊的。 你可以使用它来读取或写入值。 因此,它等效于一对函数:一个用于获取属性 ((Root) -> Value),另一个用于设置属性 ((inout Root,Value) -> Void) 。 可写关键路径比只读关键路径要大得多。 首先,它们以简洁的语法捕获了大量代码。 将streetKeyPath与等效的gettersetter对进行比较:

1
2
3
4
5
6
7
8
9
10
11
12
13
let streetKeyPath = \Person.address.street

let getStreet: (Person) -> String = { person in
return person.address.street
}

let setStreet: (inout Person, String) -> () = { person, newValue in
person.address.street = newValue
}

// Setter usage
lisa[keyPath: streetKeyPath] = "1234 Evergreen Terrace"
setStreet(&lisa, "1234 Evergreen Terrace")

        可写键路径对于数据绑定特别有用,在数据绑定中,你想将两个属性彼此绑定:当属性一更改时,属性二应自动更新,反之亦然。例如,你可以将model.name属性绑定到textField.textAPI的用户需要指定如何读取和写入model.nametextField.text,而关键路径仅用于捕获和写入。

        我们还需要一种观察属性变化的方法。为此,我们在Cocoa中使用键值观察机制,这意味着该示例仅适用于类,并且仅适用于Apple平台Foundation提供了一种类型安全的KVO API,该API隐藏了Objective-C键路径的字符串类型世界。 NSObject方法observe(_:options:changeHandler :)观察键路径(作为强类型Swift键路径传递),并在属性更改时调用更改处理程序。**不要忘记将你要观察的任何属性标记为@objc动态**。否则,KVO将无法正常工作。

        我们的目标是在两个NSObject之间实现双向绑定,但让我们从单向绑定开始:每当观察到的self属性发生变化时,我们也会更改另一个对象。关键路径使我们可以使代码在所涉及的特定属性上通用—调用者指定了两个对象和两个关键路径,此方法将处理其余的工作:

1
2
3
4
5
6
7
8
9
extension NSObjectProtocol where Self: NSObject { 
func observe<A, Other>(_ keyPath: KeyPath<Self, A>, writeTo other: Other, _ otherKeyPath: ReferenceWritableKeyPath<Other, A>) -> NSKeyValueObservation where A: Equatable, Other: NSObjectProtocol {
return observe(keyPath, options: .new) { _, change in
guard let newValue = change.newValue, other[keyPath: otherKeyPath] != newValue else {
return // prevent endless feedback loop }
other[keyPath: otherKeyPath] = newValue
}
}
}

        此代码段中有很多要解压的内容。 首先,我们在NSObject的每个子类上定义此方法,然后通过扩展NSObjectProtocol(而不是NSObject)来使用SelfReferenceWritableKeyPath就像WritableKeyPath一样,但是它也允许我们编写使用let声明的引用变量(与其他变量一样)。 (我们稍后将对此进行详细说明。)为避免不必要的写入,我们仅在值更改后才写入其他值。 NSKeyValueObservation返回值是调用者可以用来控制观察的生存期的令牌:当释放该对象或调用者调用其invalidate方法时,观察停止。

        给定observe(_:writeTo:__ :),双向绑定非常简单; 我们在两个对象上都调用observe,并返回两个观察标记:

1
2
3
4
5
6
extension NSObjectProtocol where Self: NSObject {
func bind<A, Other>(_ keyPath: ReferenceWritableKeyPath<Self,A>, to other: Other, _ otherKeyPath: ReferenceWritableKeyPath<Other,A>) -> (NSKeyValueObservation, NSKeyValueObservation) where A: Equatable, Other: NSObject {
let one = observe(keyPath, writeTo: other, otherKeyPath)
let two = other.observe(otherKeyPath, writeTo: self, keyPath) return (one,two)
}
}

        现在,我们可以构造两个不同的对象-Person和TextField,并将name和text属性彼此绑定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
final class Person: NSObject {
@objc dynamic var name: String = ""
}

class TextField: NSObject {
@objc dynamic var text: String = ""
}

let person = Person()
let textField = TextField()
let observation = person.bind(\.name, to: textField, \.text) person.name = "John"
textField.text // John
textField.text = "Sarah"
person.name // Sarah

        如果你来自函数式编程,那么可写的键路径可能会让你想起镜头。 它们紧密相关:从WritableKeypath <Root,Value>,你可以创建Lens <Root,Value>。 镜头在纯函数式语言(例如Haskell或PureScript)中很有用,但在Swift中却没有那么有用,因为Swift内置了可变性。

关键路径层次结构

        密钥路径有五种不同的类型,每种都为前一种增加了更多的精度和功能:

  • AnyKeyPath 类似于功能类型(Any)-> Any?
  • PartialKeyPath 类似于 typeoffunction(Source)-> Any?
  • KeyPath<Source,Target>typeoffunction(Source)-> Target相似。
  • WritableKeyPath<Source,Target> 与一对功能类型类似(Source)->Target (Source,Target) -> ()
  • ReferenceWritableKeyPath<Source,Target> 与类型为 (Source) ->Target(Source,Target) -> () 的一对函数相似。第二个函数可以用Target更新Source值,并且仅在Source是引用类型时才起作用。必须区分WritableKeyPath\ReferenceWritableKeyPath,因为前者的设置者必须将参数inout传入

        当前,此关键路径层次结构被实现为类层次结构。理想情况下,这些将是协议,但是Swift的泛型系统缺少一些使之可行的功能。故意将类层次结构保持关闭状态,以便在将来的发行版中对此进行更改而不会破坏现有代码。

        正如我们之前所看到的,关键路径与函数不同:它们遵循Hashable,将来也可能遵循Codable。这就是为什么我们说AnyKeyPath类似于函数(Any)-> Any。虽然我们可以将键路径转换为其相应的功能,但我们并不总是朝着另一个方向发展。

与Objective-C相比的关键路径

        在FoundationObjective-C中,关键路径被建模为字符串(我们称这些Foundation关键路径是为了将它们与Swift的关键路径区分开)。由于Foundation键路径是字符串,因此它们没有附加任何类型信息。从某种意义上讲,它们类似于AnyKeyPath。如果基础密钥路径拼写错误,格式不正确,或者类型不匹配,则程序可能会崩溃。 (Swift中的#keyPath指令有助于解决拼写错误;编译器可以检查是否存在具有指定名称的属性。)Swift的KeyPathWritableKeypathReferenceWritableKeyPath在构造上是正确的:它们不能被拼写错误并且它们不会允许输入类型错误。

        当功能可能更好时,许多Cocoa API使用(基础)键路径。这在某种程度上是一个历史产物:匿名函数(或块,正如Objective-C所称的)是一个相对较新的功能,而关键路径的存在时间更长。在将块添加到Objective-C之前,要表示类似于功能{$ 0.address.street}的东西并不容易,只是使用关键路径“address.street”

未来发展方向

        关键路径仍在积极讨论中,未来它们可能会变得更加强大。 一种可能的功能是通过可编码协议进行序列化。 这将使我们能够将关键路径保留在磁盘上,通过网络发送它们,等等。 一旦我们可以访问关键路径的结构,就可以进行自省。 例如,我们可以使用键路径的结构来构造类型正确的数据库查询。 如果类型可以自动提供指向其属性的键路径数组,则可以用作运行时反射API的基础。

Autoclosures

        我们都熟悉逻辑AND运算符&&如何评估其参数。 它首先评估其左操作数,如果评估结果为false,则立即返回。 仅当左侧操作数的值为true时,才对右侧操作数的值进行评估。 毕竟,如果左操作数的计算结果为false,则整个表达式都无法计算为true。

        此行为称为短路。 例如,如果我们要检查条件是否满足数组的第一个元素,则可以编写以下代码:

1
2
3
4
let evens = [2,4,6]
if !evens.isEmpty && evens[0] > 10 {
// Perform some work.
}

        在上面的代码段中,我们依赖于短路:只有在第一个条件成立的情况下才会进行数组查找。 如果没有短路,此代码将在空数组上崩溃。

        编写此特定示例的更好方法是使用if let绑定:

1
2
3
if let first = evens.first, first > 10 { 
// Perform some work.
}

        这是短路的另一种形式:仅当第一个条件成功时才评估第二个条件。

        在几乎所有语言中,&&|| 都会短路 语言中内置了运算符。 但是,通常无法定义自己的具有相同行为的运算符或函数。 如果语言支持一流的功能,我们可以通过提供匿名功能而不是值来伪造短路。 例如,假设我们要在Swift中定义一个 and函数 ,使其具有与&&运算符相同的行为:

1
2
3
4
func and(_ l: Bool, _ r: () -> Bool) -> Bool { 
guard l else { return false }
return r()
}

        上面的函数首先检查l的值,如果l的值为false,则返回false。 仅当l为true时,它才返回闭包r中的值。 但是,使用它比使用&&运算符稍微麻烦一些,因为正确的操作数现在必须是一个函数:

1
2
3
if and(!evens.isEmpty, { evens[0] > 10 }) { 
// Perform some work.
}

        Swift具有一个很好的功能,可以使其更漂亮。 我们可以使用@autoclosure属性来告诉编译器它应该在闭包表达式中包装一个特定的参数。 和的定义与上面几乎相同,除了添加的@autoclosure批注:

1
2
3
4
func and(_ l: Bool, _ r: @autoclosure () -> Bool) -> Bool { 
guard l else { return false }
return r()
}

        但是,and的用法现在要简单得多,因为我们不需要将第二个参数包装在闭包中。 我们可以像调用常规Bool参数一样调用它,并且编译器透明地将参数包装在闭包表达式中:

1
2
3
if and(!evens.isEmpty, evens[0] > 10) {
// Perform some work.
}

        这使我们能够定义自己的具有短路行为的函数和运算符。 例如,像??这样的运算符 和!? (如“可选”一章中定义的)现在很容易编写。 在标准库中,assertfatalError之类的函数也使用自动关闭功能,以便仅在真正需要时才评估参数。 通过将断言条件的评估从调用位置推迟到断言函数的主体,可以将这些潜在的昂贵操作完全剥离到不需要它们的优化构建中。

        编写日志记录功能时,自动关闭也可以派上用场。 例如,下面是你编写自己的日志函数的方法,该函数仅在条件为true时才评估日志消息:

1
2
3
4
5
func log(ifFalse condition: Bool, message: @autoclosure () -> (String),
file: String = #file, function: String = #function, line: Int = #line) {
guard !condition else { return }
print("Assertion failed: \(message()), \(file):\(function) (line \(line))")
}

        这意味着你可以在作为消息参数传递的表达式中执行昂贵的计算,如果不使用该值,则不会产生评估成本。 日志功能还使用调试标识符#file#function#line。 他们是当作为默认参数传递给函数时特别有用,因为它们将在调用站点上接收文件名,函数名和行号的值

        但是,请谨慎使用自动关闭功能。 他们的行为违反了正常的预期-例如,如果由于表达式包含在自动闭合中而没有执行表达式的副作用。 引用Apple的Swift书:

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

@escaping 批注

        你可能已经注意到,编译器要求你在某些闭包表达式中(而不是在其他闭包表达式中)明确声明要访问self。例如,我们需要在网络请求的完成处理程序中使用显式的self,而不必在传递给 map或filter 的闭包中对self进行显式的显示。两者之间的区别在于,闭包是存储供以后使用(如网络请求),还是仅在函数范围内同步使用(如map和filter)。

        如果将闭包存储在某个地方(例如存储在某个属性中)以供以后调用,则称该闭包正在转义。相反,从不离开函数本地范围的闭包是不可转义的。使用转义,编译器迫使我们明确地在闭包表达式中使用self,因为无意间强力捕获self是引用周期最常见的原因之一。无法转义的闭包无法创建永久的引用循环,因为在返回定义的函数时,该循环会自动销毁。

        默认情况下,闭包参数是不转义的。如果要存储闭包以供以后使用,则需要将闭包参数标记为 ***@escaping。编译器将对此进行验证:除非你将闭包参数标记为@escaping,否则它将不允许你存储闭包*(例如,将其返回给调用者)。

        在排序描述符示例中,有多个函数参数需要 @escaping属性

1
2
3
4
func sortDescriptor<Root, Value>( key: @escaping (Root) -> Value, by areInIncreasingOrder: @escaping (Value, Value) -> Bool) -> SortDescriptor<Root> {
Overusing autoclosures can make your code hard to understand. The context and function name should make it clear that evaluation is being deferred.
return { areInIncreasingOrder(key($0), key($1)) }
}

        在Swift 3之前,是另一回事:转义是默认设置,你可以选择将结束标记为@noescape。 当前行为更好,因为默认情况下它是安全的:现在需要一个函数参数明确注释以表示参考循环的潜力。 @escaping注释是对开发人员调用该函数的警告。 编译器还可以更好地优化非转义的闭包,使快速路径成为必要时必须明确偏离的规范。

        请注意,默认情况下的非转义规则仅适用于功能参数,然后仅适用于紧邻参数位置的功能类型。 这意味着具有函数类型的存储属性始终在转义(这很有意义)。 令人惊讶的是,对于用作参数但包装在其他类型(例如元组或可选)中的函数也是如此。 由于在这种情况下闭包不再是立即参数,因此它会自动转义。 因此,你无法编写带有函数参数的函数,其中参数既是可选参数也是非转义参数。 在许多情况下,可以通过为闭包提供默认值来避免使参数成为可选参数。 如果不可能,一种解决方法是使用重载来编写该函数的两个变体-一个带有可选的(转义)函数参数,而另一个带有非可选的,非转义参数:

1
2
3
4
5
6
7
8
9
10
func transform(_ input: Int, with f: ((Int) -> Int)?) -> Int { 
print("Using optional overload")
guard let f = f else { return input }
return f(input)
}

func transform(_ input: Int, with f: (Int) -> Int) -> Int {
print("Using non-optional overload")
return f(input)
}

        这样,用nil参数(或可选类型的变量)调用函数将使用可选变量,而传递文字闭包表达式将调用非转义,非可选重载:

1
2
transform(10, with: nil) // Using optional overload
transform(10) { $0 * $0 } // Using non-optional overload

withoutActuallyEscaping

        你可能会遇到这样的情况,你知道闭包无法逃脱,但编译器无法证明它,从而迫使你添加@escaping批注。 为了说明这一点,让我们看一下标准库文档中的示例。 我们在Array上编写了allSatisfy方法的自定义实现,该方法在内部使用数组的惰性视图(不要与上面讨论的惰性属性混淆)。 然后,我们将过滤器应用于惰性视图,并检查是否有任何元素通过过滤器(即,至少有一个元素不满足谓词)。 我们的第一次尝试导致编译错误:

1
2
3
4
5
6
extension Array {
func allSatisfy2(_ predicate: (Element) -> Bool) -> Bool {
// Error: Closure use of non-escaping parameter 'predicate' // may allow it to escape.
return self.lazy.filter({ !predicate($0) }).isEmpty
}
}

        我们将在“Collection协议”一章中详细介绍延迟CollectionAPI。 到目前为止,只要知道惰性视图就可以将后续转换(例如传递给filter的闭包)存储在内部属性中,以便以后应用。 这要求传入的任何闭包都必须转义,这是导致错误的原因,因为我们的谓词参数是不转义的。

        我们可以通过用@escaping注释参数来解决此问题,但是在这种情况下,我们知道闭包不会逃脱,因为惰性集合视图的生命周期与函数的生命周期绑定在一起。 Swift为这种情况提供了逃生舱口,其形式为noActuallyEscaping函数。 它允许你将非转义的闭包传递给期望转义的函数。 这样可以编译并正常工作:

1
2
3
4
5
6
7
8
9
extension Array {
func allSatisfy2(_ predicate: (Element) -> Bool) -> Bool {
return withoutActuallyEscaping(predicate) { escapablePredicate in
self.lazy.filter { !escapablePredicate($0) }.isEmpty
}
}
}
let areAllEven = [1,2,3,4].allSatisfy2 { $0 % 2 == 0 } // false
let areAllOneDigit = [1,2,3,4].allSatisfy2 { $0 < 10 } // true

        请注意,懒惰的实现并没有比allSatisfy的标准库的实现效率更高,后者使用简单的for循环,并且不需要ActuallyEscaping就不需要。 懒惰的实现仅用于演示以下情况:我们知道闭包不会逃逸,但编译器无法证明这一点。

        请注意,你使用的是不使用“实际转义”功能进入不安全区域。 允许闭包的副本从调用中转义到withoutActuallyEscaping会导致未定义的行为。

概括

        函数是Swift中的一流对象。 将函数视为数据可以使我们的代码更灵活。 我们已经看到了如何用简单的函数代替运行时编程。 我们比较了实现委托的不同方法。 我们已经研究了变异函数和inout参数,以及计算属性(实际上是一种特殊的函数)。 最后,我们讨论了@autoclosure@escaping属性。 在有关 泛型和协议 的章节中,我们将提出更多在Swift中使用函数的方式,以获得更大的灵活性。

坚持原创技术分享,您的支持将鼓励我继续创作!

欢迎关注我的其它发布渠道