如何有条件地在协议扩展方法实现之间切换?
How to conditionally switch between protocol extension method implementations?
深入研究函数式编程和 swift 总的来说,我对多种做事方式感到不知所措。在这种情况下,我希望 struct
采用 Comparable
但可以有条件地切换重载运算符中正在使用的属性。
假设我有以下内容,一个快速排序(来自 Niv Yahel 的 Wenderlich FP 教程),扩展了任何可比较的数组,这将很容易容纳我的 Collection
of Student
s
struct Student {
let name: String
let age: Int
let grades: Double
}
extension Student: Comparable {
static func <(lhs: Student, rhs: Student) -> Bool {
return lhs.grades < rhs.grades
}
static func ==(lhs: Student, rhs: Student) -> Bool {
return lhs.grades == rhs.grades
}
}
extension Array where Element: Comparable {
func quickSorted() -> [Element] {
if self.count > 1 {
let (pivot, remaining) = (self[0], dropFirst())
let lhs = remaining.filter{ [=10=] <= pivot }
let rhs = remaining.filter{ [=10=] > pivot }
return lhs.quickSorted() as [Element] + pivot + rhs.quickSorted()
}
return self
}
}
}
//Omitted, create a bunch of Students
//let bingoLittle = Student(name: "BingoLittle", age: 23, grades: 93.4)
let myStudentDirectory = [bingoLittle, studentB, ... StudentN]
let sortedStudentDirectory = myStudentDirectory.quickSorted()
但是,下一步我想要的是即时决定 property
结构将按名称、成绩或年龄排序,最好不必触及这个漂亮的快速排序功能。
- 是否应该将快速排序转换为泛型函数?
- 我应该查看类型限制吗?
- 我是否应该在 Student 中有一个 属性 枚举,它应该根据 属性 进行排序?长得丑。
- 我应该有一个类似于
quickSorted(by: .name)
的快速排序吗?它似乎不再适用于数组扩展。
有几种方法可以解决这个问题:
1) 使用本机排序函数,它允许您为比较指定一个闭包,从而提供更大的灵活性,并且不需要您的结构是可比较的:
let sortedStudentDirectory = myStudentDirectory.sorted{ [=10=].grade < .grade }
//
// This would be my recommendation given that it is standard and
// it is unlikely that the quicksorted() method would outperform it.
//
2) 修改 quicksorted() 函数以让它与闭包一起工作:
extension Array
{
func quickSorted<T:Comparable>(_ property:@escaping (Element)->T) -> [Element]
{
guard self.count > 1 else { return self }
let (pivot, remaining) = (property(self[0]), dropFirst())
let lhs = remaining.filter{ property([=11=]) <= pivot }
let rhs = remaining.filter{ property([=11=]) > pivot }
return lhs.quickSorted(property) as [Element] + self[0] + rhs.quickSorted(property)
}
}
let sortedStudentDirectory = myStudentDirectory.quickSorted{[=11=].grades}
// this one also avoids making the struct Comparable.
// you could implement it like the standard sort with a comparison
// closure instead of merely a property accessor so that descending
// sort order and multi-field sorting can be supported.
.
3) 向您的 Student 结构添加一个静态变量,以告诉您的比较运算符要使用哪个字段,并在使用 quicksorted() 函数之前设置该静态变量
struct Student
{
enum SortOrder { case name, age, grades }
static var sortOrder = .grades
let name: String
let age: Int
let grades: Double
}
extension Student: Comparable
{
static func <(lhs: Student, rhs: Student) -> Bool
{
switch Student.sortOrder
{
case .grades : return lhs.grades < rhs.grades
case .age : return lhs.age < rhs.age
default : return lhs.name < rhs.name
}
}
static func ==(lhs: Student, rhs: Student) -> Bool
{
switch Student.sortOrder
{
case .grades : return lhs.grades == rhs.grades
case .age : return lhs.age == rhs.age
default : return lhs.name == rhs.name
}
}
}
Student.sortOrder = .grades
let sortedStudentDirectory = myStudentDirectory.quickSorted()
最后一个非常糟糕且容易出错,因为它会影响可能不打算对其进行排序的结构上的其他比较操作(特别是对于 == 运算符)。它也不是线程安全的。
几个想法:
首先,Student
不应该是Comparable
。这不是必需的,而且在概念上令人困惑。
正如 Alain 指出的那样,sorted
方法通过提供 block-based 格式来支持本质上不是 Comparable
的数组排序,sorted(by:)
:
let sortedStudents = students.sorted { [=10=].age < .age }
您可以在 quickSorted
方法中使用完全相同的模式:
extension Array {
func quickSorted(by areInIncreasingOrder: (Element, Element) -> Bool) -> [Element] {
guard count > 1 else { return self }
let (pivot, remaining) = (self[0], dropFirst())
let (lhs, rhs) = remaining.reduce(into: ([Element](), [Element]())) { result, element in
if areInIncreasingOrder(element, pivot) {
result.0.append(element)
} else {
result.1.append(element)
}
}
return lhs.quickSorted(by: areInIncreasingOrder) as [Element] + [pivot] + rhs.quickSorted(by: areInIncreasingOrder)
}
}
然后你可以做:
let sortedStudents = students.quickSorted { [=12=].age < .age }
此处与您的问题无关,但您方法中的两个 filter
调用似乎效率低下,因此我将其替换为单个 reduce
。 (a) 避免遍历每个数组两次; (b) 如果比较算法很复杂,这可能会特别成问题。
话虽如此,sorted(by:)
中的构建比 quickSorted(by:)
快得多,因此您可能会坚持使用它。
但我认为这更多是 "how do I ...?" 而不是 "what's the best way to sort?" 的理论问题。如果是这样的话,这种闭包模式是处理这些情况的好方法。
如果您绝对想要一个接受排序字段参数的排序方法,在 Swift 4 中,您可以使用带有 KeyPath
的通用函数:
extension Array {
func quickSorted<T: Comparable>(on keyPath: KeyPath<Element, T>) -> [Element] {
guard count > 1 else { return self }
let (pivot, remaining) = (self[0], dropFirst())
let (lhs, rhs) = remaining.reduce(into: ([Element](), [Element]())) { result, element in
if element[keyPath: keyPath] < pivot[keyPath: keyPath] {
result.0.append(element)
} else {
result.1.append(element)
}
}
return lhs.quickSorted(on: keyPath) as [Element] + [pivot] + rhs.quickSorted(on: keyPath)
}
}
然后你可以做:
let sortedStudents = students.quickSorted(on: \.grades)
就我个人而言,我会坚持闭包模式:它更快更灵活(例如,您可以按降序排序,您可以进行复杂的比较来比较多个属性,例如按年龄然后按姓名等)而不是 keypath 方法,但如果你真的觉得有必要传递一个 属性,你可能会这样做。
深入研究函数式编程和 swift 总的来说,我对多种做事方式感到不知所措。在这种情况下,我希望 struct
采用 Comparable
但可以有条件地切换重载运算符中正在使用的属性。
假设我有以下内容,一个快速排序(来自 Niv Yahel 的 Wenderlich FP 教程),扩展了任何可比较的数组,这将很容易容纳我的 Collection
of Student
s
struct Student {
let name: String
let age: Int
let grades: Double
}
extension Student: Comparable {
static func <(lhs: Student, rhs: Student) -> Bool {
return lhs.grades < rhs.grades
}
static func ==(lhs: Student, rhs: Student) -> Bool {
return lhs.grades == rhs.grades
}
}
extension Array where Element: Comparable {
func quickSorted() -> [Element] {
if self.count > 1 {
let (pivot, remaining) = (self[0], dropFirst())
let lhs = remaining.filter{ [=10=] <= pivot }
let rhs = remaining.filter{ [=10=] > pivot }
return lhs.quickSorted() as [Element] + pivot + rhs.quickSorted()
}
return self
}
}
}
//Omitted, create a bunch of Students
//let bingoLittle = Student(name: "BingoLittle", age: 23, grades: 93.4)
let myStudentDirectory = [bingoLittle, studentB, ... StudentN]
let sortedStudentDirectory = myStudentDirectory.quickSorted()
但是,下一步我想要的是即时决定 property
结构将按名称、成绩或年龄排序,最好不必触及这个漂亮的快速排序功能。
- 是否应该将快速排序转换为泛型函数?
- 我应该查看类型限制吗?
- 我是否应该在 Student 中有一个 属性 枚举,它应该根据 属性 进行排序?长得丑。
- 我应该有一个类似于
quickSorted(by: .name)
的快速排序吗?它似乎不再适用于数组扩展。
有几种方法可以解决这个问题:
1) 使用本机排序函数,它允许您为比较指定一个闭包,从而提供更大的灵活性,并且不需要您的结构是可比较的:
let sortedStudentDirectory = myStudentDirectory.sorted{ [=10=].grade < .grade }
//
// This would be my recommendation given that it is standard and
// it is unlikely that the quicksorted() method would outperform it.
//
2) 修改 quicksorted() 函数以让它与闭包一起工作:
extension Array
{
func quickSorted<T:Comparable>(_ property:@escaping (Element)->T) -> [Element]
{
guard self.count > 1 else { return self }
let (pivot, remaining) = (property(self[0]), dropFirst())
let lhs = remaining.filter{ property([=11=]) <= pivot }
let rhs = remaining.filter{ property([=11=]) > pivot }
return lhs.quickSorted(property) as [Element] + self[0] + rhs.quickSorted(property)
}
}
let sortedStudentDirectory = myStudentDirectory.quickSorted{[=11=].grades}
// this one also avoids making the struct Comparable.
// you could implement it like the standard sort with a comparison
// closure instead of merely a property accessor so that descending
// sort order and multi-field sorting can be supported.
.
3) 向您的 Student 结构添加一个静态变量,以告诉您的比较运算符要使用哪个字段,并在使用 quicksorted() 函数之前设置该静态变量
struct Student
{
enum SortOrder { case name, age, grades }
static var sortOrder = .grades
let name: String
let age: Int
let grades: Double
}
extension Student: Comparable
{
static func <(lhs: Student, rhs: Student) -> Bool
{
switch Student.sortOrder
{
case .grades : return lhs.grades < rhs.grades
case .age : return lhs.age < rhs.age
default : return lhs.name < rhs.name
}
}
static func ==(lhs: Student, rhs: Student) -> Bool
{
switch Student.sortOrder
{
case .grades : return lhs.grades == rhs.grades
case .age : return lhs.age == rhs.age
default : return lhs.name == rhs.name
}
}
}
Student.sortOrder = .grades
let sortedStudentDirectory = myStudentDirectory.quickSorted()
最后一个非常糟糕且容易出错,因为它会影响可能不打算对其进行排序的结构上的其他比较操作(特别是对于 == 运算符)。它也不是线程安全的。
几个想法:
首先,
Student
不应该是Comparable
。这不是必需的,而且在概念上令人困惑。正如 Alain 指出的那样,
sorted
方法通过提供 block-based 格式来支持本质上不是Comparable
的数组排序,sorted(by:)
:let sortedStudents = students.sorted { [=10=].age < .age }
您可以在
quickSorted
方法中使用完全相同的模式:extension Array { func quickSorted(by areInIncreasingOrder: (Element, Element) -> Bool) -> [Element] { guard count > 1 else { return self } let (pivot, remaining) = (self[0], dropFirst()) let (lhs, rhs) = remaining.reduce(into: ([Element](), [Element]())) { result, element in if areInIncreasingOrder(element, pivot) { result.0.append(element) } else { result.1.append(element) } } return lhs.quickSorted(by: areInIncreasingOrder) as [Element] + [pivot] + rhs.quickSorted(by: areInIncreasingOrder) } }
然后你可以做:
let sortedStudents = students.quickSorted { [=12=].age < .age }
此处与您的问题无关,但您方法中的两个
filter
调用似乎效率低下,因此我将其替换为单个reduce
。 (a) 避免遍历每个数组两次; (b) 如果比较算法很复杂,这可能会特别成问题。话虽如此,
sorted(by:)
中的构建比quickSorted(by:)
快得多,因此您可能会坚持使用它。但我认为这更多是 "how do I ...?" 而不是 "what's the best way to sort?" 的理论问题。如果是这样的话,这种闭包模式是处理这些情况的好方法。
如果您绝对想要一个接受排序字段参数的排序方法,在 Swift 4 中,您可以使用带有
KeyPath
的通用函数:extension Array { func quickSorted<T: Comparable>(on keyPath: KeyPath<Element, T>) -> [Element] { guard count > 1 else { return self } let (pivot, remaining) = (self[0], dropFirst()) let (lhs, rhs) = remaining.reduce(into: ([Element](), [Element]())) { result, element in if element[keyPath: keyPath] < pivot[keyPath: keyPath] { result.0.append(element) } else { result.1.append(element) } } return lhs.quickSorted(on: keyPath) as [Element] + [pivot] + rhs.quickSorted(on: keyPath) } }
然后你可以做:
let sortedStudents = students.quickSorted(on: \.grades)
就我个人而言,我会坚持闭包模式:它更快更灵活(例如,您可以按降序排序,您可以进行复杂的比较来比较多个属性,例如按年龄然后按姓名等)而不是 keypath 方法,但如果你真的觉得有必要传递一个 属性,你可能会这样做。