Um synthetische Swift-Werte eindeutig zu halten, ohne ihre Einfügereihenfolge zu ändern, durchlaufe das Eingabearray einmal, erfasse akzeptierte Schlüssel in einer Menge und füge nur erste Vorkommen einem Ausgabearray hinzu. Diese Anleitung erstellt eine kleine lokale Berechnung, die du auf einem Mac ausführen kannst.

Die Aufgabe besteht darin, eine geordnete Liste synthetischer Formwerte zu verarbeiten, wiederholte Werte zu entfernen und aus den beibehaltenen Einträgen eine Summe zu berechnen. Alles geschieht im Arbeitsspeicher; das Beispiel liest keine externen Eingaben und speichert keine Berechnungsergebnisse.

Definiere, was „eindeutig“ bedeutet, bevor du eine Collection auswählst

Swift-Arrays erhalten die Reihenfolge und erlauben Duplikate. Mengen enthalten eindeutige Werte, haben aber keine definierte Reihenfolge. Daher bleibt beim Umwandeln eines Arrays in eine Menge und anschließend zurück in ein Array die Reihenfolge des ersten Auftretens nicht erhalten. Eine Sortierung legt danach eine Sortierreihenfolge fest, die von der Einfügereihenfolge abweichen kann. Diese Eigenschaften der Collections sind in Swift unter Collection Types dokumentiert.

In diesem Beispiel hat jeder synthetische Wert einen Formnamen und eine Größe. Der Standardvertrag behandelt zwei Einträge nur dann als Duplikate, wenn beide Eigenschaften übereinstimmen. Ein Kreis mit der Größe 12 und ein Kreis mit der Größe 20 bleiben daher getrennte Werte.

Vertragsfrage Regel für diese Berechnung
Was bestimmt die Eingabereihenfolge? Die Position im bereitgestellten Array
Wann sind zwei Werte Duplikate? Gleiche Form und gleiche Größe
Welches Vorkommen bleibt erhalten? Das erste Vorkommen
Verschiebt ein späteres Duplikat den beibehaltenen Wert? Nein
Was geschieht bei leerer Eingabe? Ein leeres Array und eine Summe von null zurückgeben
Welche Werte gehen in die Summe ein? Nur die beibehaltenen Werte

Dieser Vertrag verhindert einen leicht zu machenden Fehler: Einträge anhand des Formnamens zu entfernen, obwohl die Berechnung unterschiedliche Kombinationen aus Form und Größe benötigt. Das durchgearbeitete Beispiel berechnet auch das Ergebnis nur nach Form, damit du die Auswirkung einer geänderten Regel sehen kannst.

Erstelle ein vollständiges lokales Swift-Beispiel auf deinem Mac

Du benötigst eine installierte Swift-Toolchain. Führe in Terminal swift --version aus, um zu prüfen, ob der Befehl verfügbar ist. Das offizielle Swift-Tutorial Build a Command-line Tool beschreibt diese Prüfung, das Erstellen eines ausführbaren Pakets und das Ausführen eines Pakets.

Erstelle ein neues Verzeichnis für das Beispiel und initialisiere ein ausführbares Paket:

mkdir OrderDemo
cd OrderDemo
swift package init --name OrderDemo --type executable

Ersetze die generierte ausführbare Quelldatei unter Sources durch das vollständige Programm unten. Der generierte Dateiname und die Verzeichnisstruktur können je nach Toolchain variieren; ersetze den vorhandenen Einstiegspunkt, statt einen zweiten hinzuzufügen. Dieses Programm verwendet einen @main-Einstiegspunkt. Lege es daher in einer Datei namens OrderDemo.swift ab und benenne gegebenenfalls eine generierte Datei main.swift um. Das Swift-Befehlszeilen-Tutorial erläutert die Wahl zwischen einer Datei main.swift und einem @main-Einstiegspunkt.

Imports oder Paketabhängigkeiten sind nicht erforderlich. Das Programm verwendet Typen und Funktionen aus der Standardbibliothek.

struct Tile: Hashable {
    let shape: String
    let size: Int
}

func uniqueFirst<Value, Key: Hashable>(
    _ values: [Value],
    by key: (Value) -> Key
) -> [Value] {
    var seen: Set<Key> = []
    var result: [Value] = []

    for value in values {
        if seen.insert(key(value)).inserted {
            result.append(value)
        }
    }

    return result
}

@main
struct OrderDemo {
    static func main() {
        let input = [
            Tile(shape: "circle", size: 12),
            Tile(shape: "square", size: 8),
            Tile(shape: "circle", size: 12),
            Tile(shape: "circle", size: 20),
            Tile(shape: "square", size: 8)
        ]

        let retained = uniqueFirst(input, by: { $0 })
        let total = retained.reduce(0) { $0 + $1.size }
        let skipped = input.count - retained.count

        let expected = [
            Tile(shape: "circle", size: 12),
            Tile(shape: "square", size: 8),
            Tile(shape: "circle", size: 20)
        ]
        precondition(retained == expected)
        precondition(total == 40)
        precondition(skipped == 2)

        let empty: [Tile] = []
        precondition(uniqueFirst(empty, by: { $0 }).isEmpty)
        precondition(
            uniqueFirst([input[0], input[0]], by: { $0 })
                == [input[0]]
        )

        let byShape = uniqueFirst(input, by: { $0.shape })
        precondition(byShape == [input[0], input[1]])
        let shapeTotal = byShape.reduce(0) { $0 + $1.size }
        precondition(shapeTotal == 20)

        for tile in retained {
            print("\(tile.shape): \(tile.size)")
        }
        print("Total: \(total)")
        print("Skipped duplicates: \(skipped)")
        print("Shape-only total: \(shapeTotal)")
    }
}

Führe im Paketverzeichnis Folgendes aus:

swift run OrderDemo

Die erwartete Programmausgabe nach eventuellen Build-Meldungen ist unten dargestellt. Das Programm wurde anhand der abgerufenen API-Dokumentation geprüft, hier jedoch nicht ausgeführt, da in dieser Rechercheumgebung kein Swift-Executable vorhanden ist.

circle: 12
square: 8
circle: 20
Total: 40
Skipped duplicates: 2
Shape-only total: 20

Warum erhält der Ansatz mit Array und Menge die Reihenfolge?

Das Array ist die alleinige Quelle für die Ausgabereihenfolge. Die Schleife besucht jede Eingabeposition der Reihe nach, und append(_:) fügt einen akzeptierten Wert am Ende des Ergebnisses hinzu. Die Menge prüft nur, ob der ausgewählte Schlüssel bereits vorgekommen ist; das Programm durchläuft sie niemals.

Der entscheidende Ausdruck ist seen.insert(key(value)).inserted. Swift dokumentiert, dass insert(_:) für ein neues Element inserted als true und für ein bereits vorhandenes gleiches Element false zurückgibt. Ein vorhandenes Element bleibt unverändert. Mit diesem Rückgabewert lässt sich in einem Schritt entscheiden, ob der ursprüngliche Wert angefügt werden soll. Siehe Apples Dokumentation zum Einfügen in eine Set.

Bei der bereitgestellten Eingabe werden der erste Kreis und das erste Quadrat akzeptiert. Der zweite identische Kreis wird übersprungen. Der größere Kreis wird akzeptiert, weil sich sein vollständiger Wert unterscheidet, und das wiederholte Quadrat wird übersprungen. Die beibehaltenen Größen sind daher 12 + 8 + 20 = 40.

Die Implementierung hält eine nützliche Invariante ein: Nach jeder Iteration enthält das Ergebnis genau den zuerst angetroffenen Wert für jeden Schlüssel in seen, und zwar in der Reihenfolge, in der die Werte aufgetreten sind. Ein neuer Schlüssel erweitert beide Collections; ein wiederholter Schlüssel ändert keine von beiden. Diese Überlegung erklärt die Reihenfolge unabhängig davon, wie die Menge ihre Elemente speichert.

Wie sollten benutzerdefinierte Swift-Werte Gleichheit ausdrücken?

Die Deklaration struct Tile: Hashable fordert eine synthetisierte Konformität an. Beide gespeicherten Eigenschaften, String und Int, sind hashbar, sodass Swift das erforderliche Verhalten synthetisieren kann. Apple dokumentiert diese Bedingungen für die Synthese unter Hashable.

Wenn du { $0 } als Schlüssel verwendest, bestimmt der gesamte Wert vom Typ Tile die Eindeutigkeit. { $0.shape } zu verwenden, ändert den Vertrag so, dass ein Eintrag pro Form beibehalten wird. Im Beispiel wird dadurch der spätere Kreis der Größe 20 entfernt und die Summe auf 20 reduziert. Das ist ein gezielter Vergleich zweier Regeln und keine zweite Implementierung derselben Regel.

Sorge dafür, dass die Schlüsselauswahl während der Berechnung deterministisch bleibt. Leite den Schlüssel aus dem gerade verarbeiteten Wert ab und nicht aus einem Zähler oder einer veränderlichen externen Variable. Andernfalls könnten gleiche Eingaben unterschiedliche Schlüssel erhalten und als getrennte Einträge erhalten bleiben.

Wenn du Gleichheit und Hashing manuell implementierst, verwende in beiden dieselben wesentlichen Eigenschaften. Apple verlangt, dass gleiche Instanzen dem Hasher dieselben Werte in derselben Reihenfolge übergeben. Verwende hashValue selbst nicht als Eindeutigkeitsschlüssel: Ein Hash ersetzt nicht den Gleichheitsvertrag des Werts. Wenn du den tatsächlich hashbaren Schlüssel beibehältst, kann die Menge diesen Vertrag anwenden. Siehe Konformitätsanforderungen für Hashable.

Wann solltest du stattdessen eine geordnete Collection verwenden?

Für diese einzelne Berechnung macht ein Array zusammen mit einer lokalen Menge das Verhalten klar. Wenn eine Anwendung wiederholt eindeutige Elemente anhängt, entfernt, umordnet und abfragt, kann eine speziell dafür vorgesehene geordnete Collection die Wartung dieser Operationen erleichtern.

Auswahl Geeignete Bedingung Relevante Einschränkung
Array plus Menge Ein Durchlauf über synthetische Swift-Werte, möglicherweise mit einem ausgewählten Schlüssel Deine Funktion legt das Verhalten beim ersten Vorkommen fest
Swift Collections OrderedSet Laufende Verwendung einer geordneten Collection eindeutiger hashbarer Werte Erfordert das separate Paket Swift Collections
Foundation NSOrderedSet Eine API arbeitet bereits mit geordneten Foundation-Objekt-Collections Eine Klasse für eine statische geordnete Collection; veränderbare Operationen verwenden NSMutableOrderedSet

Die offizielle Swift-Collections-Dokumentation beschreibt OrderedSet als geordnete Collection eindeutiger Elemente, die über OrderedCollections importiert wird. Sie bietet positionsbezogene Operationen wie append(_:) und insert(_:at:). Das sind Paket-APIs. Daher reicht es nicht aus, lediglich import OrderedCollections zu ergänzen, wenn das Projekt ansonsten keine Abhängigkeiten hat. Siehe OrderedSet.

Apples Foundation-Dokumentation beschreibt NSOrderedSet als statische geordnete Collection unterschiedlicher Objekte und nennt NSMutableOrderedSet als veränderbares Gegenstück. Für die native Swift-Werteberechnung oben drückt die generische Funktion den gewählten Gleichheitsschlüssel direkt aus, ohne diese Objekt-Collection einzuführen. Siehe NSOrderedSet.

Was ändert sich, wenn die Berechnung eine Desktop-Oberfläche versorgt?

Das vollständige Beispiel ist synchron und verwaltet seine temporären Collections innerhalb der Funktion. Für eine kleine synthetische Liste ergibt sich so eine klare Verarbeitungsreihenfolge, ohne Tasks einzuführen.

Isoliere in einer macOS-Oberfläche den zustandsbezogenen Code und Aktualisierungen der Oberfläche angemessen. Apple dokumentiert MainActor als globalen Actor, dessen Executor der Haupt-Dispatch-Queue entspricht; Swifts Leitfaden zur Nebenläufigkeit erklärt, wie @MainActor auf Funktionen und Typen angewendet wird, die auf Oberflächenzustand zugreifen. Siehe MainActor und Concurrency.

Nebenläufigkeit erfordert auch eine ausdrückliche Definition der Reihenfolge. Swift garantiert keine Abschlussreihenfolge der Child-Tasks in einer Task Group. Werden Ergebnisse angefügt, sobald Tasks fertig sind, bleibt daher die Abschlussreihenfolge erhalten, die von der ursprünglichen Eingabereihenfolge abweichen kann. Der offizielle Leitfaden zur Nebenläufigkeit dokumentiert diese Einschränkung.

Wenn eine künftige Version unabhängige synthetische Berechnungen nebenläufig ausführt, übergib jede Eingabeposition zusammen mit ihrem Ergebnis, stelle nach dem Sammeln die Reihenfolge der Eingabepositionen wieder her und führe dann den Eindeutigkeitsdurchlauf aus. Dieses Design ergibt sich aus der dokumentierten Einschränkung bezüglich der Abschlussreihenfolge. Halte das aktuelle kleine Beispiel synchron, solange zusätzliche Arbeit keine weitere Koordination rechtfertigt.

Prüfe den Vertrag, bevor du die Funktion wiederverwendest

Das Programm enthält Prüfungen für die genaue beibehaltene Reihenfolge, leere Eingabe, wiederholte identische Werte und die alternative Regel nur nach Form. Diese Prüfungen betreffen die Entscheidungen, die die Bedeutung der Berechnung am ehesten ändern können.

Prüfe bei der Anpassung drei Bedingungen: Die Eingabe muss bereits eine sinnvolle Reihenfolge haben, der Schlüssel muss die beabsichtigte Gleichheitsregel ausdrücken, und es muss angemessen sein, das erste Vorkommen beizubehalten. Die Funktion überspringt bewusst spätere übereinstimmende Einträge; sie führt deren Größen weder zusammen noch ersetzt sie den beibehaltenen Wert. Die angegebene Anzahl übersprungener Einträge macht diesen Effekt sichtbar.

Für die bereitgestellte synthetische Formenliste besteht das fertige Ergebnis aus drei Werten in der Reihenfolge ihres ersten Auftretens und einer Summe von 40. Lass das Array für die Reihenfolge zuständig sein, die Menge für die Zugehörigkeitsprüfung und den ausgewählten Schlüssel dafür, was „eindeutig“ bedeutet.

Quellen