Låt anpassad Swift-jämförelse och hashning använda samma oföränderliga värdedefinition. Den här genomgången skapar en lokal bricknyckel vars namn ignorerar skillnader mellan gemener och versaler, medan måtten jämförs exakt, och kontrollerar sedan likhet, medlemskap i mängder och uppslagning i ordböcker.

Definiera exakt när två brickor är likadana

Anta att du bygger en liten grafisk övning för Mac. Du behöver gruppera syntetiska brickbeskrivningar efter namn, bredd och höjd. Uppgiften är att implementera grupperingen konsekvent: Orbit och ORBIT med samma mått ska ge likadana nycklar; om något av måtten ändras ska nyckeln bli en annan.

Skriv ner kontraktet innan du implementerar något av protokollen. I det här exemplet är två nycklar lika när deras namn, efter skiftlägesokänslig konvertering, samt deras bredd och höjd är lika. Måtten anges i positiva heltal för ritenheter. Inledande och avslutande blanksteg räknas. Det här är en illustrativ regel för tillämpningen, inte ett krav från Swift.

Syntetiska indata Förväntat förhållande Orsak
Orbit, 40 × 20 och ORBIT, 40 × 20 Lika Skiftläget ignoreras
Orbit, 40 × 20 och orbit, 41 × 20 Olika Bredden ingår i likhetsjämförelsen
Orbit, 40 × 20 och orbit, 40 × 21 Olika Höjden ingår i likhetsjämförelsen
Orbit, 40 × 20 och orbit , 40 × 20 Olika Det avslutande blanksteget bevaras

Använd en särskild nyckel när en viss grupperingsregel skiljer sig från det fullständiga värdet hos en visad bricka. Behåll den ursprungliga stavningen i visningsmodellen. Då slipper du hävda att två fullständiga brickbeskrivningar är utbytbara samtidigt som deras synligt olika namn ingår i värdet. Swifts dokumentation för Equatable beskriver likhet i termer av utbytbarhet och kräver reflexivitet, symmetri och transitivitet. Equatable

Använd samma komponenter för jämförelse och hashning

Swifts protokoll Hashable ärver från Equatable. Dess grundläggande krav är att lika instanser matar in samma värden i hasharen i samma ordning. Hashable-värden kan användas som element i mängder och som nycklar i ordböcker. Hashable

Ett vanligt misstag är att jämföra normaliserade namn i == men hasha den ursprungliga stavningen. Då har namnen olika betydelse i de två metoderna. Ett annat misstag är att utelämna bredden i likhetsjämförelsen men ta med den i hashningen. Båda konstruktionerna gör att värden som förklarats lika kan ge olika indata till hashningen. Apple anger uttryckligen att anpassade implementationer ska använda samma betydelsebärande egenskaper i båda metoderna. Adopting Common Protocols

I det här exemplet samlas likhetskomponenterna i en privat nästlad struktur. Låt Swift syntetisera den strukturens likhets- och hashningsfunktioner och delegera sedan båda operationerna för den yttre typen till den. Då finns komponentlistan samlad på ett enda ställe. Swift stöder syntetisk protokollanpassning för strukturer vars lagrade egenskaper uppfyller kraven för protokollen. Protocols — Adopting a Protocol Using a Synthesized Implementation

Om din typ redan bara lagrar komponenterna som ingår i likheten räcker det oftast att låta Swift syntetisera Hashable-anpassningen. De uttryckliga yttre metoderna nedan visar hur du kan hålla en befintlig anpassad implementation i linje utan att underhålla två separata fältlistor.

Kör hela det lokala exemplet på din Mac

Du behöver en installerad Swift-verktygskedja med Foundation. Kontrollera i Terminal med swift --version, enligt Swifts dokumentation för att komma igång. Om Swift saknas installerar du det enligt den officiella installationsguiden för macOS innan du fortsätter. Getting Started, Install Swift — macOS

Följande är ett komplett skalkommando som innehåller Swift-programmet. Klistra in hela blocket i Terminal. Programmet skickas via standardindata; exemplet utför enbart beräkningar i minnet och skriver ut resultat. Swift-koden granskades men kunde inte köras i undersökningsmiljön eftersom Swift saknades.

swift - <<'SWIFT'
import Foundation

struct TileKey: Hashable, Sendable {
    private struct Components: Hashable, Sendable {
        let foldedName: String
        let width: Int
        let height: Int
    }

    private let components: Components

    init(name: String, width: Int, height: Int) {
        precondition(width > 0 && height > 0,
                     "Tile dimensions must be positive")

        let foldedName = name.folding(
            options: [.caseInsensitive],
            locale: Locale(identifier: "en_US_POSIX")
        )
        components = Components(
            foldedName: foldedName,
            width: width,
            height: height
        )
    }

    static func == (lhs: TileKey, rhs: TileKey) -> Bool {
        lhs.components == rhs.components
    }

    func hash(into hasher: inout Hasher) {
        hasher.combine(components)
    }
}

let a = TileKey(name: "Orbit", width: 40, height: 20)
let b = TileKey(name: "ORBIT", width: 40, height: 20)
let c = TileKey(name: "orbit", width: 40, height: 20)
let wider = TileKey(name: "orbit", width: 41, height: 20)
let taller = TileKey(name: "orbit", width: 40, height: 21)
let spaced = TileKey(name: "orbit ", width: 40, height: 20)
let samples = [a, b, c, wider, taller, spaced]

precondition(a == b && b == c && a == c)
precondition(a != wider && a != taller && a != spaced)

// Check the equality laws over this finite sample.
for first in samples {
    precondition(first == first, "Reflexivity failed")
    for second in samples {
        precondition((first == second) == (second == first),
                     "Symmetry failed")
        if first == second {
            precondition(first.hashValue == second.hashValue,
                         "Equal values have different hashes")
        }
        for third in samples where first == second && second == third {
            precondition(first == third, "Transitivity failed")
        }
    }
}

let uniqueKeys = Set(samples)
precondition(uniqueKeys.count == 4)
precondition(uniqueKeys.contains(b))

let patterns = [a: "striped"]
precondition(patterns[b] == "striped")

print("Unique keys:", uniqueKeys.count)
print("Equivalent key found:", uniqueKeys.contains(b))
print("Pattern:", patterns[b] ?? "missing")
print("Sample contract checks passed")
SWIFT

Förväntad utdata:

Unika nycklar: 4
Likvärdig nyckel hittades: true
Mönster: randigt
Exempelkontrollerna av kontraktet godkändes

De sex indataobjekten bildar fyra grupper: de tre varianterna med olika skiftläge delar en nyckel, medan varianten med ändrad bredd, den med ändrad höjd och varianten med avslutande blanksteg får varsin nyckel. Ordbokskontrollen undersöker om ett värde som är kopplat till a kan hämtas med den likvärdiga nyckeln b. Kontrollerna prövar det avsedda beteendet hos samlingarna utan att vara beroende av ett visst numeriskt hashvärde.

Initialiseraren avbryter avsiktligt för mått som inte är positiva. Det passar den här fasta, syntetiska övningen, där ogiltiga mått tyder på ett programmeringsfel. Om ett skrivbordsgränssnitt tillåter att mått lämnas ofullständiga validerar du indata innan nyckeln skapas, eller ändrar initialiseraren så att den returnerar ett valfritt värde eller kastar ett fel. Ersätt inte måttet tyst, eftersom det ändrar likhetskontraktet.

Gör textnormaliseringen till en uttrycklig regel

Normaliseringssteget anropar Foundation-funktionen folding(options:locale:) via Swifts sträng-API. API:et returnerar en sträng där de angivna konverteringsalternativen har tillämpats. Här anges bara .caseInsensitive. StringProtocol.folding(options:locale:)

Foundation dokumenterar konvertering av skiftläge, teckenbredd och diakritiska tecken som separata val. Dokumentationen förklarar också att språkområdet kan ändra resultatet och att konverterad text kan vara olämplig för visning. Exemplet anger därför ett fast språkområde och lagrar det konverterade resultatet enbart i nyckeln. De syntetiska namnen är avsiktligt enkla. Valet av språkområde definierar beteendet i den här övningen; det är ingen universell rekommendation för alla namngivningsgränssnitt. NSString.folding(options:locale:)

Det användbara designbeslutet är att normalisera en gång när nyckeln skapas. Jämförelse och hashning använder därefter det lagrade resultatet, så ingen av operationerna väljer självständigt ett språkområde eller en omvandling. Om programmet senare behöver en annan jämförelsepolicy skapar du nycklar enligt den nya policyn och bygger om den berörda samlingen i minnet.

Innan du utökar normaliseringen lägger du till exempel som skiljer de önskade grupperna åt. Ska blanksteg räknas? Ska bokstäver med accent behandlas som olika? Ska visuellt olika teckenbredder räknas som samma namn? Besvara frågorna som produktregler och implementera sedan en enda omvandling som används av båda operationerna.

Undersök en avvikelse utan att förlita dig på hashvärden

Följ den här ordningen när du anpassar designen till en befintlig typ:

  1. Skriv ett par som måste jämföras som lika och ett par för varje fält som ska skilja värden åt.
  2. Följ de faktiska värden som jämförs av ==, inklusive normalisering och härledda egenskaper.
  3. Följ värdena som skickas till hasher.combine, inklusive deras ordning.
  4. Samla den gemensamma definitionen i ett oföränderligt komponentvärde, eller se till att båda implementationerna uttryckligen använder samma komponenter.
  5. Kontrollera likhetslagarna, hashningen av lika värden, medlemskap i mängder och uppslagning i ordböcker med samma testdata.

Implementera inte likhet genom att jämföra hashValue. Lika värden måste ha lika hashvärden, men ett matchande hashvärde bevisar inte likhet. De fullständiga likhetskomponenterna är fortfarande grunden för ==. Ett test som kräver att alla olika par ska få olika hashvärden kräver också mer än vad hashningskontraktet garanterar. Hashable

Jämför hashvärden bara inom den aktuella körningen när du kontrollerar kontraktet. Swifts Hasher använder en slumpmässig initiering, och algoritmen kan ändras mellan versioner av standardbiblioteket. Ett utskrivet heltal från gårdagens körning fungerar därför inte som förväntat resultat för dagens körning. Hasher

De ändliga exempelkontrollerna är användbara regressionstester, inte ett bevis för varje tänkbar sträng och varje mått. Även det strukturella resonemanget är viktigt: båda yttre metoderna delegerar till samma oföränderliga värde av typen Components, vars lagrade fält fullt ut uttrycker det valda kontraktet.

Håll krav på samtidighet åtskilda från likhet

Exemplet deklarerar båda strukturerna som Sendable eftersom deras lagrade värden är strängar, heltal och en annan struktur som är Sendable. Swift anger att Sendable-strukturer måste innehålla lagrade egenskaper som också är Sendable. Sendable handlar om att överföra värden mellan samtidighetsdomäner; det definierar inte vilka värden som är lika. Sendable, The Swift Programming Language — Concurrency

Den här lokala beräkningen behöver inga uppgifter eller aktörer. Om den senare blir en del av samtidig skrivbordskod låter du jämförelse och hashning förbli synkrona operationer på nyckelns lagrade komponenter. Behandla en delad, föränderlig samling som en separat fråga om samtidighetsdesign. Språkhandboken beskriver aktörer och isolering som mekanismer för att samordna åtkomst till föränderligt tillstånd. The Swift Programming Language — Concurrency

Det finns också en gräns mot Foundation när mönstret anpassas: en underklass till NSObject använder ärvda funktioner för likhet och hashning. Apples stödda sätt att anpassa dem är att åsidosätta isEqual(_:) och hash tillsammans. Den vanliga Swift-strukturen i det här exemplet använder == och hash(into:). Adopting Common Protocols

För en lokal bricknyckel ger en nedskriven jämförelseregel, en oföränderlig representation av regeln och kontroller som utför faktiska uppslagningar i samlingar en lättunderhållen lösning. När definitionen ändras uppdaterar du komponentrepresentationen och exemplen tillsammans, så att jämförelse och hashning fortsätter att beskriva samma värde.

Källor