使用 key paths 创建自定义查询函数

2021-07-01 10:20:26 浏览数 (1)

作为一个相当严格,静态编译的语言,Swift可能不会在语法自定义方面提供许多渠道,但这实际上确正好相反。通过自定义和重载运算符,key paths,函数/结果构建器 等功能,我们有很多机会为特定用例进行调整 Swift 的语法。

当然,无可争议的是,任何类型的语法定制都应小心谨慎地,因为如果我们不小心,非标准语法也可能很容易成为混乱的源泉。但是,在某些情况下,权衡可能是值得的,并且可以易于让我们制作类似 DSL 这种可以帮助我们使代码更清晰的语法。

否定布尔值的 key pahts

让我们查看一个这样的案例,说我们正在研究一个应用程序,用于管理,过滤和排序文章,其中包含以下Article数据模型:

代码语言:javascript复制
struct Article {
    var title: String
    var body: String
    var category: Category
    var isRead: Bool
    ...
}

现在让我们看一下我们的代码库中的一个非常常见的任务是过滤各种集合,每个集合包含上述模型的实例。这样做的一种方法是利用任何Swift key paths 表达式可以自动转换为函数的功能,这让我们在过滤任何布尔属性时, 可以使用如下在筛选isread时的凝练的语法:

代码语言:javascript复制
let articles: [Article] = ...
let readArticles = articles.filter(.isRead)

这真的是非常好,但是,只有在我们想要与true比较时才能使用以上语法 ——如果我们想创建包含所有未读文章的类似过滤的数组,那么我们必须使用闭包(或 传入一个函数)代替:

代码语言:javascript复制
let unreadArticles = articles.filter { !$0.isRead }

这肯定不是一个大问题,但如果上述操作是我们在代码上的许多不同地方上演的东西,那么我们可能会开始问自己:“如果我们也可以使用否定的布尔值的key paths语法会不会更好?“

这就是语法自定义的概念进来的地方。通过实现以下前缀函数,我们实际上可以创建一个小小的调整,这将让我们不用担心 truefalse 的使用key paths:

代码语言:javascript复制
prefix func !<T>(keyPath: KeyPath<T, Bool>) -> (T) -> Bool {
    return { !$0[keyPath: keyPath] }
}

以上基本上就是是重载内置的 前置操作符,让其可以应用于任何 Bool key paths,以便将其转换为否定(或翻转)其值的函数 ——现在我们可以计算我们的UnreadArticles数组了:

代码语言:javascript复制
let unreadArticles = articles.filter(!.isRead)
基于 key paths 的比较

现在,进一步采取措施,让我们也可以使用 key paths 来形成筛选器查询,该筛选器查询将给定属性与任何Equatable的值进行比较。例如,如果我们想要根据每篇文章的类别过滤我们的文章类别,那将变得有用。该属性,类别的类型目前被定义为如下所示的枚举:

代码语言:javascript复制
extension Article {
    enum Category {
        case fullLength
        case quickReads
        case basics
        ...
    }
}

就像我们之前重载的操作符一样,我们也可以用==运算符进行同样的事情,我们将返回一个返回Bool的闭包,然后可以直接传递给筛选器(如filter过滤器):

代码语言:javascript复制
func ==<T, V: Equatable>(lhs: KeyPath<T, V>, rhs: V) -> (T) -> Bool {
    return { $0[keyPath: lhs] == rhs }
}

通过以上重载,我们现在可以使用基于key paths 的比较轻松过滤任何集合,如下所示:

代码语言:javascript复制
let fullLengthArticles = articles.filter(.category == .fullLength)
结语

Swift让我们通过几个轻量级重载轻松创建上述功能的事实是非常棒的或令人难以置信的。我倾向于在中间的某个地方停下,认为我们确实可以让部分Swift的语法调整为适合我们的编写,但同时,我认为应该始终盯紧我们使diam更简单的目标来调整这些代码。

对于更彻底的,也更先进,更高级,更先进的技术,请查看“Swift 中的谓词”,并随时通过Twitter或电子邮件发送您的问题和评论。

译自注:补充几个其他相关扩展

代码语言:javascript复制
// 用于扩展类似 .filter(.sex != .ohter)
func !=<T, V: Equatable>(lhs: KeyPath<T, V>, rhs: V) -> (T) -> Bool {
    return { $0[keyPath: lhs] != rhs }
}

// 用于扩展类似 .filter(.age > 18)
func ><T, V: Comparable>(lhs: KeyPath<T, V>, rhs: V) -> (T) -> Bool {
    return { $0[keyPath: lhs] > rhs }
}

// 用于扩展类似 .filter(.age >= 18)
func >=<T, V: Comparable>(lhs: KeyPath<T, V>, rhs: V) -> (T) -> Bool {
    return { $0[keyPath: lhs] >= rhs }
}

// 用于扩展类似 .filter(.age < 100)
func <<T, V: Comparable>(lhs: KeyPath<T, V>, rhs: V) -> (T) -> Bool {
    return { $0[keyPath: lhs] < rhs }
}

// 用于扩展类似 .filter(.age < 100)
func <=<T, V: Comparable>(lhs: KeyPath<T, V>, rhs: V) -> (T) -> Bool {
    return { $0[keyPath: lhs] <= rhs }
}

Swift by Sundell

译自 John Sundell 的 Creating custom query functions using key paths

0 人点赞