Swift-使用多个条件对对象数组进行排序


91

我有一个Contact对象数组:

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

联系人类别:

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

而且我想通过该数组进行排序lastName,然后firstName在某些情况下接触得到了相同的lastName

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

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

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


2
完全按照您刚才说的做!大括号内的代码应显示:“如果姓氏相同,则按名字排序;否则按姓氏排序”。
马特

4
我看到一些代码味道在这里:1)Contact可能不应该继承NSObject,2)Contact也许应该是一个结构,和3)firstNamelastName可能不应该隐含展开自选。
亚历山大-恢复莫妮卡

3
@AMomchilov没有理由建议Contact应该是一个结构,因为您不知道他的其余代码在使用实例时是否已经依赖于引用语义。
Patrick Goley

3
@AMomchilov“可能”具有误导性,因为您对其余代码库一无所知。如果将其更改为结构,则在对var进行突变时会生成所有突然的副本,而不是修改手头的实例。这是行为上的巨大变化,并且这样做“可能”会导致错误,因为不可能针对引用值语义对所有代码进行正确编码。
Patrick Goley16年

1
@AMomchilov尚未听到它可能应该是结构的一个原因。我认为OP不会喜欢修改他程序其余部分的语义的建议,尤其是在甚至不需要解决当前问题时。没意识到编译器规则对某些人来说是合法的……也许我在错误的网站上
Patrick Goley16年

Answers:


118

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

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 协议。这样,您可以使用Sequence.sorted()method,该方法会咨询您的Comparable.<(_:_:)运算符实现,以确定元素之间的比较方式。这样,您可以对任何s进行排序SequenceContact而不必重复排序代码。


2
else机构必须是介于{ ... }否则代码不会编译。
卡·安吉莱蒂

得到它了。我尝试实现它,但是语法不正确。非常感谢。
sbkl

对于sortsortInPlace这里。Aslo 在下面看到了,它的模块化程度更高
Honey

sortInPlace在Swift 3中不再可用,而是必须使用sort()sort()将改变数组本身。还有一个名为func的新函数sorted(),它将返回一个已排序的数组
Honey

2
@AthanasiusOfAlex使用==不是一个好主意。它仅适用于2个属性。不仅如此,您还开始使用许多复合的布尔表达式来重复自己
亚历山大-恢复莫妮卡(Monica)

118

使用元组对多个条件进行比较

通过多个条件执行排序的一种非常简单的方法(即,通过一个比较进行排序,如果相等,则通过另一个比较进行排序)是使用元组,因为<and >运算符对其执行字典式比较的操作有重载。

/// 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属性。如果它们不相等,则排序顺序将基于<与它们的比较。如果它们相等的,那么它会移动到在元组中的下一对元素,比较即该firstName性质。

标准库为2到6个元素的元组提供<>重载。

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

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:)需要多个谓词的重载

受有关使用map闭包和SortDescriptor 对集合进行排序的讨论的启发,另一个选择是定义的自定义重载,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 {
  mutating 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: >)降序排列:

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) < ($1.lastName, $1.firstName) } 帮忙 谢谢
Prabhakar Kasi,

如果像我一样,要排序的属性是可选的,则可以执行以下操作:contacts.sort { ($0.lastName ?? "", $0.firstName ?? "") < ($1.lastName ?? "", $1.firstName ?? "") }
BobCowe

我的妈呀!如此简单却如此高效……为什么我从未听说过?非常感谢!
Ethenyl

@BobCowe这让您无法确定如何""与其他字符串进行比较(非空字符串之前)。如果您希望将nils放在列表的末尾,则这是一种隐式,有点魔术和不灵活的方法。我建议你看看我的nilComparator功能stackoverflow.com/a/44808567/3141234
亚历山大-恢复莫妮卡

18

下面显示了使用2个条件进行排序的另一种简单方法。

检查第一个字段,在这种情况下为lastName,如果它们不相等,则按by排序lastName,如果lastName相等,则在第二个字段(在本例中)排序firstName

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

这提供了比元组更大的灵活性。
巴巴克

5

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

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

你可以在这里找到它:

http://master-method.com/index.php/2016/11/23/sort-a-sequence-ie-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
        )

1
“按@Hamish的描述,字典排序不能做的一件事就是处理不同的排序方向” –是的,可以,只需交换元组中的元素;)
Hamish

我发现这是一个有趣的理论练习,但比@Hamish的答案复杂得多。我认为更少的代码就是更好的代码。
曼努埃尔

5

这个问题已经有很多不错的答案,但是我想指出一篇文章-Swift中的Sort Descriptors。我们有几种方法可以对多个条件进行排序。

  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)并比较它们的名字。合并函数需要一堆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进行比较。

尽管如此,还是强烈建议阅读原始文章。它有更多的细节,并得到了很好的解释。


2

我建议使用Hamish的元组解决方案,因为它不需要额外的代码。


如果您想要某种行为类似于if语句但简化了分支逻辑,则可以使用此解决方案,该解决方案允许您执行以下操作:

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解决方案的主要区别在于,对属性的访问是内联定义的,而不是类上的静态/实例方法。例如,$0.family而不是Animal.familyCompare。升/降由参数控制,而不是由重载运算符控制。杰米(Jamie)的解决方案在Array上添加了扩展,而我的解决方案使用了内置的sort/ sorted方法,但需要定义另外两个:comparecomparisons

为了完整起见,这是我的解决方案与Hamish的元组解决方案比较的方式。为了说明这一点,我将使用一个荒谬的示例,在该示例中,我们要根据(name, address, profileViews)Hamish的解决方案对人进行排序,在比较开始之前,将对这6个属性值中的每一个进行精确的评估。这可能是不希望的,也可能是不希望的。例如,假设profileViews是昂贵的网络通话,profileViews除非绝对必要,否则我们可能希望避免通话。我的解决方案将避免评估profileViews直到$0.name == $1.name$0.address == $1.address。但是,当它进行评估时,评估的profileViews次数可能会超过一次。


1

怎么样:

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

lexicographicallyPrecedes要求数组中的所有类型都相同。例如[String, String]。OP可能想要的是混合匹配类型:[String, Int, Bool]这样他们就可以做到[$0.first, $0.age, $0.isActive]
有意义的

-1

在Swift 3中为我的array [String]工作,在Swift 4中似乎还可以

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

您在回答之前是否已阅读问题?按多个参数排序,而不是您提供的参数。
Vive
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.