Aby posortować lokalną tablicę Swift według kilku pól, porównuj każdy klucz w kolejności priorytetu i przechodź dalej tylko wtedy, gdy wartości są równe. Osobno rozstrzygnij wartości opcjonalne używane jako zapasowe, a następnie określ, gdzie mają znaleźć się brakujące wartości. Ten przykład porządkuje syntetyczne etykiety kształtów na potrzeby listy na Macu.
Zdefiniuj zasady sortowania przed napisaniem domknięcia
Załóżmy, że lista na komputerze zawiera etykiety kształtów z numerem grupy, opcjonalną preferowaną pozycją, opcjonalną pozycją zapasową i tytułem. Zadanie polega na uzyskaniu jednego przewidywalnego porządku wyświetlania na podstawie tych wartości przechowywanych w pamięci.
Dwie decyzje wymagają osobnych reguł. Wartość zapasowa określa, która wartość reprezentuje dany element: użyj jego preferowanej pozycji, jeśli jest dostępna, a w przeciwnym razie pozycji zapasowej. Rozstrzygnięcie remisu porównuje inne pole dopiero wtedy, gdy efektywne pozycje dwóch elementów są równe. Operator Swift do łączenia z wartością nil, ??, obsługuje pierwszą decyzję, wybierając wartość opakowaną albo zapasową. Podstawowe operatory — operator do łączenia z wartością nil
W tym przykładzie zastosuj następujące jawne zasady:
| Priorytet | Klucz | Reguła |
|---|---|---|
| 1 | Grupa | Najpierw mniejsze liczby |
| 2 | Efektywna pozycja | preferredPosition ?? backupPosition |
| 3 | Umiejscowienie brakującej pozycji | Brakujące pozycje trafiają za dostępnymi w obrębie każdej grupy |
| 4 | Tytuł | Rosnąco według zwykłego porównywania ciągów znaków w Swift, gdy pozycje są równe |
| 5 | Identyfikator | Najpierw mniejsze identyfikatory, gdy wszystkie wcześniejsze klucze są równe |
Umiejscowienie brakującej pozycji jest częścią porównania pozycji, a nie niezależnym polem, które ma pierwszeństwo przed grupą. Element z grupy zero nadal poprzedza element z grupy jeden, nawet jeśli pierwszy nie ma pozycji.
Porównanie tytułu celowo wykorzystuje zwykły porządek ciągów znaków w Swift na potrzeby tego syntetycznego obliczenia. Zachowaj ten wybór, jeśli później zastąpisz domknięcie komparatorami Foundation.
Zapisz pełne sortowanie wielopolowe z jawną obsługą nil
Poniższy kompletny program korzysta ze standardowej biblioteki Swift i nie wymaga żadnych importów. Uruchom go w środowisku obsługującym Swift na Macu. Wszystkie dane wejściowe są zdefiniowane w programie; obliczenie nie korzysta z danych zewnętrznych ani pamięci trwałej. Operacja sortowania zwraca nową tablicę za pomocą sorted(by:). Dokumentacja Apple dotycząca 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))
Oczekiwany wynik:
[50, 70, 20, 40, 60, 10, 30]
Program i oczekiwana kolejność zostały sprawdzone, ale tutaj ich nie uruchomiono, ponieważ środowisko badawcze nie ma kompilatora Swift. Dołączone warunki wstępne pozwalają sprawdzić wynik podczas lokalnego uruchomienia.
Dlaczego każdy przykład znajduje się na tej pozycji
Element 50 jest pierwszy, ponieważ grupa zero ma pierwszeństwo przed pozycją. Następnie element 70 pokazuje, że zero jest prawidłową preferowaną pozycją: jego zapasowa wartość osiem nie jest używana. Element 20 korzysta z preferowanej pozycji równej jeden.
Elementy 40, 60 i 10 mają efektywną pozycję równą dwa. Oba tytuły Arc poprzedzają Circle, a identyfikatory rozstrzygają pozostały remis między 40 i 60. Element 30 jest ostatni, ponieważ w grupie jeden brakuje obu pól pozycji.
To również odpowiada na częste pytanie o wartość zapasową: element 40 nie korzysta ze swojej zapasowej pozycji równej zero, gdy jego preferowana pozycja jest równa pozycji innego elementu. Zgodnie z tymi zasadami wartość zapasowa zastępuje tylko brakującą wartość preferowaną. Jeśli pozycja zapasowa ma również rozstrzygać remisy, potrzebna będzie dodatkowa reguła porównywania.
Zadbaj o poprawność komparatora w przypadku równych wartości
Swift wymaga, aby predykat sortowania tworzył ścisły słaby porządek. W praktyce element nigdy nie może poprzedzać samego siebie, decyzje o kolejności muszą pozostawać spójne między porównaniami, a równoważne elementy muszą tworzyć spójne grupy. Apple opisuje te wymagania dla sorted(by:). Wymagania dotyczące predykatu
Przykład sprawdza, czy wcześniejszy klucz jest inny, zanim zwróci wynik porównania. Tytuł jest porównywany tylko wtedy, gdy grupa i efektywna pozycja są równe, a identyfikator dopiero wtedy, gdy równy jest również tytuł. Gdy identyfikatory i wszystkie wcześniejsze klucze są równe, końcowe ścisłe porównanie zwraca false.
Unikaj łączenia niezależnych porównań pól za pomocą ||. Jeden element może mieć wcześniejszą grupę, ale późniejszą pozycję, a drugi odwrotną kombinację. Predykat oparty na operatorze OR mógłby wówczas stwierdzić, że każdy z elementów powinien poprzedzać drugi. Sekwencyjne rozstrzyganie remisów zapobiega tej sprzeczności.
Unikaj też używania <= jako końcowego porównania: równe wartości zwróciłyby true, naruszając regułę, że element nie może poprzedzać samego siebie. Każde porównanie powinno zależeć wyłącznie od przekazanych wartości i ustalonych reguł sortowania.
Wybierz między stabilnymi remisami a końcowym identyfikatorem
Udokumentowane sortowanie w Swift jest stabilne: elementy uznane przez predykat za równoważne zachowują wzajemną kolejność z danych wejściowych. Jeśli takie zachowanie jest zamierzone, pomiń końcowe porównanie identyfikatorów i po remisie tytułów zwróć false. Gwarancja stabilnego sortowania
Ten przykład używa zamiast tego unikatowych syntetycznych identyfikatorów do rozstrzygania pozostałych remisów. Dlatego odwrócenie kolejności danych wejściowych nadal daje tę samą sekwencję identyfikatorów. Ta właściwość wynika z przyjętych zasad i przykładowych wartości. Jeśli identyfikatory powtarzają się razem ze wszystkimi innymi kluczami, te elementy pozostają równoważne i zachowują kolejność z danych wejściowych.
Zapisz te same reguły za pomocą komparatorów kluczy ścieżek Foundation
Foundation udostępnia KeyPathComparator do porównywania właściwości oraz sorted(using:) do stosowania sekwencji komparatorów. Pierwszy komparator określa główny porządek, a kolejne rozstrzygają remisy. Te API są dostępne od macOS 12. KeyPathComparator, sorted(using:)
W równoważnej wersji korzystającej z Foundation udostępnij dwa klucze pozycji: rangę oznaczającą brakującą wartość i pozycję liczbową. Ranga umieszcza brakujące pozycje na końcu. Gdy rangi są równe, numeryczna wartość zastępcza dla brakującej pozycji nie może zderzyć się z dostępnym zerem, ponieważ te elementy mają już różne rangi.
Dodaj poniższe rozszerzenie i obliczenie po kompletnym programie powyżej. To przykład zależny od istniejących definicji ShapeItem, items i expectedIDs. Wymaga macOS 14 lub nowszego, ponieważ jawnie konstruuje ComparableComparator za pomocą init(order:). Dostępność inicjalizatora
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))
}
Jawny komparator tytułu zachowuje wcześniejszy wybór porządku ciągów znaków w Swift. Dokumentacja Foundation podaje, że ComparableComparator korzysta z implementacji Comparable porównywanego typu, a KeyPathComparator przyjmuje przekazany komparator właściwości. ComparableComparator, Inicjalizator komparatora klucza ścieżki
W przypadku jednego ustalonego układu domknięcie pokazuje każdą gałąź. Tablica komparatorów przydaje się, gdy chcesz przedstawić kolejność obsługiwanych kluczy jako wartości. W obu podejściach umieść wybór wartości zapasowej we właściwości obliczeniowej modelu, aby obie implementacje korzystały z tej samej definicji.
Dostosuj reguły bez zmiany ich znaczenia
Aby sortować pozycje malejąco, pozostawiając brakujące pozycje na końcu, zmień tylko porównanie dwóch dostępnych pozycji na left > right. W wersji Foundation odwróć tylko numericPositionKey; pozostaw missingPositionRank w kolejności rosnącej. Odwrócenie całego wyniku zmieniłoby również kolejność grup, tytułów i identyfikatorów, a także umiejscowienie brakujących wartości.
Jeśli opcjonalny ciąg znaków stanie się kluczem zapasowym, osobno zdecyduj, czy pusty ciąg znaków ma być uznawany za brakujący. Operator łączenia z wartością nil reaguje na nil; nie zastępuje obecnego, pustego ciągu znaków. Przed sortowaniem określ wszelkie dodatkowe reguły normalizacji i używaj tego samego klucza efektywnego w całym procesie. Działanie operatora do łączenia z wartością nil
To niewielkie obliczenie wykonuj synchronicznie. Przewodnik Swift dotyczący współbieżności rozróżnia zwykłe sortowanie synchroniczne od punktów zawieszenia, a udokumentowany predykat sorted(by:) jest synchroniczny. Jeśli aplikacja potrzebuje wartości uzyskanych w ramach operacji asynchronicznych, pobierz je przed wywołaniem tego sortowania. Dodanie await wewnątrz domknięcia porównującego nie jest obsługiwanym rozszerzeniem jego sygnatury. Współbieżność, Sygnatura sorted(by:)
Zanim dostosujesz przykład do innej listy na komputerze, zachowaj kontrole obejmujące brak preferowanego klucza, brak obu kluczy pozycji, prawidłową wartość zero, równe efektywne pozycje oraz całkowicie równe pola wyświetlania. Zdecyduj, czy ostatni remis ma zachowywać kolejność z danych wejściowych, czy być rozstrzygany identyfikatorem. Te wybory zapewniają precyzyjne zasady sortowania listy, które pozostają zrozumiałe w obu implementacjach.
Źródła
- sorted(by:) — Apple
- Podstawowe operatory — Swift Project
- KeyPathComparator — Apple
- sorted(using:) — Apple
- ComparableComparator — Apple
- init(order:) — Apple
- init(_:comparator:) — Apple
- Concurrency — Swift Project