swift - 类型擦除用于更复杂的协议
问题描述
我正在构建一个声明性和基于类型的过滤器模型。我被困在一个属性中存储活动过滤器的状态,因为我的协议有关联的类型。``
我听说过Type Erasure,但是我发现的所有示例都使用了超级简单的示例,并且不知何故我无法将其映射到我的用例。
这是我的协议:
protocol Filter {
// The Type to be filtered (`MyModel`)
associatedtype ParentType
// The type of the property to be filtered (e.g `Date`)
associatedtype InputType
// The type of the possible FilterOption (e.g. `DateFilterOption` or the same as the Input type for filtering in enums.)
associatedtype OptionType
// This should return a list of all possible filter options
static var allOptions: [OptionType] { get }
static var allowsMultipleSelection: Bool { get }
// the adopting object will be setting this.
var selectedOptions: [OptionType] { get set }
func isIncluded(_ item: InputType) -> Bool
// For getting reference to the specific property. I think Swift 4's keypaths could be working here too.
var filter: FilterClosure<ParentType> { get }
}
以及具有减少复制/粘贴代码扩展的子协议
protocol EquatableFilter: Filter where InputType: Equatable, OptionType == InputType {}
extension EquatableFilter {
var allowsMultipleSelection: Bool { return true }
func isIncluded(_ item: InputType) -> Bool {
if selectedOptions.count == 0 { return true }
return selectedOptions.contains(item)
}
}
// Another specific filter. See gist file for extension.
protocol DateFilter: Filter where InputType == Date, OptionType == DateFilterOption {}
有关更多代码,请参阅我的 gist以查看我的实现的外观以及示例模型。
问题
如何存储包含
struct
实例的数组,符合不同的Filter
协议?以及如何存储仅包含结构类型的静态数组,以便访问静态属性?
解决方案
有趣的是,我在今年早些时候为一个商业项目建造了一些与此不同的东西。以一般的方式去做是很有挑战性的,但大多数问题都来自于逆向思维。“以终为始。”
// I want to be able to filter a sequence like this:
let newArray = myArray.filteredBy([
MyModel.Filters.DueDateFilter(selectedOptions: [.in24hours(past: false)]),
MyModel.Filters.StatusFilter(selectedOptions: [.a, .b])
])
这部分非常简单。它甚至不需要filteredBy
. 只需添加.filter
到每个元素:
let newArray = myArray
.filter(MyModel.Filters.DueDateFilter(selectedOptions: [.in24hours(past: false)]).filter)
.filter(MyModel.Filters.StatusFilter(selectedOptions: [.a, .b]).filter)
如果你愿意,你可以用这种方式编写过滤器,并做同样的事情:
func filteredBy(_ filters: [(Element) -> Bool]) -> [Element] {...}
关键是Filter
这里并不是真正的“过滤器”。它是对过滤器的描述,还有很多关于 UI 的其他内容(我们稍后会详细讨论)。要实际过滤,您只需要(Element) -> Bool
.
我们在这里真正想要的是一种方法来构建一个([Element]) -> Element
具有良好、富有表现力的语法。在函数式语言中,这非常简单,因为我们会有部分应用程序和函数式组合之类的东西。但是 Swift 并不真正喜欢做这些事情,所以为了让它更漂亮,让我们构建一些结构。
struct Filter<Element> {
let isIncluded: (Element) -> Bool
}
struct Map<Input, Output> {
let transform: (Input) -> Output
}
我们需要一种方法来开始,所以让我们使用身份映射
extension Map where Input == Output {
init(on: Input.Type) { transform = { $0 }}
}
我们需要一种方法来考虑 keyPaths
extension Map {
func keyPath<ChildOutput>(_ keyPath: KeyPath<Input, ChildOutput>) -> Map<Input, ChildOutput> {
return Map<Input, ChildOutput>(transform: { $0[keyPath: keyPath] })
}
}
最后我们要创建一个实际的过滤器
extension Map {
func inRange<RE: RangeExpression>(_ range: RE) -> Filter<Input> where RE.Bound == Output {
let transform = self.transform
return Filter(isIncluded: { range.contains(transform($0)) })
}
}
为“过去 24 小时”添加助手
extension Range where Bound == Date {
static var last24Hours: Range<Date> { return Date(timeIntervalSinceNow: -24*60*60)..<Date() }
}
现在我们可以构建一个过滤器,如下所示:
let filters = [Map(on: MyModel.self).keyPath(\.dueDate).inRange(Range.last24Hours)]
filters
是 type Filter<MyModel>
,所以任何其他过滤的东西MyModel
在这里都是合法的。调整你的filteredBy
:
extension Sequence {
func filteredBy(_ filters: [Filter<Element>]) -> [Element] {
return filter{ element in filters.allSatisfy{ $0.isIncluded(element) } }
}
}
好的,这就是过滤步骤。但是您的问题也基本上是“UI 配置”,为此您想要捕获比这更多的元素。
但是您的示例用法不会让您到达那里:
// Also I want to be able to save the state of all filters like this
var activeFilters: [AnyFilter] = [ // ???
MyModel.Filters.DueDateFilter(selectedOptions: [.in24hours(past: false)]),
MyModel.Filters.StatusFilter(selectedOptions: [.a, .b])
]
如何转换AnyFilter
成 UI 元素?您的过滤器协议实际上允许任何选项类型。OutputStream
如果选项类型是or ,您将如何显示 UI DispatchQueue
?您创建的类型不能解决问题。
这是解决问题的一种方法。创建一个 FilterComponent 结构,该结构定义所需的 UI 元素并提供构造过滤器的方法。
struct FilterComponent<Model> {
let optionTitles: [String]
let allowsMultipleSelection: Bool
var selectedOptions: IndexSet
let makeFilter: (IndexSet) -> Filter<Model>
}
然后要创建一个日期过滤器组件,我们需要一些日期选项。
enum DateOptions: String, CaseIterable {
case inPast24hours = "In the past 24 hours"
case inNext24hours = "In the next 24 hours"
var dateRange: Range<Date> {
switch self {
case .inPast24hours: return Date(timeIntervalSinceNow: -24*60*60)..<Date()
case .inNext24hours: return Date()..<Date(timeIntervalSinceNow: -24*60*60)
}
}
}
然后我们想要一种方法来创建这样一个正确的组件makeFilter
:
extension FilterComponent {
static func byDate(ofField keyPath: KeyPath<Model, Date>) -> FilterComponent<Model> {
return FilterComponent(optionTitles: DateOptions.allCases.map{ $0.rawValue },
allowsMultipleSelection: false,
selectedOptions: [],
makeFilter: { indexSet in
guard let index = indexSet.first else {
return Filter<Model> { _ in true }
}
let range = DateOptions.allCases[index].dateRange
return Map(on: Model.self).keyPath(keyPath).inRange(range)
})
}
}
有了这一切,我们可以创建类型的组件FilterComponent<MyModel>
。Date
不必公开内部类型(如)。不需要协议。
let components = [FilterComponent.byDate(ofField: \MyModel.dueDate)]
推荐阅读
- python - How do I make a function for cancel button which when button is clicked unchecks all buttons that I have checked?
- python - 从不同长度的主题响应对创建数据帧
- python - Heroku部署后Django(djongo)无法连接到MondoDB Atlas
- javascript - xmlhttprequest 并将内容放入 iframe
- docker - 由于 docker 容器未运行,代理失败时设置 nginx 回退
- r - 在数据子集化时,没有起始估计是 coxme 的成功错误
- php - Laravel:我在 html 上得到了另一种形式
- c# - 如何在 MVVM AvalonDock 程序中使用 ApplicationCommands?
- wireshark - 如何使用 Wireshark 在 SSH 远程服务器中本地捕获 MQTT 数据?
- python - optuna.integration.lightGBM 自定义优化指标