Om du vill sortera en lokal Swift-array efter flera fält jämför du varje nyckel i prioritetsordning och fortsätter bara när de är lika. Hantera valfria reservvärden separat och bestäm sedan var saknade värden ska placeras. Det här exemplet sorterar syntetiska formetiketter för en lista på ett Mac-skrivbord.

Definiera sorteringsreglerna innan du skriver avslutningen

Anta att en lista på ett skrivbord innehåller formetiketter med ett gruppnummer, en valfri föredragen position, en valfri reservposition och en titel. Uppgiften är att skapa en förutsägbar visningsordning utifrån dessa värden i minnet.

Två beslut behöver separata regler. En reservregel väljer vilket värde som representerar ett objekt: använd dess föredragna position om den finns, annars dess reservposition. En skiljeregel jämför ett annat fält först när två objekt har samma effektiva position. Swifts nil-sammanslagningsoperator, ??, stöder det första beslutet genom att välja det inneslutna värdet eller reservvärdet. Grundläggande operatorer — nil-sammanslagningsoperatorn

Använd följande uttryckliga regler i exemplet:

Prioritet Nyckel Regel
1 Grupp Lägre nummer först
2 Effektiv position preferredPosition ?? backupPosition
3 Placering av saknad position Saknade positioner kommer efter befintliga positioner inom varje grupp
4 Titel Stigande Swift-strängjämförelse när positionerna är lika
5 Identifierare Lägre identifierare först när alla tidigare nycklar är lika

Placeringen av saknad position ingår i positionsjämförelsen, snarare än att vara ett oberoende fält som åsidosätter gruppen. Ett objekt i grupp noll kommer fortfarande före ett objekt i grupp ett, även om det första objektet saknar position.

Titeljämförelsen använder avsiktligt Swifts vanliga strängordning för den här syntetiska beräkningen. Behåll det valet konsekvent om du senare ersätter avslutningen med Foundation-jämförare.

Skriv en fullständig flernyckelsortering med uttrycklig nil-hantering

Följande fullständiga program använder Swift-standardbiblioteket och kräver inga importer. Kör det i en Swift-miljö på din Mac. Alla indata definieras i programmet; beräkningen använder inga externa indata eller beständig lagring. Sorteringsoperationen returnerar en ny array via sorted(by:). Apples dokumentation för 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))

Förväntad utdata:

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

Programmet och den förväntade ordningen har granskats, men kördes inte här eftersom undersökningsmiljön saknar en Swift-kompilator. De inkluderade förvillkoren låter dig kontrollera resultatet när du kör programmet lokalt.

Varför varje exempel hamnar på sin plats

Objekt 50 kommer först eftersom grupp noll prioriteras före positionen. Objekt 70 visar sedan att noll är en giltig föredragen position: reservvärdet åtta används inte. Objekt 20 använder sin föredragna position ett.

Objekten 40, 60 och 10 har alla den effektiva positionen två. De två titlarna Arc kommer före Circle, och identifierarna avgör den återstående skiljelikheten mellan 40 och 60. Objekt 30 kommer sist eftersom båda positionsfälten saknas i grupp ett.

Det här besvarar också en vanlig fråga om reservvärden: objekt 40 använder inte sin reservposition noll när dess föredragna position är lika med ett annat objekts. Enligt dessa regler ersätter reservvärdet bara ett saknat föredraget värde. Om reservvärdet också ska avgöra skiljelikheter krävs ytterligare en jämförelseregel.

Se till att jämförelsefunktionen förblir giltig när värden är lika

Swift kräver att sorteringspredikatet bildar en strikt svag ordning. I praktiken får ett objekt aldrig komma före sig självt, sorteringsbesluten måste vara konsekventa mellan jämförelser och likvärdiga objekt måste bilda konsekventa grupper. Apple dokumenterar dessa krav för sorted(by:). Predikatkrav

Exemplet kontrollerar om en tidigare nyckel skiljer sig innan det returnerar jämförelseresultatet. Det når titeln bara när grupp och effektiv position är lika, och når sedan identifieraren bara när även titeln är lika. Om identifierarna och alla tidigare nycklar är lika returnerar den avslutande strikta jämförelsen false.

Undvik att sammanfoga oberoende fältjämförelser med ||. Ett objekt kan ha en tidigare grupp men en senare position, medan ett annat kan ha motsatt kombination. Ett OR-baserat predikat kan då säga att vart och ett av objekten ska komma före det andra. Sekventiell hantering av skiljelikheter förhindrar den motsägelsen.

Undvik också att använda <= som den sista jämförelsen: lika värden skulle returnera true, vilket bryter mot regeln att ett objekt inte kan komma före sig självt. Låt varje jämförelse bero enbart på de angivna värdena och de fasta sorteringsreglerna.

Välj mellan stabila skiljelikheter och en avslutande identifierare

Swifts dokumenterade sortering är stabil: element som predikatet bedömer som likvärdiga behåller sin relativa ordning i indata. Om det är det avsedda beteendet utelämnar du den avslutande identifierarjämförelsen och returnerar false när titlarna är lika. Garanti om stabil sortering

Det här exemplet använder i stället unika syntetiska identifierare för att avgöra återstående skiljelikheter. Därför ger även en omvänd indata samma sekvens av identifierare. Den egenskapen följer av reglerna och exempelvärdena. Om identifierare upprepas tillsammans med alla andra nycklar är objekten likvärdiga och behåller sin ordning i indata.

Uttryck samma regler med Foundation-jämförare baserade på nyckelvägar

Foundation tillhandahåller KeyPathComparator för att jämföra egenskaper och sorted(using:) för att tillämpa en sekvens av jämförare. Den första jämföraren anger den primära ordningen; efterföljande jämförare avgör skiljelikheter. Dessa API:er finns från och med macOS 12. KeyPathComparator, sorted(using:)

I en motsvarande Foundation-version visar du två positionsnycklar: en rang för saknat värde och en numerisk position. Rangen placerar saknade positioner sist. När rangen är lika kan en numerisk ersättning för saknade positioner inte sammanfalla med en befintlig nolla, eftersom objekten redan har olika rang.

Lägg till följande tillägg och beräkning efter hela programmet ovan. Det här är ett beroende exempel som använder de befintliga definitionerna av ShapeItem, items och expectedIDs. Det kräver macOS 14 eller senare eftersom det uttryckligen skapar ComparableComparator med init(order:). Tillgänglighet för initieraren

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))
}

Den uttryckliga titeljämföraren bevarar det tidigare valet av Swifts strängordning. Foundation dokumenterar att ComparableComparator använder den jämförda typens Comparable-implementation, och att KeyPathComparator accepterar en angiven egenskapsjämförare. ComparableComparator, Initierare för nyckelvägsjämförare

För en fast ordning gör avslutningen varje gren synlig. En jämförararray är användbar när du vill representera ordningen för de nycklar som stöds som värden. I båda lösningarna bör du behålla valet av reservvärde i modellens beräknade egenskap, så att båda implementationerna använder samma definition.

Anpassa reglerna utan att ändra deras betydelse

Om positionerna ska sorteras fallande medan saknade positioner fortfarande placeras sist ändrar du bara jämförelsen mellan två befintliga positioner till left > right. I Foundation-versionen vänder du endast på numericPositionKey; låt missingPositionRank vara framåtriktad. Om du vände på hela resultatet skulle även grupper, titlar, identifierare och placeringen av saknade värden vändas.

Om en valfri sträng blir en reservnyckel avgör du separat om en tom sträng ska räknas som saknad. Nil-sammanslagning reagerar på nil; den ersätter inte en befintlig tom sträng. Bestäm eventuella ytterligare normaliseringsregler före sorteringen och använd samma effektiva nyckel genomgående. Beteende för nil-sammanslagning

Håll den här lilla beräkningen synkron. Swifts samtidighetsguide skiljer vanlig synkron sortering från suspenderingspunkter, och det dokumenterade predikatet för sorted(by:) är synkront. Om en applikation behöver värden från asynkront arbete hämtar du dem innan du anropar sorteraren. Att lägga till await i jämförelseavslutningen är inte en stödd utökning av dess signatur. Samtidighet, Signatur för sorted(by:)

Innan du anpassar exemplet till en annan skrivbordslista bör du behålla kontroller för en saknad föredragen nyckel, två saknade positionsnycklar, en giltig nolla, lika effektiva positioner och helt lika visningsfält. Bestäm om den sista skiljelikheten ska bevara indatans ordning eller använda en identifierare. De valen ger listan tydliga sorteringsregler som är begripliga i båda implementationerna.

Källor