Swift 4 引入了一种名为 KeyPath
的新类型,它允许使用非常简洁的语法访问对象的属性。例如
let string = "Foo"
let keyPathForCount = \String.count
let count = string[keyPath: keyPathForCount] // count == 3
最棒的是,这种语法可以非常简洁,因为它支持类型推断和属性链。
因此,我认为利用这个新概念来构建一个 API 会很好,该 API 允许以非常声明式的方式执行数据操作。
SQL 是一种非常适合此类操作的语言,因此我从中汲取灵感,并使用 KeyPath
在 Swift 4 中实现了它的大部分标准运算符。
但真正使 KeyPathKit
从竞争中脱颖而出的是其巧妙的语法,它允许以非常无缝的方式表达查询。例如
contacts.filter(where: \.lastName == "Webb" && \.age < 40)
将以下内容添加到您的 Podfile
中
pod "KeyPathKit"
将以下内容添加到您的 Cartfile
中
github "vincent-pradeilles/KeyPathKit"
创建一个文件 Package.swift
// swift-tools-version:4.0
import PackageDescription
let package = Package(
name: "YourProject",
dependencies: [
.package(url: "https://github.com/vincent-pradeilles/KeyPathKit.git", "1.0.0" ..< "2.0.0")
],
targets: [
.target(name: "YourProject", dependencies: ["KeyPathKit"])
]
)
为了演示运算符的用法,定义了以下模拟数据
struct Person {
let firstName: String
let lastName: String
let age: Int
let hasDriverLicense: Bool
let isAmerican: Bool
}
let contacts = [
Person(firstName: "Charlie", lastName: "Webb", age: 10, hasDriverLicense: false, isAmerican: true),
Person(firstName: "Alex", lastName: "Elexson", age: 22, hasDriverLicense: false, isAmerican: true),
Person(firstName: "Charles", lastName: "Webb", age: 45, hasDriverLicense: true, isAmerican: true),
Person(firstName: "Alex", lastName: "Zunino", age: 34, hasDriverLicense: true, isAmerican: true),
Person(firstName: "Alex", lastName: "Alexson", age: 8, hasDriverLicense: false, isAmerican: true),
Person(firstName: "John", lastName: "Webb", age: 28, hasDriverLicense: true, isAmerican: true),
Person(firstName: "Webb", lastName: "Elexson", age: 30, hasDriverLicense: true, isAmerican: true)
]
对 Bool
类型的属性执行布尔 AND 运算。
contacts.and(\.hasDriverLicense)
contacts.and(\.isAmerican)
false
true
计算数值属性的平均值。
contacts.average(of: \.age).rounded()
25
过滤掉属性值不在范围内的元素。
contacts.between(\.age, range: 20...30)
// or
contacts.filter(where: 20...30 ~= \.age)
[Person(firstName: "Alex", lastName: "Elexson", age: 22, hasDriverLicense: false, isAmerican: true),
Person(firstName: "John", lastName: "Webb", age: 28, hasDriverLicense: true, isAmerican: true),
Person(firstName: "Webb", lastName: "Elexson", age: 30, hasDriverLicense: true, isAmerican: true)]
返回序列是否包含一个元素,该元素的指定布尔属性或谓词为真。
contacts.contains(where: \.hasDriverLicense)
contacts.contains(where: \.lastName.count > 10)
true
false
返回属性的所有不同值。
contacts.distinct(\.lastName)
["Webb", "Elexson", "Zunino", "Alexson"]
通过跳过元素(当 Bool
类型的属性或谓词评估为 true 时)并返回剩余元素,返回一个子序列。
contacts.drop(while: \.age < 40)
[Person(firstName: "Charles", lastName: "Webb", age: 45, hasDriverLicense: true, isAmerican: true),
Person(firstName: "Alex", lastName: "Zunino", age: 34, hasDriverLicense: true, isAmerican: true),
Person(firstName: "Alex", lastName: "Alexson", age: 8, hasDriverLicense: false, isAmerican: true),
Person(firstName: "John", lastName: "Webb", age: 28, hasDriverLicense: true, isAmerican: true),
Person(firstName: "Webb", lastName: "Elexson", age: 30, hasDriverLicense: true, isAmerican: true)]
过滤掉一个(或多个)布尔属性值为 false
的元素。
contacts.filter(where: \.hasDriverLicense)
[Person(firstName: "Charles", lastName: "Webb", age: 45, hasDriverLicense: true, isAmerican: true),
Person(firstName: "Alex", lastName: "Zunino", age: 34, hasDriverLicense: true, isAmerican: true),
Person(firstName: "John", lastName: "Webb", age: 28, hasDriverLicense: true, isAmerican: true),
Person(firstName: "Webb", lastName: "Elexson", age: 30, hasDriverLicense: true, isAmerican: true)]
Filter 也适用于谓词
contacts.filter(where: \.firstName == "Webb")
[Person(firstName: "Charlie", lastName: "Webb", age: 10, hasDriverLicense: false, isAmerican: true),
Person(firstName: "Charles", lastName: "Webb", age: 45, hasDriverLicense: true, isAmerican: true),
Person(firstName: "John", lastName: "Webb", age: 28, hasDriverLicense: true, isAmerican: true)]
过滤掉 Equatable
属性的值不在给定 Sequence
中的元素。
contacts.filter(where: \.firstName, in: ["Alex", "John"])
[Person(firstName: "Alex", lastName: "Elexson", age: 22, hasDriverLicense: false, isAmerican: true),
Person(firstName: "Alex", lastName: "Zunino", age: 34, hasDriverLicense: true, isAmerican: true),
Person(firstName: "Alex", lastName: "Alexson", age: 8, hasDriverLicense: false, isAmerican: true),
Person(firstName: "John", lastName: "Webb", age: 28, hasDriverLicense: true, isAmerican: true)]
过滤掉 Comparable
属性的值大于常量的元素。
contacts.filter(where: \.age, lessThan: 30)
// or
contacts.filter(where: \.age < 30)
[Person(firstName: "Charlie", lastName: "Webb", age: 10, hasDriverLicense: false, isAmerican: true),
Person(firstName: "Alex", lastName: "Elexson", age: 22, hasDriverLicense: false, isAmerican: true),
Person(firstName: "Alex", lastName: "Alexson", age: 8, hasDriverLicense: false, isAmerican: true),
Person(firstName: "John", lastName: "Webb", age: 28, hasDriverLicense: true, isAmerican: true)]
contacts.filter(where: \.age, lessOrEqual: 30)
// or
contacts.filter(where: \.age <= 30)
[Person(firstName: "Charlie", lastName: "Webb", age: 10, hasDriverLicense: false, isAmerican: true),
Person(firstName: "Alex", lastName: "Elexson", age: 22, hasDriverLicense: false, isAmerican: true),
Person(firstName: "Alex", lastName: "Alexson", age: 8, hasDriverLicense: false, isAmerican: true),
Person(firstName: "John", lastName: "Webb", age: 28, hasDriverLicense: true, isAmerican: true),
Person(firstName: "Webb", lastName: "Elexson", age: 30, hasDriverLicense: true, isAmerican: true)]
过滤掉字符串属性的值与正则表达式不匹配的元素。
contacts.filter(where: \.lastName, like: "^[A-Za-z]*son$")
[Person(firstName: "Alex", lastName: "Elexson", age: 22, hasDriverLicense: false, isAmerican: true),
Person(firstName: "Alex", lastName: "Alexson", age: 8, hasDriverLicense: false, isAmerican: true),
Person(firstName: "Webb", lastName: "Elexson", age: 30, hasDriverLicense: true, isAmerican: true)]
过滤掉 Comparable
属性的值小于常量的元素。
contacts.filter(where: \.age, moreThan: 30)
// or
contacts.filter(where: \.age > 30)
[Person(firstName: "Charles", lastName: "Webb", age: 45, hasDriverLicense: true, isAmerican: true),
Person(firstName: "Alex", lastName: "Zunino", age: 34, hasDriverLicense: true, isAmerican: true)]
contacts.filter(where: \.age, moreOrEqual: 30)
// or
contacts.filter(where: \.age >= 30)
[Person(firstName: "Charles", lastName: "Webb", age: 45, hasDriverLicense: true, isAmerican: true),
Person(firstName: "Alex", lastName: "Zunino", age: 34, hasDriverLicense: true, isAmerican: true),
Person(firstName: "Webb", lastName: "Elexson", age: 30, hasDriverLicense: true, isAmerican: true)]
返回第一个匹配谓词的元素。
contacts.first(where: \.lastName == "Webb")
Optional(Person(firstName: "Charlie", lastName: "Webb", age: 10, hasDriverLicense: false, isAmerican: true))
按属性的相等性对值进行分组。
contacts.groupBy(\.lastName)
["Alexson": [Person(firstName: "Alex", lastName: "Alexson", age: 8, hasDriverLicense: false, isAmerican: true)],
"Webb": [Person(firstName: "Charlie", lastName: "Webb", age: 10, hasDriverLicense: false, isAmerican: true), Person(firstName: "Charles", lastName: "Webb", age: 45, hasDriverLicense: true, isAmerican: true), Person(firstName: "John", lastName: "Webb", age: 28, hasDriverLicense: true, isAmerican: true)],
"Elexson": [Person(firstName: "Alex", lastName: "Elexson", age: 22, hasDriverLicense: false, isAmerican: true), Person(firstName: "Webb", lastName: "Elexson", age: 30, hasDriverLicense: true, isAmerican: true)],
"Zunino": [Person(firstName: "Alex", lastName: "Zunino", age: 34, hasDriverLicense: true, isAmerican: true)]]
通过各自属性的相等性,将两个序列的值在元组中连接起来。
contacts.join(\.firstName, with: contacts, on: \.lastName)
// or
contacts.join(with: contacts, where: \.firstName == \.lastName)
[(Person(firstName: "Webb", lastName: "Elexson", age: 30, hasDriverLicense: true, isAmerican: true), Person(firstName: "Charlie", lastName: "Webb", age: 10, hasDriverLicense: false, isAmerican: true)),
(Person(firstName: "Webb", lastName: "Elexson", age: 30, hasDriverLicense: true, isAmerican: true), Person(firstName: "Charles", lastName: "Webb", age: 45, hasDriverLicense: true, isAmerican: true)),
(Person(firstName: "Webb", lastName: "Elexson", age: 30, hasDriverLicense: true, isAmerican: true), Person(firstName: "John", lastName: "Webb", age: 28, hasDriverLicense: true, isAmerican: true))]
也支持在多个属性上连接
contacts.join(with: contacts, .where(\.firstName, equals: \.lastName), .where(\.hasDriverLicense, equals: \.isAmerican))
// or
contacts.join(with: contacts, where: \.firstName == \.lastName, \.hasDriverLicense == \.isAmerican)
将元素映射到其属性的值。
contacts.map(\.lastName)
["Webb", "Elexson", "Webb", "Zunino", "Alexson", "Webb", "Elexson"]
将属性序列映射到一个函数。例如,这对于将属性子集提取到结构化类型中非常有用。
struct ContactCellModel {
let firstName: String
let lastName: String
}
contacts.map(\.lastName, \.firstName, to: ContactCellModel.init)
[ContactCellModel(firstName: "Webb", lastName: "Charlie"),
ContactCellModel(firstName: "Elexson", lastName: "Alex"),
ContactCellModel(firstName: "Webb", lastName: "Charles"),
ContactCellModel(firstName: "Zunino", lastName: "Alex"),
ContactCellModel(firstName: "Alexson", lastName: "Alex"),
ContactCellModel(firstName: "Webb", lastName: "John"),
ContactCellModel(firstName: "Elexson", lastName: "Webb")]
返回 Comparable
属性值最大的元素。
contacts.max(by: \.age)
contacts.max(\.age)
Optional(Person(firstName: "Charles", lastName: "Webb", age: 45, hasDriverLicense: true, isAmerican: true))
Optional(45)
返回 Comparable
属性值最小的元素。
contacts.min(by: \.age)
contacts.min(\.age)
Optional(Person(firstName: "Alex", lastName: "Alexson", age: 8, hasDriverLicense: false, isAmerican: true))
Optional(8)
对 Bool
类型的属性执行布尔 OR 运算。
contacts.or(\.hasDriverLicense)
true
允许在 switch
语句中使用谓词
switch person {
case \.firstName == "Charlie":
print("I'm Charlie!")
fallthrough
case \.age < 18:
print("I'm not an adult...")
fallthrough
default:
break
}
返回一个子序列,其中包含初始的、连续的元素,这些元素的 Bool
类型属性或谓词评估为 true。
contacts.prefix(while: \.age < 40)
[Person(firstName: "Charlie", lastName: "Webb", age: 10, hasDriverLicense: false, isAmerican: true),
Person(firstName: "Alex", lastName: "Elexson", age: 22, hasDriverLicense: false, isAmerican: true)]
计算数值属性的值的总和。
contacts.sum(of: \.age)
177
根据 Comparable
属性对元素进行排序。
contacts.sorted(by: \.age)
[Person(firstName: "Alex", lastName: "Alexson", age: 8, hasDriverLicense: false, isAmerican: true),
Person(firstName: "Charlie", lastName: "Webb", age: 10, hasDriverLicense: false, isAmerican: true),
Person(firstName: "Alex", lastName: "Elexson", age: 22, hasDriverLicense: false, isAmerican: true), Person(firstName: "John", lastName: "Webb", age: 28, hasDriverLicense: true, isAmerican: true),
Person(firstName: "Webb", lastName: "Elexson", age: 30, hasDriverLicense: true, isAmerican: true),
Person(firstName: "Alex", lastName: "Zunino", age: 34, hasDriverLicense: true, isAmerican: true),
Person(firstName: "Charles", lastName: "Webb", age: 45, hasDriverLicense: true, isAmerican: true)]
也可以指定排序顺序,按多个条件排序,或两者都做。
contacts.sorted(by: .ascending(\.lastName), .descending(\.age))
[Person(firstName: "Alex", lastName: "Alexson", age: 8, hasDriverLicense: false, isAmerican: true),
Person(firstName: "Webb", lastName: "Elexson", age: 30, hasDriverLicense: true, isAmerican: true),
Person(firstName: "Alex", lastName: "Elexson", age: 22, hasDriverLicense: false, isAmerican: true),
Person(firstName: "Charles", lastName: "Webb", age: 45, hasDriverLicense: true, isAmerican: true),
Person(firstName: "John", lastName: "Webb", age: 28, hasDriverLicense: true, isAmerican: true),
Person(firstName: "Charlie", lastName: "Webb", age: 10, hasDriverLicense: false, isAmerican: true),
Person(firstName: "Alex", lastName: "Zunino", age: 34, hasDriverLicense: true, isAmerican: true)]
非常感谢 Jérôme Alves (elegantswift.com) 提出了正确的模型,以允许对具有异构类型的多个属性进行排序。