为更复杂的协议键入擦除
Type Erasure for more complex protocols
我正在构建声明式和基于类型的过滤器模型。我无法将活动过滤器的状态存储在 属性 中,因为我的协议具有关联类型。``
我听说过 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 }
}
以及具有减少copy/paste-code
扩展的子协议
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 {}
如需更多代码,please see my gist可通过示例模型查看我的实现方式。
问题
如何存储包含 struct
个实例的数组,符合
不同的 Filter
协议?
我如何存储一个只包含结构类型的静态数组,以便我可以访问静态属性?
有趣的是,今年早些时候我为一个商业项目构建了一些与此不同的东西。笼统地去做是有挑战性的,但是大部分问题都来自于逆向思考。 "Start with the end in mind."
// 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
这里并不是真正的 "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 = { [=14=] }}
}
我们需要一种方法来考虑 keyPaths
extension Map {
func keyPath<ChildOutput>(_ keyPath: KeyPath<Input, ChildOutput>) -> Map<Input, ChildOutput> {
return Map<Input, ChildOutput>(transform: { [=15=][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([=16=])) })
}
}
为 "last 24 hours"
添加助手
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
属于 Filter<MyModel>
类型,因此过滤 MyModel
的任何其他内容在这里都是合法的。调整你的 filteredBy
:
extension Sequence {
func filteredBy(_ filters: [Filter<Element>]) -> [Element] {
return filter{ element in filters.allSatisfy{ [=19=].isIncluded(element) } }
}
}
好的,这就是过滤步骤。但是你的问题基本上也是 "UI configuration" 并且为此你想要捕获比这更多的元素。
但是您的示例用法不会让您到达那里:
// 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 元素?您的过滤器协议允许 any 选项类型。如果选项类型是 OutputStream
或 DispatchQueue
,您将如何显示 UI?您创建的类型没有解决问题。
这是一种解决方法。创建一个定义所需 UI 元素并提供构建过滤器的方法的 FilterComponent 结构。
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{ [=23=].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)]
我正在构建声明式和基于类型的过滤器模型。我无法将活动过滤器的状态存储在 属性 中,因为我的协议具有关联类型。``
我听说过 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 }
}
以及具有减少copy/paste-code
扩展的子协议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 {}
如需更多代码,please see my gist可通过示例模型查看我的实现方式。
问题
如何存储包含
struct
个实例的数组,符合 不同的Filter
协议?我如何存储一个只包含结构类型的静态数组,以便我可以访问静态属性?
有趣的是,今年早些时候我为一个商业项目构建了一些与此不同的东西。笼统地去做是有挑战性的,但是大部分问题都来自于逆向思考。 "Start with the end in mind."
// 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
这里并不是真正的 "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 = { [=14=] }}
}
我们需要一种方法来考虑 keyPaths
extension Map {
func keyPath<ChildOutput>(_ keyPath: KeyPath<Input, ChildOutput>) -> Map<Input, ChildOutput> {
return Map<Input, ChildOutput>(transform: { [=15=][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([=16=])) })
}
}
为 "last 24 hours"
添加助手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
属于 Filter<MyModel>
类型,因此过滤 MyModel
的任何其他内容在这里都是合法的。调整你的 filteredBy
:
extension Sequence {
func filteredBy(_ filters: [Filter<Element>]) -> [Element] {
return filter{ element in filters.allSatisfy{ [=19=].isIncluded(element) } }
}
}
好的,这就是过滤步骤。但是你的问题基本上也是 "UI configuration" 并且为此你想要捕获比这更多的元素。
但是您的示例用法不会让您到达那里:
// 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 元素?您的过滤器协议允许 any 选项类型。如果选项类型是 OutputStream
或 DispatchQueue
,您将如何显示 UI?您创建的类型没有解决问题。
这是一种解决方法。创建一个定义所需 UI 元素并提供构建过滤器的方法的 FilterComponent 结构。
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{ [=23=].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)]