Um ein lokales Swift-Array nach mehreren Feldern zu sortieren, vergleiche jeden Schlüssel in Prioritätsreihenfolge und fahre nur fort, wenn Gleichstand herrscht. Ermittle optionale Ersatzwerte separat und lege dann fest, wo fehlende Werte einsortiert werden. Dieses Beispiel ordnet synthetische Formbezeichnungen für eine Mac-Desktopliste.

Den Sortiervertrag festlegen, bevor die Closure geschrieben wird

Angenommen, eine Desktopliste enthält Formbezeichnungen mit einer Gruppennummer, einer optionalen bevorzugten Position, einer optionalen Ersatzposition und einem Titel. Die Aufgabe besteht darin, aus diesen In-Memory-Werten eine einzige vorhersehbare Anzeigereihenfolge zu erzeugen.

Zwei Entscheidungen benötigen getrennte Regeln. Ein Ersatzwert bestimmt, welcher Wert ein Element repräsentiert: Verwende seine bevorzugte Position, falls vorhanden, andernfalls seine Ersatzposition. Ein Tie-Breaker vergleicht ein weiteres Feld erst, nachdem zwei Elemente denselben effektiven Positionswert haben. Swifts Nil-Coalescing-Operator ?? unterstützt die erste Entscheidung, indem er den enthaltenen Wert oder den Ersatzwert auswählt. Grundlegende Operatoren — Nil-Coalescing-Operator

Verwende für das Beispiel diesen expliziten Vertrag:

Priorität Schlüssel Regel
1 Gruppe Kleinere Zahlen zuerst
2 Effektive Position preferredPosition ?? backupPosition
3 Platzierung fehlender Positionen Fehlende Positionen folgen innerhalb jeder Gruppe auf vorhandene Positionen
4 Titel Aufsteigender Swift-Zeichenfolgenvergleich bei gleichen Positionen
5 Bezeichner Kleinere Bezeichner zuerst, wenn bei allen vorherigen Schlüsseln Gleichstand herrscht

Die Platzierung fehlender Positionen ist Teil des Positionsvergleichs und kein unabhängiges Feld, das die Gruppe außer Kraft setzt. Ein Element in Gruppe null steht weiterhin vor einem Element in Gruppe eins, auch wenn dem ersten Element eine Position fehlt.

Der Titelvergleich verwendet für diese synthetische Berechnung bewusst Swifts gewöhnliche Zeichenfolgenreihenfolge. Behalte diese Wahl bei, wenn du die Closure später durch Foundation-Vergleiche ersetzt.

Eine vollständige Sortierung nach mehreren Feldern mit expliziter Nil-Behandlung schreiben

Das folgende vollständige Programm verwendet die Swift-Standardbibliothek und benötigt keine Imports. Führe es in einer Swift-fähigen Umgebung auf deinem Mac aus. Alle Eingaben sind im Programm definiert; die Berechnung verwendet weder externe Eingaben noch dauerhafte Speicherung. Die Sortieroperation gibt über sorted(by:) ein neues Array zurück. Apples Dokumentation zu sorted(by:)

struct ShapeItem {
    let id: Int
    let group: Int
    let preferredPosition: Int?
    let backupPosition: Int?
    let title: String

    var effectivePosition: Int? {
        preferredPosition ?? backupPosition
    }
}

func comesBefore(_ lhs: ShapeItem, _ rhs: ShapeItem) -> Bool {
    if lhs.group != rhs.group {
        return lhs.group < rhs.group
    }

    switch (lhs.effectivePosition, rhs.effectivePosition) {
    case let (.some(left), .some(right)):
        if left != right {
            return left < right
        }
    case (.some(_), .none):
        return true
    case (.none, .some(_)):
        return false
    case (.none, .none):
        break
    }

    if lhs.title != rhs.title {
        return lhs.title < rhs.title
    }

    return lhs.id < rhs.id
}

let items = [
    ShapeItem(id: 10, group: 1, preferredPosition: nil,
              backupPosition: 2, title: "Circle"),
    ShapeItem(id: 20, group: 1, preferredPosition: 1,
              backupPosition: 9, title: "Square"),
    ShapeItem(id: 30, group: 1, preferredPosition: nil,
              backupPosition: nil, title: "Arc"),
    ShapeItem(id: 40, group: 1, preferredPosition: 2,
              backupPosition: 0, title: "Arc"),
    ShapeItem(id: 50, group: 0, preferredPosition: nil,
              backupPosition: nil, title: "Dot"),
    ShapeItem(id: 60, group: 1, preferredPosition: 2,
              backupPosition: nil, title: "Arc"),
    ShapeItem(id: 70, group: 1, preferredPosition: 0,
              backupPosition: 8, title: "Zero")
]

let ordered = items.sorted(by: comesBefore)
let expectedIDs = [50, 70, 20, 40, 60, 10, 30]

precondition(ordered.map(\.id) == expectedIDs)
precondition(ordered.count == items.count)
precondition(items.reversed().sorted(by: comesBefore).map(\.id)
             == expectedIDs)

print(ordered.map(\.id))

Erwartete Ausgabe:

[50, 70, 20, 40, 60, 10, 30]

Das Programm und die erwartete Reihenfolge wurden überprüft, hier jedoch nicht ausgeführt, da die Rechercheumgebung keinen Swift-Compiler hat. Mit den enthaltenen Preconditions kannst du das Ergebnis beim lokalen Ausführen prüfen.

Warum jedes Beispiel an dieser Stelle landet

Element 50 steht an erster Stelle, weil Gruppe null Vorrang hat, bevor die Position berücksichtigt wird. Element 70 zeigt anschließend, dass null eine gültige bevorzugte Position ist: Sein Ersatzwert acht wird nicht verwendet. Element 20 verwendet seine bevorzugte Position eins.

Die Elemente 40, 60 und 10 haben alle die effektive Position zwei. Die beiden Titel Arc stehen vor Circle, und die Bezeichner lösen den verbleibenden Gleichstand zwischen 40 und 60 auf. Element 30 steht zuletzt, weil beide Positionsfelder in Gruppe eins fehlen.

Damit ist auch eine häufige Frage zu Ersatzwerten beantwortet: Element 40 verwendet seinen Ersatzwert null nicht, wenn seine bevorzugte Position mit der eines anderen Elements gleichzieht. Nach diesem Vertrag ersetzt der Ersatzwert nur eine fehlende bevorzugte Position. Soll der Ersatzwert auch Gleichstände auflösen, ist dafür eine zusätzliche Vergleichsregel erforderlich.

Den Comparator bei Gleichständen gültig halten

Swift verlangt, dass das Sortierprädikat eine strikte schwache Ordnung bildet. Praktisch bedeutet das: Ein Element darf sich niemals selbst voranstellen, Sortierentscheidungen müssen über Vergleiche hinweg konsistent bleiben, und äquivalente Elemente müssen konsistente Gruppen bilden. Apple dokumentiert diese Anforderungen für sorted(by:). Anforderungen an Prädikate

Das Beispiel prüft, ob sich ein früherer Schlüssel unterscheidet, bevor es das Sortierergebnis zurückgibt. Es erreicht den Titel nur, wenn Gruppe und effektive Position gleich sind, und den Bezeichner nur, wenn auch der Titel gleich ist. Sind die Bezeichner und alle vorherigen Schlüssel gleich, gibt der abschließende strikte Vergleich false zurück.

Vermeide es, unabhängige Feldvergleiche mit || zu verknüpfen. Ein Element könnte eine frühere Gruppe, aber eine spätere Position haben; ein anderes könnte die umgekehrte Kombination aufweisen. Ein auf OR basierendes Prädikat könnte dann aussagen, dass jedes Element vor dem anderen steht. Eine sequenzielle Behandlung von Gleichständen verhindert diesen Widerspruch.

Vermeide außerdem <= als abschließenden Vergleich: Bei gleichen Werten würde es true zurückgeben und damit gegen die Regel verstoßen, dass ein Element sich nicht selbst voranstellen kann. Jeder Vergleich sollte nur von den übergebenen Werten und festen Sortierregeln abhängen.

Zwischen stabilen Gleichständen und einem abschließenden Bezeichner wählen

Swifts dokumentierte Sortierung ist stabil: Elemente, die das Prädikat als äquivalent behandelt, behalten ihre relative Eingabereihenfolge bei. Wenn das beabsichtigt ist, lass den abschließenden Bezeichnervergleich weg und gib nach einem Gleichstand beim Titel false zurück. Garantie stabiler Sortierung

Dieses Beispiel verwendet stattdessen eindeutige synthetische Bezeichner, um verbleibende Gleichstände aufzulösen. Daher ergibt auch eine Umkehrung der Beispieleingabe dieselbe Bezeichnerreihenfolge. Diese Eigenschaft ergibt sich aus diesem Vertrag und den Beispielwerten. Wenn sich Bezeichner bei allen anderen Schlüsseln wiederholen, bleiben diese Elemente äquivalent und behalten ihre Eingabereihenfolge bei.

Dieselben Regeln mit Foundation-Key-Path-Comparators ausdrücken

Foundation stellt KeyPathComparator zum Vergleichen von Eigenschaften und sorted(using:) zum Anwenden einer Folge von Comparators bereit. Der erste Comparator legt die primäre Reihenfolge fest; nachfolgende Comparators lösen Gleichstände auf. Diese APIs sind ab macOS 12 verfügbar. KeyPathComparator, sorted(using:)

Für eine entsprechende Foundation-Version werden zwei Positionsschlüssel offengelegt: ein Rang für fehlende Werte und eine numerische Position. Der Rang platziert fehlende Positionen zuletzt. Wenn dieser Rang gleich ist, kann ein numerischer Ersatzwert für fehlende Positionen nicht mit einer vorhandenen Null kollidieren, da diese Elemente bereits unterschiedliche Ränge haben.

Füge die folgende Erweiterung und Berechnung nach dem vollständigen Programm oben ein. Dies ist ein abhängiges Beispiel, das die vorhandenen Definitionen ShapeItem, items und expectedIDs verwendet. Es erfordert macOS 14 oder neuer, da es ComparableComparator explizit mit init(order:) erstellt. Verfügbarkeit des Initializers

import Foundation

extension ShapeItem {
    var missingPositionRank: Int {
        effectivePosition == nil ? 1 : 0
    }

    var numericPositionKey: Int {
        effectivePosition ?? 0
    }
}

if #available(macOS 14.0, *) {
    let comparators: [KeyPathComparator<ShapeItem>] = [
        KeyPathComparator(\ShapeItem.group, order: .forward),
        KeyPathComparator(\ShapeItem.missingPositionRank,
                          order: .forward),
        KeyPathComparator(\ShapeItem.numericPositionKey,
                          order: .forward),
        KeyPathComparator(
            \ShapeItem.title,
            comparator: ComparableComparator<String>(order: .forward)
        ),
        KeyPathComparator(\ShapeItem.id, order: .forward)
    ]

    let foundationOrdered = items.sorted(using: comparators)
    precondition(foundationOrdered.map(\.id) == expectedIDs)
    print(foundationOrdered.map(\.id))
} else {
    print(items.sorted(by: comesBefore).map(\.id))
}

Der explizite Titel-Comparator behält die zuvor gewählte Swift-Zeichenfolgenreihenfolge bei. Foundation dokumentiert, dass ComparableComparator die Comparable-Implementierung des verglichenen Typs verwendet und dass KeyPathComparator einen bereitgestellten Property-Comparator annimmt. ComparableComparator, Key-Path-Comparator-Initializer

Für eine feste Anordnung macht die Closure jeden Zweig sichtbar. Ein Comparator-Array ist nützlich, wenn die Reihenfolge unterstützter Schlüssel als Werte dargestellt werden soll. In beiden Ansätzen sollte die Auswahl von Ersatzwerten in der berechneten Eigenschaft des Modells verbleiben, damit beide Implementierungen dieselbe Definition verwenden.

Die Regeln anpassen, ohne ihre Bedeutung zu ändern

Um Positionen absteigend zu sortieren und fehlende Positionen zuletzt zu belassen, ändere nur den Vergleich zwischen zwei vorhandenen Positionen zu left > right. In der Foundation-Version kehrst du nur numericPositionKey um; missingPositionRank bleibt aufsteigend. Eine Umkehrung des gesamten Ergebnisses würde auch Gruppen, Titel, Bezeichner und die Platzierung fehlender Werte umkehren.

Wird eine optionale Zeichenfolge zu einem Ersatzschlüssel, entscheide separat, ob eine leere Zeichenfolge als fehlend gilt. Nil Coalescing reagiert auf nil; es ersetzt keine vorhandene leere Zeichenfolge. Lege zusätzliche Normalisierungsregeln vor dem Sortieren fest und verwende durchgehend denselben effektiven Schlüssel. Verhalten von Nil Coalescing

Halte diese kleine Berechnung synchron. Swifts Concurrency-Leitfaden unterscheidet gewöhnliches synchrones Sortieren von Unterbrechungspunkten, und das dokumentierte Prädikat sorted(by:) ist synchron. Wenn eine Anwendung Werte aus asynchroner Arbeit benötigt, beschaffe diese Werte vollständig, bevor du diesen Sortierer aufrufst. await innerhalb der Vergleichs-Closure hinzuzufügen, ist keine unterstützte Erweiterung ihrer Signatur. Nebenläufigkeit, Signatur von sorted(by:)

Behalte beim Anpassen des Beispiels an eine andere Desktopliste Prüfungen für einen fehlenden bevorzugten Schlüssel, zwei fehlende Positionsschlüssel, eine gültige Null, gleiche effektive Positionen und vollständig übereinstimmende Anzeigefelder bei. Entscheide, ob beim letzten Gleichstand die Eingabereihenfolge erhalten bleiben oder ein Bezeichner verwendet werden soll. Diese Entscheidungen geben der Liste einen präzisen Sortiervertrag, der in beiden Implementierungen verständlich bleibt.

Quellen