KeyPathKit
上下文
Swift 4 引入了一个名为 KeyPath
的新类型,它通过非常简洁的语法允许访问对象的属性。例如
let string = "Foo"
let keyPathForCount = \String.count
let count = string[keyPath: keyPathForCount] // count == 3
语法可以非常简洁,因为它支持类型推断和属性链运算。
KeyPathKit 的目的
因此,我认为利用这个新概念来构建一个允许非常声明性方式进行数据操作的 API 是一件很棒的事情。
SQL 是一种非常适合这种操作的优秀语言,因此我从中吸取了灵感,并在 Swift 4 中使用 KeyPath
实现了其大多数标准操作符。
但是,KeyPathKit 的真正亮点是其巧妙的语法,它允许非常流畅地表达查询。例如
contacts.filter(where: \.lastName == "Webb" && \.age < 40)
安装
CocoaPods
将以下内容添加到您的 Podfile
pod "KeyPathKit"
Carthage
将以下内容添加到您的 Cartfile
github "vincent-pradeilles/KeyPathKit"
Swift 包管理器
创建一个文件 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
average
计算数值属性的均值。
contacts.average(of: \.age).rounded()
25
between
过滤掉属性值不在指定范围内的元素。
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)]
contains
返回序列是否包含一个元素,该元素的指定布尔属性或谓词为真。
contacts.contains(where: \.hasDriverLicense)
contacts.contains(where: \.lastName.count > 10)
true
false
distinct
返回属性的所有不同值。
contacts.distinct(\.lastName)
["Webb", "Elexson", "Zunino", "Alexson"]
drop
返回一个子序列,通过跳过类型为 Bool
的属性或谓词为真的元素,然后返回剩余元素。
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)]
filter
过滤掉值为 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)]
过滤器也与谓词一起使用
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)]
filterIn
过滤出那些具有给定Sequence
值的Equatable
属性值的元素。
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)]
filterLess
过滤出那些具有大于一个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)]
filterLike
过滤出那些字符串属性值不匹配正则表达式的元素。
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)]
filterMore
过滤出那些具有小于一个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)]
first
返回匹配谓词的第一个元素。
contacts.first(where: \.lastName == "Webb")
Optional(Person(firstName: "Charlie", lastName: "Webb", age: 10, hasDriverLicense: false, isAmerican: true))
groupBy
根据属性上的等价性对数值进行分组。
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)]]
join
通过属性上的等价性将两个序列的值作为元组连接起来。
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)
map
将元素映射到其属性的值。
contacts.map(\.lastName)
["Webb", "Elexson", "Webb", "Zunino", "Alexson", "Webb", "Elexson"]
mapTo
将一系列属性映射到一个函数。这通常用于从结构化类型中提取属性子集。
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")]
max
返回具有最大值的元素,该值对应于Comparable
属性。
contacts.max(by: \.age)
contacts.max(\.age)
Optional(Person(firstName: "Charles", lastName: "Webb", age: 45, hasDriverLicense: true, isAmerican: true))
Optional(45)
min
返回最小值对应的元素,该值对应于Comparable
属性。
contacts.min(by: \.age)
contacts.min(\.age)
Optional(Person(firstName: "Alex", lastName: "Alexson", age: 8, hasDriverLicense: false, isAmerican: true))
Optional(8)
or
对类型为Bool
的属性执行布尔或操作。
contacts.or(\.hasDriverLicense)
true
patternMatching
允许在switch
语句中使用谓词。
switch person {
case \.firstName == "Charlie":
print("I'm Charlie!")
fallthrough
case \.age < 18:
print("I'm not an adult...")
fallthrough
default:
break
}
prefix
返回一个子序列,该子序列包含初始连续元素,其属性类型为Bool
或谓词评估为真。
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) 提出了正确的模型化方案,允许对异构类型进行多属性排序。