Um aufeinanderfolgende Swift-Enum-Fälle unter Ignorieren zugeordneter Werte zu unterdrücken, vergleiche einen separaten Fall-Schlüssel und behalte den ersten vollständigen Wert jedes Durchlaufs bei. Dieses lokale Mac-Beispiel erhält die Reihenfolge und lässt einen Fall erneut auftreten, nachdem ein anderer Fall dazwischenliegt.
Genau festlegen, was als Duplikat gilt
Angenommen, eine Desktop-Grafikvorschau erzeugt eine synthetische Folge von Schritten: Leerlauf, Zeichnen einer benannten Form und Anzeigen einer benannten Vorschau. Du möchtest eine kompakte Liste der Fallübergänge. Zwei aufeinanderfolgende Zeichenschritte gelten als Duplikate, selbst wenn sich die Namen der Formen unterscheiden.
Swift-Enums können für jeden Fall unterschiedliche zugeordnete Werte enthalten. Diese Werte sind Teil des Enum-Werts. Wenn du sie ignorieren möchtest, musst du eine explizite Vergleichsregel festlegen. Der Swift-Sprachleitfaden erläutert diesen Unterschied unter Enumerations.
Für diese Aufgabe gilt folgender Vertrag:
- Den ersten Eingabewert beibehalten.
- Jeden späteren Wert unterdrücken, dessen Fall mit dem Fall des vorherigen Durchlaufs übereinstimmt.
- Den vollständigen zugeordneten Wert jedes beibehaltenen Elements erhalten.
- Einen Fall erneut beibehalten, wenn ihm ein anderer Fall vorausgeht.
- Die Eingabereihenfolge erhalten; eine leere Eingabe ergibt ein leeres Ergebnis.
Hier ist das beabsichtigte Verhalten für eine beispielhafte Eingabe. Alle Namen sind synthetisch.
| Eingabewert | Entscheidung | Grund |
|---|---|---|
.idle |
Beibehalten | Erster Wert |
.drawing("Circle") |
Beibehalten | Fall geändert |
.drawing("Square") |
Unterdrücken | Zeichnen läuft weiterhin |
.ready("Preview A") |
Beibehalten | Fall geändert |
.ready("Preview B") |
Unterdrücken | Bereit-Status besteht weiterhin |
.drawing("Triangle") |
Beibehalten | Zeichnen folgt auf Bereit |
.idle |
Beibehalten | Fall geändert |
.idle |
Unterdrücken | Leerlauf besteht weiterhin |
Die beibehaltenen Zeichnungswerte sind also \"Circle\" und \"Triangle\". Der Wert \"Square\" wird absichtlich ausgeschlossen. Wenn die Desktop-Ansicht jeden aktualisierten Formnamen anzeigen muss, würden mit diesem Vertrag Änderungen verworfen, die die Ansicht benötigt; verwende für diesen Anwendungsfall einen payload-sensitiven Vergleich.
Ein vollständiges lokales Swift-Beispiel auf deinem Mac ausführen
Verwende einen Mac mit einer funktionierenden Swift-Entwicklungsumgebung. Swift.org empfiehlt Xcode für die Entwicklung auf Apple-Plattformen und dokumentiert einen Workflow für ausführbare Befehlszeilenprogramme mit Swift Package Manager. Folge bei Bedarf den Anweisungen unter Install Swift — macOS, um die Werkzeuge einzurichten.
Erstelle im Terminal in einem neuen Arbeitsverzeichnis ein kleines ausführbares Paket:
mkdir EnumCaseDemo
cd EnumCaseDemo
swift package init --name EnumCaseDemo --type executable
Ersetze den Inhalt der generierten Quelldatei des ausführbaren Programms durch das folgende vollständige Programm. Das Programm verwendet Typen aus der Swift-Standardbibliothek und benötigt weder Imports noch Paketabhängigkeiten. Ein- und Ausgabe bleiben im Arbeitsspeicher; ausgegeben werden die beibehaltenen synthetischen Schritte.
enum PreviewStep: Equatable {
case idle
case drawing(String)
case ready(String)
enum Kind: Equatable {
case idle
case drawing
case ready
}
var kind: Kind {
switch self {
case .idle:
return .idle
case .drawing(_):
return .drawing
case .ready(_):
return .ready
}
}
var label: String {
switch self {
case .idle:
return "idle"
case .drawing(let shape):
return "drawing: \(shape)"
case .ready(let name):
return "ready: \(name)"
}
}
}
func suppressConsecutiveCases(
_ steps: [PreviewStep]
) -> [PreviewStep] {
var result: [PreviewStep] = []
var previousKind: PreviewStep.Kind? = nil
for step in steps {
let currentKind = step.kind
if let previous = previousKind,
previous == currentKind {
continue
}
result.append(step)
previousKind = currentKind
}
return result
}
let input: [PreviewStep] = [
.idle,
.drawing("Circle"),
.drawing("Square"),
.ready("Preview A"),
.ready("Preview B"),
.drawing("Triangle"),
.idle,
.idle
]
let expected: [PreviewStep] = [
.idle,
.drawing("Circle"),
.ready("Preview A"),
.drawing("Triangle"),
.idle
]
let output = suppressConsecutiveCases(input)
precondition(output == expected)
precondition(suppressConsecutiveCases([]).isEmpty)
precondition(
suppressConsecutiveCases([
.drawing("Circle"), .drawing("Square")
]) == [.drawing("Circle")]
)
precondition(
suppressConsecutiveCases([
.idle, .drawing("Circle"), .idle
]) == [.idle, .drawing("Circle"), .idle]
)
for step in output {
print(step.label)
}
Führe das Paket in seinem Verzeichnis aus:
swift run EnumCaseDemo
Die erwartete Programmausgabe nach eventuellen Build-Meldungen lautet:
idle
drawing: Circle
ready: Preview A
drawing: Triangle
idle
Diese Beispiele wurden anhand der abgerufenen Dokumentation geprüft. Sie wurden hier nicht ausgeführt, da die Rechercheumgebung kein Swift-Programm enthält. Die enthaltenen Preconditions prüfen die erwarteten beibehaltenen Werte, eine leere Eingabe, aufeinanderfolgende Werte mit unterschiedlichen zugeordneten Werten sowie einen Fall, der nach einem Übergang erneut auftritt.
Warum ein separater Fall-Schlüssel funktioniert
Die Eigenschaft kind wandelt jeden vollständigen Enum-Wert in einen kleinen Schlüssel ohne zugeordnete Werte um. Sowohl .drawing("Circle") als auch .drawing("Square") ergeben den Schlüssel .drawing. Die Filterfunktion vergleicht diese Schlüssel und fügt dabei das ursprüngliche step hinzu, sodass die zugeordneten Werte der beibehaltenen Elemente intakt bleiben.
Die Funktion muss sich nur einen Schlüssel merken. Zu Beginn ist previousKind gleich nil, sodass das erste Element durchgelassen wird. Nachdem ein Element beibehalten wurde, merkt sich die Funktion dessen Typ. Unterdrückte Elemente haben denselben Typ, daher würde eine Aktualisierung des gespeicherten Schlüssels für diese Elemente keinen Unterschied machen.
Diese Implementierung durchläuft die Eingabe einmal. Bei diesem festen Enum und seinen Schlüsseln konstanter Größe führt sie für jedes weitere Element einen Vergleich der Fall-Schlüssel durch und verwendet neben dem zurückgegebenen Array nur konstanten zusätzlichen Speicherzustand. Die Sequence-Dokumentation von Swift beschreibt die sequenzielle Iteration und das übliche lineare Durchlaufmodell.
PreviewStep entspricht außerdem dem Protokoll Equatable, sodass die Preconditions vollständige Ergebnisse einschließlich der beibehaltenen Zeichenfolgen überprüfen können. Swift unterstützt automatisch erzeugte Gleichheit für geeignete Enums, deren zugeordnete Werte Equatable entsprechen; siehe Protocols. Der Vergleich der Fälle in Kind gibt diesem Filtervorgang eine eigene explizite Regel, ohne die Gleichheit im gesamten Programm neu zu definieren.
Das exhaustive switch in kind bietet außerdem einen nützlichen Kontrollpunkt für die Wartung. Wenn du einen neuen Fall hinzufügst, ergänze den zugehörigen Schlüssel und die Zuordnung. Vermeide eine allgemeine Auffangzuordnung, die einen neuen Fall versehentlich mit einem vorhandenen gruppiert. Swift dokumentiert die vollständige Behandlung von Enum-Fällen mit Switch-Anweisungen unter Enumerations.
Denselben Vergleich mit Combine verwenden
Wenn dein Mac-Code bereits einen Combine-Publisher verwendet, stellt Apple removeDuplicates(by:) für einen benutzerdefinierten Vergleich bereit. Das Prädikat gibt true zurück, wenn der zweite Wert als Duplikat gelten soll. Apple unterstützt diesen Operator ausdrücklich für Fälle, in denen sich der gewünschte Vergleich von der üblichen Gleichheit des Ausgabetyps unterscheidet. Er ist ab macOS 10.15 verfügbar. Siehe removeDuplicates(by:).
Das Folgende ist ein Erweiterungsfragment und kein eigenständiges Programm. Füge es dem vollständigen Beispiel oben auf einem Mac mit verfügbarem Combine hinzu. Es verwendet die vorhandene Definition von PreviewStep und das Array input erneut:
import Combine
let subscription = input.publisher
.removeDuplicates { previous, current in
previous.kind == current.kind
}
.sink { step in
print("Combine: \(step.label)")
}
Damit werden dieselben fünf beibehaltenen Schritte mit dem Präfix \"Combine: \" ausgegeben. Das Prädikat vergleicht die Fall-Schlüssel, und der Publisher gibt die ursprünglichen beibehaltenen Enum-Werte weiter. Apples eigenes Operatorbeispiel folgt demselben Prinzip, indem es eine Eigenschaft vergleicht und eine andere ignoriert. removeDuplicates(by:)
Lass das zurückgegebene Abonnement während der erforderlichen Abonnementdauer bestehen. Apple dokumentiert, dass beim Freigeben des von sink(receiveValue:) zurückgegebenen Werts das Abonnement beendet wird. Diese Überladung gilt für Publisher, deren Fehlertyp Never ist, wie in Apples Beispielen für Sequence-Publisher. sink(receiveValue:)
Vor dem Anpassen des Codes die Randfälle prüfen
Verwende die Array-Funktion, wenn du bereits eine endliche lokale Liste hast. Verwende den Combine-Operator, wenn die Werte bereits über einen Publisher eingehen. Beide hier gezeigten Versionen folgen demselben Vertrag für Fallübergänge.
| Anforderung | Geeigneter Vergleich oder Ansatz |
|---|---|
| Den ersten vollständigen Wert in jedem Falldurchlauf beibehalten | kind vergleichen, wie gezeigt |
| Änderungen an zugeordneten Zeichenfolgen beibehalten | Vollständige Werte mit der üblichen Gleichheit vergleichen |
| Den letzten vollständigen Wert in jedem Durchlauf beibehalten | Einen Wert zwischenspeichern und ihn bei einem Fallübergang sowie am Ende ausgeben |
| Einen wiederkehrenden Fall nach einem anderen Fall beibehalten | Aufeinanderfolgende Durchläufe vergleichen, statt eine globale Liste bereits aufgetretener Fälle zu führen |
| Getrennte Arrays unabhängig filtern | Die Funktion separat aufrufen; jeder Aufruf beginnt mit leerem Vergleichszustand |
Um den letzten zugeordneten Wert beizubehalten, ist ein anderer Algorithmus nötig, da dieses Programm den ersten Wert sofort hinzufügt. Ebenso setzt die Aufteilung einer logischen Folge in getrennte Arrays den Vergleichszustand zurück: Ein Zeichnungswert am Ende eines Stapels und ein weiterer zu Beginn des nächsten bleiben bei getrennten Aufrufen beide erhalten. Wenn Stapel als eine Folge behandelt werden müssen, behalte den vorherigen Schlüssel über Stapelgrenzen hinweg bei.
Foundation stellt Sammlungen und andere grundlegende Funktionen für Apps auf Apple-Plattformen bereit, doch diese Berechnung wird vollständig mit Swift-Typen umgesetzt. Die Publisher-Variante benötigt ausdrücklich Combine; import Foundation importiert diesen Operator nicht. Apple dokumentiert diese Frameworks getrennt unter Foundation und removeDuplicates(by:).
Die synchrone Array-Funktion benötigt auch keine Nebenläufigkeitsmechanismen: Ihr Vergleichszustand gehört zu jedem einzelnen Aufruf. Wenn du später einen gemeinsam genutzten Desktop-Präsentationszustand einführst, lege dessen Isolation separat fest. Apple dokumentiert MainActor als Singleton-Actor, dessen Executor der Haupt-Dispatch-Queue entspricht; die Unterdrückung von Duplikaten wählt selbst keinen Actor aus. MainActor
Für die hier gezeigte lokale Aufgabe ist entscheidend, welcher zugeordnete Wert einen Durchlauf überlebt. Wenn du den ersten Wert auswählst, ist das Verhalten mit einem separaten Fall-Schlüssel leicht nachzuvollziehen und zu prüfen: Bei einem Fallübergang den ursprünglichen Wert beibehalten, übereinstimmende aufeinanderfolgende Schlüssel unterdrücken und wiederkehrende Fälle durchlassen.
Quellen
- Enumerations — The Swift Programming Language, Swift.org.
- Protocols — The Swift Programming Language, Swift.org.
- Install Swift — macOS — Swift.org.
- Sequence — Apple Developer Documentation.
- removeDuplicates(by:) — Apple Developer Documentation.
- sink(receiveValue:) — Apple Developer Documentation.
- Foundation — Apple Developer Documentation.
- MainActor — Apple Developer Documentation.