Sorgen Sie dafür, dass benutzerdefinierte Swift-Gleichheit und Hash-Berechnung dieselbe unveränderliche Wertdefinition verwenden. In dieser Anleitung erstellen Sie einen lokalen Kachelschlüssel, bei dem Groß- und Kleinschreibung im Namen ignoriert werden, die Abmessungen jedoch exakt übereinstimmen müssen. Anschließend prüfen Sie Gleichheit, Mengenmitgliedschaft und den Wörterbuchzugriff.

Genau festlegen, wann zwei Kacheln gleich sind

Angenommen, Sie erstellen eine kleine Grafikübung für den Mac-Desktop. Sie müssen synthetische Kachelbeschreibungen anhand von Name, Breite und Höhe gruppieren. Die Aufgabe besteht darin, diese Gruppierung konsistent umzusetzen: Orbit und ORBIT mit denselben Abmessungen sollen gleiche Schlüssel ergeben; eine Änderung einer der beiden Abmessungen soll einen anderen Schlüssel ergeben.

Legen Sie den Vertrag fest, bevor Sie eines der beiden Protokolle implementieren. In diesem Beispiel sind zwei Schlüssel gleich, wenn ihre Namen nach Umwandlung der Groß- und Kleinschreibung sowie ihre Breiten und Höhen übereinstimmen. Die Abmessungen sind positive ganzzahlige Zeichnungseinheiten. Vorangestellte und nachgestellte Leerzeichen bleiben relevant. Dies ist eine beispielhafte Anwendungsregel und keine von Swift vorgegebene Anforderung.

Synthetische Eingaben Erwartete Beziehung Grund
Orbit, 40 × 20 und ORBIT, 40 × 20 Gleich Groß- und Kleinschreibung wird ignoriert
Orbit, 40 × 20 und orbit, 41 × 20 Ungleich Die Breite fließt in den Gleichheitsvergleich ein
Orbit, 40 × 20 und orbit, 40 × 21 Ungleich Die Höhe fließt in den Gleichheitsvergleich ein
Orbit, 40 × 20 und orbit , 40 × 20 Ungleich Das nachgestellte Leerzeichen bleibt erhalten

Verwenden Sie einen eigenen Schlüssel, wenn eine bestimmte Gruppierungsregel von den vollständigen Werten einer angezeigten Kachel abweicht. Bewahren Sie die ursprüngliche Schreibweise für die Anzeige im umgebenden Präsentationsmodell auf. So erklären Sie nicht zwei vollständige Kachelbeschreibungen für austauschbar, obwohl ihre sichtbaren Namen unterschiedliche Werte enthalten. Die Swift-Dokumentation zu Equatable beschreibt Gleichheit im Hinblick auf Ersetzbarkeit und fordert Reflexivität, Symmetrie und Transitivität. Equatable

Für Gleichheit und Hash-Berechnung dieselben Komponenten verwenden

Swifts Protokoll Hashable erbt von Equatable. Die wesentliche Anforderung lautet, dass gleiche Instanzen dem Hasher dieselben Werte in derselben Reihenfolge übergeben. Hashable-Werte können als Elemente von Mengen und als Wörterbuchschlüssel dienen. Hashable

Ein häufiger Fehler besteht darin, normalisierte Namen in == zu vergleichen, beim Hashen aber die ursprüngliche Schreibweise zu verwenden. Dann haben die Namen in den beiden Methoden unterschiedliche Bedeutungen. Ein weiterer Fehler ist, die Breite beim Gleichheitsvergleich auszulassen, sie aber beim Hashen einzubeziehen. Bei beiden Entwürfen können als gleich erklärte Werte unterschiedliche Eingaben für die Hash-Berechnung liefern. Apple weist ausdrücklich darauf hin, in beiden Methoden dieselben relevanten Eigenschaften zu verwenden. Adopting Common Protocols

Verlagern Sie für dieses Beispiel die Gleichheitskomponenten in eine private verschachtelte Struktur. Lassen Sie Swift die Gleichheit und Hash-Berechnung dieser Struktur synthetisieren und delegieren Sie dann beide äußeren Operationen an sie. So ist die Liste der Komponenten an einer einzigen Stelle definiert und kann leicht überprüft werden. Swift unterstützt die synthetisierte Protokollkonformität für Strukturen, deren gespeicherte Eigenschaften die erforderlichen Protokolle erfüllen. Protocols — Adopting a Protocol Using a Synthesized Implementation

Wenn Ihr Typ bereits genau die Gleichheitskomponenten speichert, genügt in der Regel die synthetisierte Hashable-Konformität. Die expliziten äußeren Methoden unten zeigen, wie sich eine vorhandene benutzerdefinierte Implementierung abstimmen lässt, ohne zwei voneinander unabhängige Feldlisten pflegen zu müssen.

Das vollständige lokale Beispiel auf dem Mac ausführen

Sie benötigen eine installierte Swift-Toolchain mit Foundation. Prüfen Sie sie in Terminal mit swift --version, wie in Swifts Dokumentation zum Einstieg empfohlen. Falls Swift nicht verfügbar ist, folgen Sie zunächst der offiziellen Installationsanleitung für macOS. Getting Started, Install Swift — macOS

Der folgende vollständige Shell-Befehl enthält das Swift-Programm. Fügen Sie den gesamten Block in Terminal ein. Das Programm wird über die Standardeingabe übergeben; das Beispiel führt nur Berechnungen im Speicher aus und gibt Ergebnisse aus. Der Swift-Code wurde überprüft, konnte in der Rechercheumgebung jedoch nicht ausgeführt werden, weil Swift dort nicht verfügbar war.

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

Erwartete Ausgabe:

Unique keys: 4
Equivalent key found: true
Pattern: striped
Sample contract checks passed

Die sechs Eingaben ergeben vier Gruppen: Die drei Varianten mit unterschiedlicher Groß- und Kleinschreibung teilen sich einen Schlüssel, während die abweichende Breite, die abweichende Höhe und die Variante mit nachgestelltem Leerzeichen jeweils eine eigene Gruppe bilden. Bei der Wörterbuchprüfung wird getestet, ob sich ein Wert, der a zugeordnet ist, mit dem gleichen Schlüssel b abrufen lässt. Diese Prüfungen testen das beabsichtigte Verhalten in Collections, ohne von einem bestimmten numerischen Hashwert abzuhängen.

Der Initialisierer bricht bei nicht positiven Abmessungen absichtlich ab. Das ist für diese festgelegte synthetische Übung angemessen, da ungültige Abmessungen auf einen Programmierfehler hinweisen. Wenn eine Desktop-Oberfläche unvollständige Eingaben für Abmessungen zulässt, validieren Sie die Eingabe vor der Erstellung des Schlüssels oder ändern Sie den Initialisierer so, dass er einen optionalen Wert zurückgibt oder einen Fehler auslöst. Ersetzen Sie eine Abmessung nicht stillschweigend, denn dadurch ändert sich der Gleichheitsvertrag.

Textnormalisierung als ausdrückliche Regel festlegen

Der Normalisierungsschritt ruft Foundation-folding(options:locale:) über Swifts String-API auf. Die API gibt einen String zurück, auf den die angegebenen Faltungsoptionen angewendet wurden. Hier wird nur .caseInsensitive angefordert. StringProtocol.folding(options:locale:)

Foundation beschreibt die Faltung von Groß- und Kleinschreibung, Zeichenbreite und diakritischen Zeichen als separate Optionen. Außerdem wird erläutert, dass die Locale die Faltungsergebnisse beeinflussen kann und gefalteter Text für die Anzeige ungeeignet sein kann. Daher gibt das Beispiel eine feste Locale an und speichert das gefaltete Ergebnis nur im Schlüssel. Die synthetischen Namen sind bewusst einfach gehalten. Diese Locale-Wahl legt das Verhalten dieser Übung fest; sie ist keine allgemeingültige Empfehlung für jede Benennungsschnittstelle. NSString.folding(options:locale:)

Die wichtige Entwurfsentscheidung besteht darin, bei der Erstellung einmal zu normalisieren. Gleichheit und Hash-Berechnung verwenden anschließend das gespeicherte Ergebnis, sodass keine der beiden Operationen unabhängig eine Locale oder Transformation auswählt. Benötigt die Anwendung später eine andere Vergleichsregel, erstellen Sie Schlüssel nach dieser neuen Regel und bauen Sie die betroffene In-Memory-Collection neu auf.

Bevor Sie die Normalisierung ausweiten, ergänzen Sie Beispiele, die die gewünschten Gruppen voneinander abgrenzen. Sollen Leerzeichen relevant sein? Sollen Buchstaben mit Akzent unterschiedlich bleiben? Sollen sich Zeichenbreiten sichtbar unterscheidender Zeichen als derselbe Name gelten? Legen Sie diese Fragen als Produktregeln fest und implementieren Sie anschließend eine einzige Transformation, die von beiden Operationen verwendet wird.

Eine Abweichung erkennen, ohne Hashwerte als Beweis anzusehen

Verwenden Sie beim Übertragen dieses Entwurfs auf einen vorhandenen Typ die folgende Abfolge:

  1. Schreiben Sie ein Wertepaar auf, das gleich sein muss, und für jedes Feld, das Werte unterscheiden muss, ein Wertepaar, das ungleich sein muss.
  2. Verfolgen Sie die tatsächlichen Werte, die == vergleicht, einschließlich Normalisierungen und abgeleiteter Eigenschaften.
  3. Verfolgen Sie die Werte, die hasher.combine zugeführt werden, einschließlich ihrer Reihenfolge.
  4. Verlagern Sie die gemeinsame Definition in einen einzigen unveränderlichen Komponentenwert oder sorgen Sie dafür, dass beide Implementierungen ausdrücklich dieselben Komponenten verwenden.
  5. Prüfen Sie anhand derselben Testdaten die Gleichheitsgesetze, Hashes gleicher Werte, Mengenmitgliedschaft und Wörterbuchzugriff.

Implementieren Sie Gleichheit nicht durch den Vergleich von hashValue. Gleiche Werte müssen gleiche Hashes haben, aber übereinstimmende Hashes belegen keine Gleichheit. Die vollständigen Gleichheitskomponenten bleiben die Grundlage für ==. Ein Test, der für jedes ungleiche Paar unterschiedliche Hashes verlangt, fordert ebenfalls mehr, als der Vertrag zur Hash-Berechnung garantiert. Hashable

Vergleichen Sie Hashes zur Prüfung dieses Vertrags nur innerhalb derselben Programmausführung. Swifts Hasher verwendet eine zufällige Initialisierung, und sein Algorithmus kann sich zwischen Versionen der Standardbibliothek ändern. Eine gestern ausgegebene Ganzzahl ist daher kein geeigneter erwarteter Wert für die heutige Ausführung. Hasher

Die Prüfungen mit einer begrenzten Zahl von Beispielen sind nützliche Regressionstests, aber kein Beweis für jeden möglichen String und jede mögliche Abmessung. Auch die strukturelle Begründung ist wichtig: Beide äußeren Methoden delegieren an denselben unveränderlichen Components-Wert, dessen gespeicherte Felder den gewählten Vertrag vollständig ausdrücken.

Anforderungen an Nebenläufigkeit getrennt von Gleichheit behandeln

Das Beispiel deklariert beide Strukturen als Sendable, da ihre gespeicherten Werte Strings, Ganzzahlen und eine weitere sendbare Struktur sind. Swift dokumentiert, dass sendbare Strukturen sendbare gespeicherte Eigenschaften enthalten müssen. Sendbarkeit betrifft die Übertragung von Werten zwischen Nebenläufigkeitsdomänen; sie legt nicht fest, welche Werte als gleich gelten. Sendable, The Swift Programming Language — Concurrency

Diese lokale Berechnung benötigt weder Tasks noch Actors. Wird sie später Teil nebenläufigen Desktop-Codes, führen Sie Gleichheit und Hash-Berechnung weiterhin synchron anhand der gespeicherten Komponenten des Schlüssels aus. Betrachten Sie jede gemeinsam genutzte veränderliche Collection als separate Frage des Nebenläufigkeitsentwurfs. Der Sprachleitfaden beschreibt Actors und Isolation als Mechanismen zur Koordination des Zugriffs auf veränderlichen Zustand. The Swift Programming Language — Concurrency

Bei der Übertragung dieses Musters gibt es außerdem eine Foundation-Besonderheit: Eine NSObject-Unterklasse verwendet das geerbte Verhalten für Gleichheit und Hash-Berechnung. Apples unterstützter Anpassungsweg besteht darin, isEqual(_:) und hash gemeinsam zu überschreiben. Die einfache Swift-Struktur in diesem Beispiel verwendet == und hash(into:). Adopting Common Protocols

Für einen lokalen Kachelschlüssel besteht das wartbare Ergebnis aus einer schriftlich festgehaltenen Vergleichsregel, einer unveränderlichen Repräsentation dieser Regel und Prüfungen, die echte Collection-Zugriffe ausführen. Ändert sich die Definition, aktualisieren Sie die Komponentenrepräsentation und die zugehörigen Beispiele gemeinsam, damit Gleichheit und Hash-Berechnung weiterhin denselben Wert beschreiben.

Quellen