swift 5.2 swift52域_数组

作者 | Paul Hudson 

随着 Xcode 11.4 的发布,Swift 5.2 也正式到来,新的版本包含少量语言层面的更新,代码大小和内存使用的减少,以及新的诊断体系结构。新的诊断体系结构可以帮助我们更快地理解和解决错误。

在本文中,将通过具体的实例来说明 Swift 5.2 中的一些新特性,以便让您对这些更新有一个更清晰的认识。我建议你通过链接到对应的 Swift Evolution 中,可获取更多信息。

将 Key Path 表达式作为函数

SE-0249 介绍了一个奇特的快捷方式,让我们可以在一些特定情况下以函数调用的方式使用 keypath。

这个 Evolution 提案提议在使用类型为 (Root) -> Value 的函数的地方,可以使用更简便的 \Root.value 来代替,也就是说如果将一个 Car 类型实例作为参数传递给方法,然后返回这个 Car 实例的 licensePlate 属性值,则现在可以直接使用 Car.licensePlate 来代替方法调用。

让我们举个例子,这里有一个 User 类型,定义了 4 个属性:

struct User {
    let name: String
    let age: Int
    let bestFriend: String?

    var canVote: Bool {
        age >= 18
    }
}

我们可以创建该结构的一些实例并将其放入数组,如下所示:

let eric = User(name: "Eric Effiong", age: 18, bestFriend: "Otis Milburn")
let maeve = User(name: "Maeve Wiley", age: 19, bestFriend: nil)
let otis = User(name: "Otis Milburn", age: 17, bestFriend: "Eric Effiong")
let users = [eric, maeve, otis]

我们的目标是获取所有用户的 name 值的数组,那么可以使用类似以下的 key path 来实现:

let userNames = users.map(\.name)
print(userNames)

在此之前,我们必须通过闭包来手动获取 name 值,即

let oldUserNames = users.map { $0.name }

在其它地方也可以使用同样的方法,即以类型实例为参数,并返回其某一属性值的任何地方,都可以使用这种 key path。例如,以下代码返回所有可以投票的用户:

let voters = users.filter(\.canVote)

而以下代码则返回 bestFriend 不为空的用户的好友:

let bestFriends = users.compactMap(\.bestFriend)

可调用类型

SE-0253 在 Swift 中引入了可静态调用的值,这是一种有趣的说法,即如果值的类型实现了一个名为 callAsFunction() 的方法,则现在可以直接以函数的方式来调用该类型的值。无需遵循任何特殊协议即可让这种行为生效;只需要将该方法添加到您的类型中即可。

例如,我们可以创建一个Dice 结构体,它有两个属性:lowerBound 和 upperBound ,然后在类型中添加 callAsFunction ,这样每次调用 Dice 的值时都会得到一个随机值:

struct Dice {
    var lowerBound: Int
    var upperBound: Int

    func callAsFunction() -> Int {
        (lowerBound...upperBound).randomElement()!
    }
}

let d6 = Dice(lowerBound: 1, upperBound: 6)
let roll1 = d6()
print(roll1)

上面的代码将打印一个从1到6的随机数,这与直接使用 callAsFunction() 的效果是相同。例如,我们可以这样调用它:

let d12 = Dice(lowerBound: 1, upperBound: 12)
let roll2 = d12.callAsFunction()
print(roll2)

Swift会根据定义 callAsFunction() 的方式自动调整调用方式。例如,可以根据需要添加任意数量的参数,可以控制返回值,甚至可以根据需要将方法标记为 mutating

例如,以下代码将创建一个 StepCounter 结构,该结构跟踪某人已经走了多远,并报告他们是否达到了 10,000 步的目标:

struct StepCounter {
    var steps = 0

    mutating func callAsFunction(count: Int) -> Bool {
        steps += count
        print(steps)
        return steps > 10_000
    }
}

var steps = StepCounter()
let targetReached = steps(count: 10)

还有更高级的用法,callAsFunction() 支持 throws 和 rethrows ,甚至可以在单个类型上定义多个 callAsFunction() 方法,Swift 会根据调用方式选择正确的方法,就像常规重载一样。

下标可以声明默认参数值

将自定义下标添加到类型时,现在可以将默认参数用于任何参数。例如,如果我们有一个带有自定义下标的 PoliceForce 结构体,以从部队中获取军官信息,我们可以添加一个 default 参数,以便在有人尝试读取数组范围之外的索引时返回默认值:

struct PoliceForce {
    var officers: [String]

    subscript(index: Int, default default: String = "Unknown") -> String {
        if index >= 0 && index < officers.count {
            return officers[index]
        } else {
            return `default`
        }
    }
}

let force = PoliceForce(officers: ["Amy", "Jake", "Rosa", "Terry"])
print(force[0])
print(force[5])

以上代码将打印 "Amy",然后打印 "Unknown",后者是因为数组越界而输出默认值。需要注意的是,如果要让参数可用,则需要重复两次标签,因为下标是不使用参数标签的。

因此,由于我在下标中使用了 default default ,所以可以使用如下自定义值:

print(force[-1, default: "The Vulture"])

lazy 序列的多个 filter 的顺序现在颠倒了

Swift 5.2 中有一个微小变化有可能导致你的功能中断:如果您使用诸如数组之类的惰性序列,并对其应用多个过滤器,则这些过滤器现在将以相反的顺序运行。

例如,下面的代码有一个过滤器,该过滤器选择以S开头的名称,然后另一个过滤器打印出名称,然后返回 true:

let people = ["Arya", "Cersei", "Samwell", "Stannis"]
    .lazy
    .filter { $0.hasPrefix("S") }
    .filter { print($0); return true }
_ = people.count

在Swift 5.2和更高版本中,上述代码将打印“ Samwell”和“ Stannis”,因为在第一个过滤器运行之后,这两个字符串是剩下的进入第二个过滤器的唯一名称。但是在Swift 5.2之前,则会打印所有四个名称,因为第二个过滤器将在第一个过滤器之前运行。这会令人困惑,因为如果删除 lazy,那么无论Swift 是哪个版本,代码始终只会返回Samwell和Stannis。

这很有问题,因为其行为取决于代码的运行位置:如果您在iOS 13.3或更早版本或macOS 10.15.3或更早版本上运行Swift 5.2代码,则以原始的方式执行,但是在较新的操作系统上运行的相同代码将提供新的正确行为。

因此,此更改可能会导致代码意外中断,但希望这只是短期内的麻烦。

新的改进的诊断体系

Swift 5.2 引入了一种新的诊断体系结构,该体系结构旨在在发生编码错误时提高 Xcode 发出的错误消息的质量和准确性。这在使用SwiftUI代码时尤其明显,因为Swift经常会误报错误消息。

例如,考虑如下代码:

struct ContentView: View {
    @State private var name = 0

    var body: some View {
        VStack {
            Text("What is your name?")
            TextField("Name", text: $name)
                .frame(maxWidth: 300)
        }
    }
}

这里试图将 TextField 视图绑定到整数 @State 属性,这样做是无效的。在 Swift 5.1 中,报出的错误是 frame() 修饰符出错,提示的是 'Int' is not convertible to 'CGFloat? ,但是在 Swift 5.2 及更高版本中,这可以正确地识别是 $name 的绑定错误:Cannot convert value of type Binding<Int> to expected argument type Binding<String>

swift 5.2 swift52域_User_02