使用Swift 4 KeyPath构建过滤器

最近,我与一个同事一起工作,该同事试图将过滤器应用于可观察的数据库对象流。 虽然建议的代码有效,但仅限于仅针对BoolString值进行过滤,并且它是Stringly类型的API,依靠String键映射到属性。

可行,但可能会更好

不幸的是,这不是很可扩展,也不能防止我们怪异的运行时崩溃

例如,假设我们运行一个更改属性名称或类型的数据库迁移,并且具有以下代码:

 过滤器{中的(filterKey,filterValue) 
守卫让值= object.getValue(for:filterKey)为? 整数
否则{fatalError(“无效的类型转换/无效的密钥”)}
}

字符串键入的密钥现在可能已过期并且将导致错误的值查找, 或者呼叫站点将使用错误的类型🤕。 在这种情况下,您可能会出现fatalErrorreturn nil的情况,但是无论哪种方式,这种行为都是不希望的。

因此,我们着手通过两个目标(底部的最终代码)来改善这一点:

  1. 使它通用
  2. 获得编译时安全

对于那些熟悉KVO或键值观察的人,应该立即浮现一个Stringly typed API的Stringly ,哈哈。 如果您不熟悉,则只需知道需要KVO(过去时,请参阅此处的新iOS 11版本)即可使用NSString/String键路径,这会在属性名称更改且您未更新所有的属性时导致运行时崩溃使用它的地方。

由于我们想要访问属性,因此我想看看Swift 4的新KeyPath API,它是通用的并且很棒。 KeyPaths是一种类型安全的方法,用于将引用类型的属性与评估属性分开。 这是一个例子:

在上面的代码中,我们创建对MyStruct.name的引用,并在第13行对其求值

如上所示,要引用属性,您只需要特殊字符\后跟属性引用即可。 您可以将KeyPath中的泛型视为KeyPath ,尽管它在形式上被称为KeyPath ,但是您可以理解。 然后,使用下标方法[keyPath: ]可以评估您的密钥路径所对应的属性。 有了这种类型的安全性,我们现在可以构建一个过滤器,该过滤器的初始值设定项将导致我们正在寻找的编译时安全性。

在进入代码之前,让我们考虑一下过滤器应该做什么。

与以前非常相似,过滤器应提供一种确定对象是否具有与某个期望值匹配的值的方法。 我们可能要求该值具有Equatable一致性,但这将导致限制性非常严格的API,因此我们只需要一个Matcher闭包即可。 让我们看一下脚手架代码:

过滤脚手架代码

初始化程序有两个通用参数类型: Object ,它是指将应用过滤器的对象, Type是我们将要评估的属性的类型。 使用Swift语言的类型推断,当我们为初始化程序提供KeyPath ,将能够推断出初始化程序的Type约束为PropertyType

为什么这么好❓

它将强制我们的matcher闭包接受一个匹配PropertyType的参数,这意味着如果该属性的类型由于任何原因(例如数据库迁移)而沿线向下移动,则编译器将捕获匹配matcherPropertyType的不matcher ,从而导致编译时间安全! 🙌🏾

我们现在进展顺利,但这仅仅是脚手架。 我们还需要能够在一个数组中存储多个过滤器。 这意味着matcher一旦存储在Filter ,就不能再引用Type否则我们必须在Filter声明中声明Type要求,例如Filter,如果我们要存储多个过滤器,那就太糟糕了具有不同的Type要求。 出于完全相同的原因,我们不能将keypPath存储为KeyPath 😐。

让我们首先解决密钥路径问题。

到目前为止,我只提到KeyPath ,但这只是KeyPath一种特定类型。 真正的类层次结构,每个都表示前者的子类,如下所示:

  AnyKeyPath 
-PartialKeyPath
-KeyPath
-WritableKeyPath
-ReferenceWritableKeyPath

这是每个的快速细分:

  • AnyKeyPath是完全类型的擦除密钥路径。
  • PartialKeyPath仅公开根,使其在数组中为同一根类型存储不同的键路径时很有用。 但是,对PartialKeyPath值将返回Any ,因此必须牢记。
  • 我们熟悉的KeyPath ,因此我将跳过这一部分。
  • WritableKeyPathKeyPath的子类,并在键路径引用mutable属性时生成。 *即使是let属性, structs属性也总是如此。 这是由于当前缺少运行时信息而导致无法区分letvar
  • 当属性位于引用类型(即class )上时,将生成子类WritableKeyPath ReferenceWritableKeyPath 。 这向编译器指示,由于它是class类型,因此在任何上下文中都可以写入该属性。

信息超载? 🚧

我知道要花很多PartialKeyPath ,但是幸运的是我们只需要KeyPathPartialKeyPath 。 由于KeyPathPartialKeyPath的子类, PartialKeyPath我们可以要求初始化程序接受KeyPath ,然后将其存储为PartialKeyPath

大! 🎉但是匹配器呢?

好吧,在匹配器的情况下,我们可以使用我喜欢称为闭包类型擦除的技术 。 我们将匹配器包装在一个闭包中,该闭包捕获Type但不公开它。 困惑? 让我们看看它的作用:

首先,我们的Filter现在可以存储keyPath而不暴露Type 。 其次,第8-11行显示了如何创建类型(Any) -> Bool的闭包,以包装我们的类型(Type) -> Bool matcher ,并将其传递给初始化程序。 类型信息是在包装闭包中捕获的,并且最重要的是,将matcher参数的类型保持为(Type) -> Bool确保我们的编译时安全。 我会说是个胜利🥇

我们还没有完成。 我不喜欢我们如何在Filter上显示matcher Filter尤其是对Any宽松要求。

那我们该怎么办? 🤔

我们知道, keyPath 肯定会映射到Object类型的值,并且传递给初始化程序的matcher能够处理该值。 这意味着,如果我们在内部使用PartialKeyPath ,我们就知道它可以由我们的匹配器评估-我们所需要的只是一个Object实例。

上一次更改我们的代码,我们得到了:

现在,在最终版本中,调用者无法将任何旧值传递给匹配器,从而使API更加简洁。 取而代之的是,它们必须传入一个Object实例,我们坚决保证keyPath可以对其进行评估。 最重要的是,我们的Filter是通用的,初始化器为我们提供了我们一直在寻找的编译时安全性! 🍾

分词与资源

有人可能会提出这样的观点,即过滤器可能只是一个闭包(Object) -> Bool ,您可以根据需要进行评估,这很好-也许那就是我们接下来要去的地方? 我碰巧喜欢我们最终使用的Filter的声明式样式,但是无论哪种方式,这都是研究新API并很好地使用它的好机会!

  • 查看Kevin Lundberg的博客,获取一些很棒的KeyPath信息。
  • 这是迅速发展的提议-包括KeyPath API的基本原理,如果您想了解Swift的变化,那就非常有趣。

好吧,这就是我今天要做的。 这是一个很长的游戏,但是如果您走得这么远,谢谢! 和往常一样,评论,鼓掌,订阅-如果那是您的事👍🏽

☮️❤️🐯&🐶