首页 > 解决方案 > 类型擦除用于更复杂的协议

问题描述

我正在构建一个声明性和基于类型的过滤器模型。我被困在一个属性中存储活动过滤器的状态,因为我的协议有关联的类型。``

我听说过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以查看我的实现的外观以及示例模型。


问题

  1. 如何存储包含struct实例的数组,符合不同的Filter协议?

  2. 以及如何存储仅包含结构类型的静态数组,以便访问静态属性?

标签: swiftgenericsswift-protocolstype-erasure

解决方案


有趣的是,我在今年早些时候为一个商业项目建造了一些与此不同的东西。以一般的方式去做是很有挑战性的,但大多数问题都来自于逆向思维。“以终为始。”

// 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)]

推荐阅读