[Swift] ソートと SortComparator

     
⌛️ 3 min.

ソートと SortComparator を改めて確認します。

環境&対象

以下の環境で動作確認を行なっています。

  • macOS14.6
  • Xcode 16.1 Beta
  • iOS 17.5
  • Swift 5.9

ソート

Sequence には、要素を特定の順番に並び替えるsorted というメソッドが複数用意されています。

例えば、以下は 要素が Comparable であることをベースにした sorted メソッドです。

参考
sortedApple Developer Documentation

上記意外にも、さまざまな方法で比較方法を指定することができるようになっていますので、確認していきます。

sorted()

1つめが、引数なしに、sorted() とつかう sort です。

これは、Sequence の要素が、Comparable に conform している時に使用できます。

例えば、Int は、Comparable に conform しています。
ですので、Int を要素に持つ配列を sorted した結果は以下のようになります。

@Test func sort_comparableElement() async throws {
    let data = [1,5,2,3,4]

    let result = data.sorted()
    #expect(result == [1,2,3,4,5])
}

アプリケーションを作る時には、Int のように自然に導入された Comparable だけではなく、アプリケーションの要件からくる順番に ソートしたい時があります。そのような 特殊な大小関係を使用した ソート もできるようになっています。

sorted(by:)

例えば、偶数のあとに 奇数がくるようにソートしたい。さらに、偶数は 大きい数字が前に、奇数は小さい数字が前に来るようにソートしたい というケースを考えます。

sorted(by:) というメソッドは、比較方法を closure として渡してソートするものです。


参考
sorted(by:)Apple Developer Documentation

上記のソートを実装すると、以下のようになります。

@Test func sort_usingBy() async throws {
    let data = [1,5,2,3,4]

    let result = data.sorted(by: { lhs, rhs in
        if lhs.isMultiple(of: 2) && rhs.isMultiple(of: 2) { return lhs > rhs }
        if lhs.isMultiple(of: 2) { return true }
        if rhs.isMultiple(of: 2) { return false }
        return lhs < rhs
    })
    #expect(result == [4,2,1,3,5])
}

比較関数に書いた通り、偶数では 4 は 2よりも前に来ていて、奇数は 1,3,5 と並んでいます。

この sorted(by:) を使用することで 自前の比較関数を使用してソートすることができます。

アプリケーションが大きくなってくると この sorted(by:) を複数箇所で使用することも増えてきて、メンテナンスも考慮して 比較関数を共通化したくなります。

もちろん、比較関数をどこかで定義して、使用するのも良いのですが、その方法をすこし 進めた方法が次に説明する SortComparator です。

sorted(using:)

Sequence をソートするためのメソッドとして sorted(using:) という方法も用意されています。


参考
sorted(using:) Apple Developer Documentation

この方法では、引数に SortComparator に conform しているものを渡す必要があります。


参考
SortComparatorApple Developer Documentation

言い方を変えると、比較関数を一般化するための protocol が SortComparator です。

SortComparator に conform するためには比較関数である compare を定義することが必要です。
# そのほかに、順序を決める var order: SortOrder も必要です。

func compare(
    _ lhs: Self.Compared,
    _ rhs: Self.Compared
) -> ComparisonResult

先ほどの
「偶数のあとに 奇数がくるようにし、さらに、偶数は 大きい数字が前に、奇数は小さい数字が前に来るようにする」
という比較関数を SortComparator に準拠させて作ると以下になります。

struct OddAddictionComparator: SortComparator {
    typealias Compared = Int
    var order: SortOrder = .forward

    func compare(_ lhs: Int, _ rhs: Int) -> ComparisonResult {
        if lhs.isMultiple(of: 2) && rhs.isMultiple(of: 2) {
            if lhs < rhs { return .orderedDescending
            } else if lhs > rhs { return .orderedAscending }
            return .orderedSame
        }
        if lhs.isMultiple(of: 2) { return .orderedAscending }
        if rhs.isMultiple(of: 2) { return .orderedDescending }
        if lhs < rhs {
            return .orderedAscending
        } else if lhs > rhs {
            return .orderedDescending
        }
        return .orderedSame
    }
}

# 上記の OddAddictionComparator は、order の指定を無視しています。実際には order を考慮して実装することが必要となるでしょう。

上記の OddAddictionComparator を使用するソートは以下のようになります。

@Test func usingUsing() async throws {
    let data = [1,5,2,3,4]

    let result = data.sorted(using: OddAddictionComparator())
    #expect(result == [4,2,1,3,5])
}

このように SortComparator に conform した比較関数を用意しておくと、同じソートを複数箇所で簡単に使用することができるようになります。

order も考慮した実装にすると 逆順にソートすることも引数指定することで 容易になります。

SortComparator に conform している Comparator

自分で、SortComparator に conform する比較関数を実装する例を説明しましたが、Foundation には、SortComparator に conform している 型がいくつか用意されています。

KeyPathComparator

KeyPath を指定してソートするための SortComparator として、KeyPathComparator が用意されています。


参考
KeyPathComparatorApple Developer Documentation

名前の通り、比較対象として KeyPath を指定することができます。

[Swift] KeyPath の理解

KeyPath で指定するので、struct 等で指定先となる要素が必要です。
以下のように TestStruct を定義しました。

struct TestStruct: Equatable {
    var item1: Int
    var item2: Double

    init(_ item1: Int,_ item2: Double) {
        self.item1 = item1
        self.item2 = item2
    }
}

# Test が書きやすいように Equatable に conform していますが、KeyPathComparator 使用に必須ではありません。

KeyPathComparator を使用してソートする例は、以下のようになります。

@Test func KeyPathComparatorTest() async throws {
    let structs = [TestStruct(1, 1.5),
                   TestStruct(3, 8.9),
                   TestStruct(2, 1.1),
                   TestStruct(6, 0.9),
                   TestStruct(5, 5.9),
    ]
    let key1Result = structs.sorted(using: KeyPathComparator(\.item1))
    #expect(key1Result == [
        TestStruct(1, 1.5),
        TestStruct(2, 1.1),
        TestStruct(3, 8.9),
        TestStruct(5, 5.9),
        TestStruct(6, 0.9),
        ])

    let key2Result = structs.sorted(using: KeyPathComparator(\.item2, order: .reverse))
    #expect(key2Result == [
        TestStruct(3, 8.9),
        TestStruct(5, 5.9),
        TestStruct(1, 1.5),
        TestStruct(2, 1.1),
        TestStruct(6, 0.9),
        ])
}

.item1 という KeyPath での比較、.item2 という KeyPath での比較 それぞれが 指定された KeyPath を使用してソートされていることがわかります。
なお、KeyPathComparator は、きちんと(?) order 指定に対しても対応しているので、.reverse を指定すると逆順にソートされます。

SortDescriptor

KeyPathComparator と似ている SortComparator として SortDescriptor があります。


参考
SortDescriptorApple Developer Documentation

SortDescriptor は、CoreData や SwiftData を使用する時によく見る気がします。

KeyPathComparator と SortDescriptor の重要な相違点は、SortDescriptor は、Sendable という点です。
# KeyPath は、Sendable ではないので、KeyPathComparator も Sendable ではありません。

使用例もほとんど同じです。

@Test func SortDescriptorComparatorTest() async throws {
    let structs = [TestStruct(1, 1.5),
                   TestStruct(3, 8.9),
                   TestStruct(2, 1.1),
                   TestStruct(6, 0.9),
                   TestStruct(5, 5.9),
    ]
    let key1Result = structs.sorted(using: SortDescriptor(\.item1))
    #expect(key1Result == [
        TestStruct(1, 1.5),
        TestStruct(2, 1.1),
        TestStruct(3, 8.9),
        TestStruct(5, 5.9),
        TestStruct(6, 0.9),
        ])

    let key2Result = structs.sorted(using: SortDescriptor(\.item2, order: .reverse))
    #expect(key2Result == [
        TestStruct(3, 8.9),
        TestStruct(5, 5.9),
        TestStruct(1, 1.5),
        TestStruct(2, 1.1),
        TestStruct(6, 0.9),
        ])
}

String.Comparator

最後に、String.Comparator を紹介します。

その名の通り、文字列を比較する時のために用意されている Comparator です


参考
String.ComparatorApple Developer Documentation

比較順のための order だけではなく、比較を指定できる option が指定できるようになっています。
# 様々なオプションが用意されているように見えますが、大抵は 検索時に使用できるオプションです。

面白い(?)オプションとしては、「文字に含まれる数値を使った比較」である numeric オプションがあります。

参考
numericApple Developer Documentation

オプションの詳細はドキュメントを確認してください。

参考
NSString.CompareOptionsApple Developer Documentation

numeric 指定すると 以下のように、文字列に含まれる数値の意味を理解してソートできます。

@Test func StringOptionsComparatorTest() async throws {
    let strings = ["Hello2",
                   "Hello56",
                   "Hello3",
                   "Hello21",
    ]

    let literalResult = strings.sorted(using: String.Comparator(options: []))
    #expect(literalResult == [
        "Hello2",
        "Hello21",
        "Hello3",
        "Hello56",
        ])

    let numericResult = strings.sorted(using: String.Comparator(options: [.numeric]))
    #expect(numericResult == [
        "Hello2",
        "Hello3",
        "Hello21",
        "Hello56",
        ])
}

最初のソートは文字列としてソートしているので、Hello2 -> Hello21 -> Hello3 .. となります。

.numeric オプションを指定してソートすると、2 < 3 < 21 なので、Hello2 -> Hello3 -> Hello21 となっています。

SortComparator を複数受け付ける sorted

sorted(using:) には、複数の SortComparator を受け付けるバージョンもあります。


参考
sorted(using:)Apple Developer Documentation

最初の SortComparator で、.orderedSame と判定された場合、次の SortComparator での比較が行われます。

@Test func sortWithSortComparators() async throws {
    let structs = [TestStruct(1, 1.5),
                   TestStruct(3, 8.9),
                   TestStruct(2, 1.1),
                   TestStruct(2, 4.1),
                   TestStruct(3, 0.9),
                   TestStruct(1, 1.9),
    ]

    let results = structs.sorted(using: [KeyPathComparator(\.item1), KeyPathComparator(\.item2)])
    #expect(results == [
        TestStruct(1, 1.5),
        TestStruct(1, 1.9),
        TestStruct(2, 1.1),
        TestStruct(2, 4.1),
        TestStruct(3, 0.9),
        TestStruct(3, 8.9),
        ])
}

上記では、item1 の比較で、.orderedSame となった要素については、item2 を比較したソートになっています。

まとめ

ソートと SortComparator を確認しました。

ソートと SortComparator
  • Comparable な要素については、sorted() が使える
  • 比較関数を closure として渡すと、sorted(by:) が使える
  • 比較関数を SortComparator に conform させると、sorted(using:) が使える
  • KeyPath でソートさせる KeyPathComparator や 文字列を理解する String.Comparator 等が用意されている
  • sorted(using:) には、複数の SortComparator を渡すこともできる

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

SwiftUI おすすめ本

SwiftUI を理解するには、以下の本がおすすめです。

SwiftUI ViewMatery

SwiftUI で開発していくときに、ViewやLayoutのための適切なmodifierを探すのが大変です。
英語での説明になってしまいますが、以下の”SwiftUI Views Mastery Bundle”という本がビジュアル的に確認して探せるので、便利です。

英語ではありますが、1ページに コードと画面が並んでいるので、非常にわかりやすいです。

View に適用できる modifier もわかりやすく説明されているので、ビューの理解だけではなく、どのような装飾ができるかも簡単にわかります。

超便利です

SwiftUIViewsMastery

販売元のページは、こちらです。

SwiftUI 徹底入門

# SwiftUI は、毎年大きく改善されていますので、少し古くなってしまいましたが、いまでも 定番本です。

Swift学習におすすめの本

詳解Swift

Swift の学習には、詳解 Swift という書籍が、おすすめです。

著者は、Swift の初期から書籍を出していますし、Swift の前に主力言語だった Objective-C という言語についても同様の書籍を出しています。

最新版を購入するのがおすすめです。

現時点では、上記の Swift 5 に対応した第5版が最新版です。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です