【问题标题】:Swift - Sort array of objects with multiple criteriaSwift - 使用多个条件对对象数组进行排序
【发布时间】:2016-10-02 21:43:24
【问题描述】:

我有一个Contact 对象数组:

var contacts:[Contact] = [Contact]()

联系人类:

Class Contact:NSOBject {
    var firstName:String!
    var lastName:String!
}

我想按lastName 对数组进行排序,然后按firstName 排序,以防某些联系人得到相同的lastName

我可以按其中一个标准进行排序,但不能同时按两者。

contacts.sortInPlace({$0.lastName < $1.lastName})

如何添加更多条件来对这个数组进行排序?

【问题讨论】:

  • 完全按照你刚才说的方法做!花括号内的代码应该说:“如果姓氏相同,则按名字排序;否则按姓氏排序”。
  • 我在这里看到一些代码异味:1) Contact 可能不应该从 NSObject 继承,2) Contact 可能应该是一个结构,以及 3) firstName 和 @ 987654332@ 可能不应该是隐式展开的选项。
  • @AMomchilov 没有理由建议 Contact 应该是一个结构,因为你不知道他的其余代码在使用它的实例时是否已经依赖于引用语义。
  • @PatrickGoley "...可能..."
  • @AMomchilov “可能”具有误导性,因为您对代码库的其余部分一无所知。如果将其更改为结构,则在更改 var 时会生成所有突然的副本,而不是修改手头的实例。这是行为的巨大变化,这样做“可能”会导致错误,因为不太可能所有内容都已针对引用 值语义正确编码。

标签: swift sorting


【解决方案1】:

使用元组做多个条件的比较

通过多个标准执行排序(即通过一个比较进行排序,如果相等,则通过另一个比较进行排序)的一种非常简单的方法是使用 元组,如 &lt; 和 @987654326 @ 运算符具有执行字典比较的重载。

/// Returns a Boolean value indicating whether the first tuple is ordered
/// before the second in a lexicographical ordering.
///
/// Given two tuples `(a1, a2, ..., aN)` and `(b1, b2, ..., bN)`, the first
/// tuple is before the second tuple if and only if
/// `a1 < b1` or (`a1 == b1` and
/// `(a2, ..., aN) < (b2, ..., bN)`).
public func < <A : Comparable, B : Comparable>(lhs: (A, B), rhs: (A, B)) -> Bool

例如:

struct Contact {
  var firstName: String
  var lastName: String
}

var contacts = [
  Contact(firstName: "Leonard", lastName: "Charleson"),
  Contact(firstName: "Michael", lastName: "Webb"),
  Contact(firstName: "Charles", lastName: "Alexson"),
  Contact(firstName: "Michael", lastName: "Elexson"),
  Contact(firstName: "Alex", lastName: "Elexson"),
]

contacts.sort {
  ($0.lastName, $0.firstName) <
    ($1.lastName, $1.firstName)
}

print(contacts)

// [
//   Contact(firstName: "Charles", lastName: "Alexson"),
//   Contact(firstName: "Leonard", lastName: "Charleson"),
//   Contact(firstName: "Alex", lastName: "Elexson"),
//   Contact(firstName: "Michael", lastName: "Elexson"),
//   Contact(firstName: "Michael", lastName: "Webb")
// ]

这将首先比较元素的lastName 属性。如果它们不相等,则排序顺序将基于 &lt; 与它们的比较。如果它们 相等,那么它将移动到元组中的下一对元素,即比较 firstName 属性。

标准库为 2 到 6 个元素的元组提供 &lt;&gt; 重载。

如果您想要不同属性的不同排序顺序,您可以简单地交换元组中的元素:

contacts.sort {
  ($1.lastName, $0.firstName) <
    ($0.lastName, $1.firstName)
}

// [
//   Contact(firstName: "Michael", lastName: "Webb")
//   Contact(firstName: "Alex", lastName: "Elexson"),
//   Contact(firstName: "Michael", lastName: "Elexson"),
//   Contact(firstName: "Leonard", lastName: "Charleson"),
//   Contact(firstName: "Charles", lastName: "Alexson"),
// ]

现在将按lastName 降序排序,然后firstName 升序。


定义一个采用多个谓词的sort(by:) 重载

Sorting Collections with map closures and SortDescriptors 讨论的启发,另一种选择是定义处理多个谓词的sort(by:)sorted(by:) 的自定义重载——其中每个谓词被依次考虑以决定元素的顺序。

extension MutableCollection where Self : RandomAccessCollection {
  mutating func sort(
    by firstPredicate: (Element, Element) -> Bool,
    _ secondPredicate: (Element, Element) -> Bool,
    _ otherPredicates: ((Element, Element) -> Bool)...
  ) {
    sort(by:) { lhs, rhs in
      if firstPredicate(lhs, rhs) { return true }
      if firstPredicate(rhs, lhs) { return false }
      if secondPredicate(lhs, rhs) { return true }
      if secondPredicate(rhs, lhs) { return false }
      for predicate in otherPredicates {
        if predicate(lhs, rhs) { return true }
        if predicate(rhs, lhs) { return false }
      }
      return false
    }
  }
}

extension Sequence {
  func sorted(
    by firstPredicate: (Element, Element) -> Bool,
    _ secondPredicate: (Element, Element) -> Bool,
    _ otherPredicates: ((Element, Element) -> Bool)...
  ) -> [Element] {
    return sorted(by:) { lhs, rhs in
      if firstPredicate(lhs, rhs) { return true }
      if firstPredicate(rhs, lhs) { return false }
      if secondPredicate(lhs, rhs) { return true }
      if secondPredicate(rhs, lhs) { return false }
      for predicate in otherPredicates {
        if predicate(lhs, rhs) { return true }
        if predicate(rhs, lhs) { return false }
      }
      return false
    }
  }
}

secondPredicate: 参数很不幸,但它是必需的,以避免与现有的 sort(by:) 重载产生歧义)

然后我们可以说(使用前面的contacts 数组):

contacts.sort(by:
  { $0.lastName > $1.lastName },  // first sort by lastName descending
  { $0.firstName < $1.firstName } // ... then firstName ascending
  // ...
)

print(contacts)

// [
//   Contact(firstName: "Michael", lastName: "Webb")
//   Contact(firstName: "Alex", lastName: "Elexson"),
//   Contact(firstName: "Michael", lastName: "Elexson"),
//   Contact(firstName: "Leonard", lastName: "Charleson"),
//   Contact(firstName: "Charles", lastName: "Alexson"),
// ]

// or with sorted(by:)...
let sortedContacts = contacts.sorted(by:
  { $0.lastName > $1.lastName },  // first sort by lastName descending
  { $0.firstName < $1.firstName } // ... then firstName ascending
  // ...
)

虽然调用站点不像元组变体那样简洁,但您可以更清楚地了解正在比较的内容和顺序。


符合Comparable

如果您要定期进行此类比较,那么按照@AMomchilov@appzYourLife 的建议,您可以将ContactComparable 一致:

extension Contact : Comparable {
  static func == (lhs: Contact, rhs: Contact) -> Bool {
    return (lhs.firstName, lhs.lastName) ==
             (rhs.firstName, rhs.lastName)
  }
  
  static func < (lhs: Contact, rhs: Contact) -> Bool {
    return (lhs.lastName, lhs.firstName) <
             (rhs.lastName, rhs.firstName)
  }
}

现在只需调用sort() 进行升序:

contacts.sort()

sort(by: &gt;) 降序排列:

contacts.sort(by: >)

在嵌套类型中定义自定义排序顺序

如果您有其他想要使用的排序顺序,您可以在嵌套类型中定义它们:

extension Contact {
  enum Comparison {
    static let firstLastAscending: (Contact, Contact) -> Bool = {
      return ($0.firstName, $0.lastName) <
               ($1.firstName, $1.lastName)
    }
  }
}

然后简单地调用为:

contacts.sort(by: Contact.Comparison.firstLastAscending)

【讨论】:

  • contacts.sort { ($0.lastName, $0.firstName) &lt; ($1.lastName, $1.firstName) } 帮助。谢谢
  • 如果像我一样,要排序的属性是可选的,那么你可以这样做:contacts.sort { ($0.lastName ?? "", $0.firstName ?? "") &lt; ($1.lastName ?? "", $1.firstName ?? "") }
  • 冬青莫莉!如此简单却如此高效……为什么我从未听说过?!非常感谢!
  • @BobCowe 这让您受制于"" 与其他字符串的比较(它位于非空字符串之前)。如果您希望 nils 出现在列表末尾,这有点隐含、有点神奇且不灵活。我建议你看看我的nilComparator函数stackoverflow.com/a/44808567/3141234
【解决方案2】:

想想“按多个标准排序”是什么意思。这意味着首先通过一个标准比较两个对象。然后,如果这些标准相同,则下一个标准将打破平局,依此类推,直到您获得所需的排序。

let sortedContacts = contacts.sort {
    if $0.lastName != $1.lastName { // first, compare by last names
        return $0.lastName < $1.lastName
    }
    /*  last names are the same, break ties by foo
    else if $0.foo != $1.foo {
        return $0.foo < $1.foo
    }
    ... repeat for all other fields in the sorting
    */
    else { // All other fields are tied, break ties by last name
        return $0.firstName < $1.firstName
    }
}

您在此处看到的是Sequence.sorted(by:) method,它参考提供的闭包来确定元素的比较方式。

如果你的排序会在很多地方使用,最好让你的类型符合Comparable protocol。这样,您可以使用Sequence.sorted() method,它会参考您的Comparable.&lt;(_:_:) operator 实现来确定元素的比较方式。这样,您可以对Contacts 中的任何Sequence 进行排序,而无需重复排序代码。

【讨论】:

  • else 正文必须在 { ... } 之间,否则代码无法编译。
  • 知道了。我试图实现它,但无法获得正确的语法。非常感谢。
  • sortsortInPlace 参见 here。请参阅下面的this,它更加模块化
  • @AthanasiusOfAlex 使用== 不是一个好主意。它仅适用于 2 个属性。不仅如此,您还开始用大量复合布尔表达式重复自己
  • 你们是可以使用分离代码的完美例子。其他人根本不需要!如果返回,则返回!
【解决方案3】:

另一种使用 2 个标准进行排序的简单方法如下所示。

检查第一个字段,在本例中为lastName,如果它们不相等,则按lastName 排序,如果lastName 相等,则按第二个字段排序,在本例中为@987654324 @。

contacts.sort { $0.lastName == $1.lastName ? $0.firstName < $1.firstName : $0.lastName < $1.lastName  }

【讨论】:

  • 这比元组提供了更多的灵活性。
  • 我喜欢它如此紧凑的事实。我确实添加了一些 cmets,所以如果我在一段时间后回到代码中,我会理解它背后的逻辑。但这可能只是我......
  • 这是一个与已接受答案重复的答案:使用三元运算符或 if-else 语句没有任何区别——问题不在于语法。
【解决方案4】:

这个问题已经有很多很好的答案了,不过我想点一篇文章——Sort Descriptors in Swift。我们有几种方法来进行多条件排序。

  1. 使用NSSortDescriptor,这种方式有一定的局限性,对象应该是一个类,并且继承自NSObject。

    class Person: NSObject {
        var first: String
        var last: String
        var yearOfBirth: Int
        init(first: String, last: String, yearOfBirth: Int) {
            self.first = first
            self.last = last
            self.yearOfBirth = yearOfBirth
        }
    
        override var description: String {
            get {
                return "\(self.last) \(self.first) (\(self.yearOfBirth))"
            }
        }
    }
    
    let people = [
        Person(first: "Jo", last: "Smith", yearOfBirth: 1970),
        Person(first: "Joe", last: "Smith", yearOfBirth: 1970),
        Person(first: "Joe", last: "Smyth", yearOfBirth: 1970),
        Person(first: "Joanne", last: "smith", yearOfBirth: 1985),
        Person(first: "Joanne", last: "smith", yearOfBirth: 1970),
        Person(first: "Robert", last: "Jones", yearOfBirth: 1970),
    ]
    

    例如,在这里,我们要按姓氏排序,然后是名字,最后是出生年份。我们希望不区分大小写并使用用户的语言环境。

    let lastDescriptor = NSSortDescriptor(key: "last", ascending: true,
      selector: #selector(NSString.localizedCaseInsensitiveCompare(_:)))
    let firstDescriptor = NSSortDescriptor(key: "first", ascending: true, 
      selector: #selector(NSString.localizedCaseInsensitiveCompare(_:)))
    let yearDescriptor = NSSortDescriptor(key: "yearOfBirth", ascending: true)
    
    
    
    (people as NSArray).sortedArray(using: [lastDescriptor, firstDescriptor, yearDescriptor]) 
    // [Robert Jones (1970), Jo Smith (1970), Joanne smith (1970), Joanne smith (1985), Joe Smith (1970), Joe Smyth (1970)]
    
  2. 使用 Swift 的姓/名排序方式。 这种方式应该适用于类/结构。但是,我们在这里不按 yearOfBirth 排序。

    let sortedPeople = people.sorted { p0, p1 in
        let left =  [p0.last, p0.first]
        let right = [p1.last, p1.first]
    
        return left.lexicographicallyPrecedes(right) {
            $0.localizedCaseInsensitiveCompare($1) == .orderedAscending
        }
    }
    sortedPeople // [Robert Jones (1970), Jo Smith (1970), Joanne smith (1985), Joanne smith (1970), Joe Smith (1970), Joe Smyth (1970)]
    
  3. 模仿 NSSortDescriptor 的快捷方式。这使用了“函数是一流类型”的概念。 SortDescriptor 是一个函数类型,接受两个值,返回一个布尔值。说 sortByFirstName 我们接受两个参数($0,$1)并比较它们的名字。 combine 函数接受一堆 SortDescriptor,比较它们并给出命令。

    typealias SortDescriptor<Value> = (Value, Value) -> Bool
    
    let sortByFirstName: SortDescriptor<Person> = {
        $0.first.localizedCaseInsensitiveCompare($1.first) == .orderedAscending
    }
    let sortByYear: SortDescriptor<Person> = { $0.yearOfBirth < $1.yearOfBirth }
    let sortByLastName: SortDescriptor<Person> = {
        $0.last.localizedCaseInsensitiveCompare($1.last) == .orderedAscending
    }
    
    func combine<Value>
        (sortDescriptors: [SortDescriptor<Value>]) -> SortDescriptor<Value> {
        return { lhs, rhs in
            for isOrderedBefore in sortDescriptors {
                if isOrderedBefore(lhs,rhs) { return true }
                if isOrderedBefore(rhs,lhs) { return false }
            }
            return false
        }
    }
    
    let combined: SortDescriptor<Person> = combine(
        sortDescriptors: [sortByLastName,sortByFirstName,sortByYear]
    )
    people.sorted(by: combined)
    // [Robert Jones (1970), Jo Smith (1970), Joanne smith (1970), Joanne smith (1985), Joe Smith (1970), Joe Smyth (1970)]
    

    这很好,因为您可以将它与 struct 和 class 一起使用,您甚至可以扩展它以与 nil 进行比较。

仍然强烈建议阅读original article。它有更多的细节和很好的解释。

【讨论】:

    【解决方案5】:

    @Hamish 描述的字典排序不能做的一件事是处理不同的排序方向,比如按第一个字段降序排序,下一个字段升序排序,等等。

    我创建了一篇关于如何在 Swift 3 中实现这一点的博客文章,并保持代码简单易读。

    你可以在这里找到它:

    http://master-method.com/index.php/2016/11/23/sort-a-sequence-i-e-arrays-of-objects-by-multiple-properties-in-swift-3/

    您还可以在此处找到包含代码的 GitHub 存储库:

    https://github.com/jallauca/SortByMultipleFieldsSwift.playground

    这一切的要点,比如说,如果你有位置列表,你将能够做到这一点:

    struct Location {
        var city: String
        var county: String
        var state: String
    }
    
    var locations: [Location] {
        return [
            Location(city: "Dania Beach", county: "Broward", state: "Florida"),
            Location(city: "Fort Lauderdale", county: "Broward", state: "Florida"),
            Location(city: "Hallandale Beach", county: "Broward", state: "Florida"),
            Location(city: "Delray Beach", county: "Palm Beach", state: "Florida"),
            Location(city: "West Palm Beach", county: "Palm Beach", state: "Florida"),
            Location(city: "Savannah", county: "Chatham", state: "Georgia"),
            Location(city: "Richmond Hill", county: "Bryan", state: "Georgia"),
            Location(city: "St. Marys", county: "Camden", state: "Georgia"),
            Location(city: "Kingsland", county: "Camden", state: "Georgia"),
        ]
    }
    
    let sortedLocations =
        locations
            .sorted(by:
                ComparisonResult.flip <<< Location.stateCompare,
                Location.countyCompare,
                Location.cityCompare
            )
    

    【讨论】:

    • “@Hamish 描述的字典排序不能做的一件事是处理不同的排序方向”——是的,他们可以,只需交换元组中的元素;)
    • 我发现这是一个有趣的理论练习,但比@Hamish 的答案复杂得多。在我看来,更少的代码是更好的代码。
    【解决方案6】:

    我建议使用Hamish's tuple solution,因为它不需要额外的代码。


    如果您想要一些行为类似于 if statements 但简化了分支逻辑的东西,您可以使用此解决方案,它允许您执行以下操作:

    animals.sort {
      return comparisons(
        compare($0.family, $1.family, ascending: false),
        compare($0.name, $1.name))
    }
    

    以下是允许您执行此操作的函数:

    func compare<C: Comparable>(_ value1Closure: @autoclosure @escaping () -> C, _ value2Closure: @autoclosure @escaping () -> C, ascending: Bool = true) -> () -> ComparisonResult {
      return {
        let value1 = value1Closure()
        let value2 = value2Closure()
        if value1 == value2 {
          return .orderedSame
        } else if ascending {
          return value1 < value2 ? .orderedAscending : .orderedDescending
        } else {
          return value1 > value2 ? .orderedAscending : .orderedDescending
        }
      }
    }
    
    func comparisons(_ comparisons: (() -> ComparisonResult)...) -> Bool {
      for comparison in comparisons {
        switch comparison() {
        case .orderedSame:
          continue // go on to the next property
        case .orderedAscending:
          return true
        case .orderedDescending:
          return false
        }
      }
      return false // all of them were equal
    }
    

    如果你想测试一下,你可以使用这个额外的代码:

    enum Family: Int, Comparable {
      case bird
      case cat
      case dog
    
      var short: String {
        switch self {
        case .bird: return "B"
        case .cat: return "C"
        case .dog: return "D"
        }
      }
    
      public static func <(lhs: Family, rhs: Family) -> Bool {
        return lhs.rawValue < rhs.rawValue
      }
    }
    
    struct Animal: CustomDebugStringConvertible {
      let name: String
      let family: Family
    
      public var debugDescription: String {
        return "\(name) (\(family.short))"
      }
    }
    
    let animals = [
      Animal(name: "Leopard", family: .cat),
      Animal(name: "Wolf", family: .dog),
      Animal(name: "Tiger", family: .cat),
      Animal(name: "Eagle", family: .bird),
      Animal(name: "Cheetah", family: .cat),
      Animal(name: "Hawk", family: .bird),
      Animal(name: "Puma", family: .cat),
      Animal(name: "Dalmatian", family: .dog),
      Animal(name: "Lion", family: .cat),
    ]
    

    Jamie's solution 的主要区别在于,对属性的访问是内联定义的,而不是类上的静态/实例方法。例如。 $0.family 而不是 Animal.familyCompare。并且升序/降序由参数而不是重载运算符控制。 Jamie 的解决方案在 Array 上添加了一个扩展,而我的解决方案使用内置的 sort/sorted 方法,但需要定义两个额外的方法:comparecomparisons

    为了完整起见,以下是我的解决方案与Hamish's tuple solution 的比较。为了演示,我将使用一个狂野的示例,我们希望按(name, address, profileViews) 对人员进行排序,Hamish 的解决方案将在比较开始之前对 6 个属性值中的每一个进行一次准确的评估。这可能不是或可能不是所希望的。例如,假设profileViews 是一个昂贵的网络调用,我们可能希望避免调用profileViews,除非它是绝对必要的。我的解决方案将避免评估profileViews,直到$0.name == $1.name$0.address == $1.address。但是,当它确实评估 profileViews 时,它可能会评估不止一次。

    【讨论】:

      【解决方案7】:

      怎么样:

      contacts.sort() { [$0.last, $0.first].lexicographicalCompare([$1.last, $1.first]) }
      

      【讨论】:

      • lexicographicallyPrecedes 要求数组中的所有类型都相同。例如[String, String]。 OP 可能想要的是混合和匹配类型:[String, Int, Bool],这样他们就可以做[$0.first, $0.age, $0.isActive]
      【解决方案8】:

      在 Swift 3 中对我的数组 [String] 有效,在 Swift 4 中似乎还可以

      array = array.sorted{$0.compare($1, options: .numeric) == .orderedAscending}
      

      【讨论】:

      • 您在回答之前阅读了问题吗?按多个参数排序,而不是一个,你呈现的内容。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2021-06-11
      • 2012-11-02
      • 1970-01-01
      • 2022-12-02
      • 2021-02-23
      • 1970-01-01
      相关资源
      最近更新 更多