如何有条件地在协议扩展方法实现之间切换?

How to conditionally switch between protocol extension method implementations?

深入研究函数式编程和 swift 总的来说,我对多种做事方式感到不知所措。在这种情况下,我希望 struct 采用 Comparable 但可以有条件地切换重载运算符中正在使用的属性。

假设我有以下内容,一个快速排序(来自 Niv Yahel 的 Wenderlich FP 教程),扩展了任何可比较的数组,这将很容易容纳我的 Collection of Students

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 结构将按名称、成绩或年龄排序,最好不必触及这个漂亮的快速排序功能。

  1. 是否应该将快速排序转换为泛型函数?
  2. 我应该查看类型限制吗?
  3. 我是否应该在 Student 中有一个 属性 枚举,它应该根据 属性 进行排序?长得丑。
  4. 我应该有一个类似于 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()

最后一个非常糟糕且容易出错,因为它会影响可能不打算对其进行排序的结构上的其他比较操作(特别是对于 == 运算符)。它也不是线程安全的。

几个想法:

  1. 首先,Student不应该是Comparable。这不是必需的,而且在概念上令人困惑。

  2. 正如 Alain 指出的那样,sorted 方法通过提供 block-based 格式来支持本质上不是 Comparable 的数组排序,sorted(by:):

    let sortedStudents = students.sorted { [=10=].age < .age }
    
  3. 您可以在 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?" 的理论问题。如果是这样的话,这种闭包模式是处理这些情况的好方法。

  4. 如果您绝对想要一个接受排序字段参数的排序方法,在 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 方法,但如果你真的觉得有必要传递一个 属性,你可能会这样做。