CodeGym /Kurse /Swift SELF /Raw values und init?(rawValue:) in Swift

Raw values und init?(rawValue:) in Swift

Swift SELF
Level 25, Lektion 1
Verfügbar

1. Manchmal wird «einfach nur ein String» zum Problem

Wenn Sie eine Konsolenanwendung schreiben, kommuniziert sie staendig mit der Aussenwelt. Und die Aussenwelt kommuniziert ... ueber Strings. Der Benutzer gibt "help", "add", "exit" ein. Irgendwo in einer Datei liegt "paperback". Irgendwo in einer API kommen 200 oder 404 an. Und anfangs scheint es: na und, wir vergleichen einfach String mit String — los geht's.

Das Problem tritt nicht sofort auf. Es taucht in dem Moment auf, in dem Sie sich vertippen ("exiit"), die Gross-/Kleinschreibung aendern ("Help"), einen neuen Befehl hinzufuegen, vergessen, eine von zehn Stellen zu aktualisieren, an denen Sie Strings verglichen haben, und ... die Anwendung benimmt sich «seltsam». Das Aergste daran: Der Compiler kann Sie nicht schuetzen, denn fuer ihn ist "exit" nur eine Folge von Buchstaben.

Raw values sind genau hier nuetzlich: Wir wollen, dass es innerhalb des Programms typsicher ist (enum Command), aber aussen stabil und vorhersehbar ("exit" oder 0) ist, und dass zwischen diesen beiden Welten eine sichere Umwandlung existiert.

2. Raw values in enum

Ein raw value (der rohe/aeussere «Code» einer Variante) ist ein Wert vom Typ String oder Int, der fest mit einem konkreten case verbunden ist. Das heisst: Jede Variante bekommt einen «Pass»: So stellt sie sich in der Aussenwelt dar.

Die Syntax sieht so aus:

import Foundation

enum Command: String {
    case help
    case exit
}

Hier ist Command weiterhin ein enum, aber jetzt ist er mit einem String «hinterlegt». Unter der Haube sorgt Swift dafuer, dass jeder case einen rawValue hat.

Wichtig: Im Rahmen dieses Tages arbeiten wir nur mit enum ohne «zusatzliche Daten» innerhalb des Cases. Also case help, aber nicht case add(title: String) — solche Dinge kommen spaeter.

rawValue: wie man die externe Darstellung abruft

Wenn ein enum raw values hat, bekommt jeder Wert eine Eigenschaft rawValue.

Lassen Sie uns mit einem moeglichst einfachen Beispiel beginnen:

import Foundation

enum Command: String {
    case help
    case exit
}

let cmd: Command = .help
print(cmd.rawValue) // help

Achten Sie auf die Feinheit: Wir geben nicht cmd aus (das ist Command), sondern genau cmd.rawValue (das ist String). Das heisst: rawValue ist gewissermassen «wie der Befehl im Text aussehen wuerde».

Das ist praktisch, wenn Sie dem Benutzer zum Beispiel einen Hinweis im Format ausgeben moechten, das er eingeben soll:

import Foundation

enum Command: String {
    case help
    case exit
}

print("Verfuegbare Befehle: \(Command.help.rawValue), \(Command.exit.rawValue)")
// Verfuegbare Befehle: help, exit

In diesem Moment arbeitet das enum wie eine «einzige Quelle der Wahrheit». Der Befehl heisst .help, und nach aussen erscheint er immer als "help".

Automatische und explizite raw values

Wenn der raw value ein String ist, kann Swift automatisch den String aus dem Namen des case einsetzen. Also case help → "help". Das sieht praktisch aus ... bis Sie merken, dass Sie die Namen der Cases aus Lesbarkeitsgruenden vielleicht umbenennen wollen, das Ein-/Ausgabeformat aber stabil bleiben muss.

Mit Int ist es anders: Swift kann Cases automatisch durchnummerieren, beginnend bei 0, wenn Sie keine konkreten Zahlen angeben.

Damit Sie sich das nicht nur «vom Hoeren sagen» merken, halten wir es in einer kleinen Tabelle fest:

Basis des raw value Wenn keine Werte explizit gesetzt sind Beispiel
String
der String entspricht dem Namen des case
case inProgress → "inProgress"
Int
automatische Nummerierung (meist ab 0)
case small, medium, large → 0, 1, 2

Und genau hier entsteht eine praktische Regel: Wenn der raw value Teil des Formats ist, das der Benutzer eingibt (CLI-Befehle) oder das in einer Datei gespeichert wird, sollte man ueber explizite Werte nachdenken. Denn das Umbenennen eines Cases ist ein normaler Refactoring-Schritt, das Zerstoeren eines Formats eher nicht.

Beispiel mit expliziten String-raw-values:

import Foundation

enum Command: String {
    case help = "help"
    case exit = "exit"
}

Auf den ersten Blick sieht das nach «ein bisschen doppelt gemoppelt» aus, aber dieser Ansatz hat einen Vorteil: Sie zeigen klar, dass "help" ein Format ist und nicht einfach ein Nebenprodukt des Namens des case.

Rueckwaertige Umwandlung: init?(rawValue:) und Optional

Wir haben gelernt, ein enum ueber rawValue in einen String zu verwandeln. Jetzt das Wichtigste: Wie man einen String wieder in ein enum verwandelt.

Dafuer bekommt ein enum mit raw values automatisch einen Initializer:

Command(rawValue: "help") // Command?

Der Schluesselpunkt: Das Ergebnis ist ein Optional. Das ist kein «Swift ist launisch», sondern Schutz: Der String "helpp" darf nicht magisch zu einem Befehl werden. Er muss ehrlich zu nil werden.

Ein solcher Initializer heisst failable initializer: Er kann keinen Wert erzeugen und nil zurueckgeben. In Swift ist die Idee, dass ein Initializer fehlschlagen und nil zurueckgeben darf, ein normaler Teil der Sprache: Solche Initializer duerfen tatsaechlich mit return nil enden.

Beispiel:

import Foundation

enum Command: String {
    case help
    case exit
}

let input = "exit"

if let cmd = Command(rawValue: input) {
    print("Command recognized: \(cmd.rawValue)") // Command recognized: exit
} else {
    print("Unknown command: \(input)")
}

Hier tun wir genau das, was wir zuvor gelernt haben: Wenn ein Wert existiert — arbeiten wir damit, wenn nicht — melden wir einen Fehler.

Manchmal moechte man es kuerzer haben — man kann ?? verwenden, muss aber vorsichtig sein, weil es einen Fehler «verschlucken» kann:

import Foundation

enum Command: String {
    case help
    case exit
}

let input = "unknown"
let cmd = Command(rawValue: input) ?? .help
print(cmd.rawValue) // help

Das ist akzeptabel, wenn Sie wirklich sagen wollen: «nicht erkannt → wir nehmen help an». Aber wenn Sie eine echte CLI schreiben, ist es haeufig besser, klar mitzuteilen, dass der Befehl unbekannt ist.

Mini-Schema: Befehl-Parsing-Pipeline

Bevor wir Code fuer eine lehrreiche CLI schreiben, ist es nuetzlich, den Prozess als Ganzes zu sehen — als kurze Pipeline. Das ist der Fall, in dem ein Flussdiagramm ein paar Minuten «warum ist es nil?» spart.

flowchart TD
    A["readLine() -> String?"] --> B["Optional entpacken (guard/if let)"]
    B --> C["trimmen/lowercase"]
    C --> D["erstes Token nehmen"]
    D --> E["Command(rawValue: token) -> Command?"]
    E -->|some| F["Befehl ausfuehren"]
    E -->|nil| G["melden: unbekannter Befehl"]

Wir tun nichts Magisches: Wir bringen die Eingabe einfach sauber in das Format und versuchen dann, ein enum zu erzeugen.

3. Beispiel: LibraryCLI und das Erkennen eines Befehls

Stellen wir uns vor, wir haben bereits eine einfache Schleife zum Lesen von Befehlen (die Idee begegnete uns schon, als wir while und Eingabe gelernt haben). Unser Ziel ist jetzt ganz bescheiden: Wir wollen nur das Verb des Befehls erkennen. Argumente (zum Beispiel der Buchtitel) lassen wir vorerst als Strings stehen, weil die Modellierung von «Befehl + Argumente» ein naechster Schritt und ein eigenes Thema ist.

Beginnen wir mit dem enum fuer Befehle:

import Foundation

enum LibraryCommand: String {
    case help
    case list
    case exit
}

Jetzt schreiben wir eine kleine Funktion parseCommand(_:), die einen String entgegennimmt und versucht, den Befehl aus dem ersten Token herauszulesen:

import Foundation

func parseCommand(_ line: String) -> LibraryCommand? {
    let cleaned = line.trimmingCharacters(in: .whitespacesAndNewlines)
    let parts = cleaned.split(separator: " ")
    guard let first = parts.first else { return nil }

    return LibraryCommand(rawValue: String(first))
}

Achten Sie darauf, wie wir vorsichtig vorgehen:

Wir bereinigen Leerzeichen am Rand, zerlegen den String in Teile, nehmen den ersten Teil und wandeln Substring in String um, weil rawValue bei uns ein String ist.

Jetzt koennen wir es verwenden:

import Foundation

let line = "list books"
let cmd = parseCommand(line)

print(cmd?.rawValue ?? "nil") // list

Ja, das ist bisher Minimalismus, aber schon wichtig: Wir vergleichen nicht mehr Strings mit "list" == ..., sondern bekommen den Typ LibraryCommand.

Etwas besserer UX: Befehle nicht vom Gross-/Kleinschreiben abhaengig machen

Der Benutzer wird garantiert "HELP" eingeben, weil Caps Lock eine eigene Lebensform ist. Unterstuetzen wir das mit einem einfachen lowercased():

import Foundation

func parseCommand(_ line: String) -> LibraryCommand? {
    let cleaned = line.trimmingCharacters(in: .whitespacesAndNewlines)
    let parts = cleaned.split(separator: " ")
    guard let first = parts.first else { return nil }

    let token = String(first).lowercased()
    return LibraryCommand(rawValue: token)
}

Jetzt wird aus "HELP" "help", und der Befehl wird erkannt.

4. Int raw values: Menus und numerische Codes

String-raw-values sind gut fuer Befehle, die der Benutzer in Worten eingibt. Aber manchmal ist die Oberflaeche anders aufgebaut: Sie drucken ein Menu aus und bitten um eine Zahl. "1" einzugeben ist fuer viele einfacher als "list".

Genau hier werden Int-raw-values sehr praktisch.

Beispiel fuer ein Menu:

import Foundation

enum MenuChoice: Int {
    case list = 1
    case help = 2
    case exit = 0
}

Die Bedeutung ist fast ohne Kommentar lesbar: 0 — beenden, 1 — Liste anzeigen, 2 — Hilfe.

Jetzt sieht das Parsen so aus:

import Foundation

let inputNumber = 2

if let choice = MenuChoice(rawValue: inputNumber) {
    print("Sie haben gewaehlt: \(choice)") // Sie haben gewaehlt: help
} else {
    print("Unbekannter Eintrag: \(inputNumber)")
}

In einer echten Eingabe erhalten Sie zuerst einen String, wandeln ihn dann in Int um und anschliessend in MenuChoice?:

import Foundation

let line = "1"
let number = Int(line) ?? -1

let choice = MenuChoice(rawValue: number)
print(choice?.rawValue ?? -1) // 1

Hier sieht man zwei «Risikozonen»: die Umwandlung von String -> Int und die Umwandlung von Int -> enum. Und beide geben uns ehrlich die Moeglichkeit, einen Fehler zu behandeln.

5. Raw value ist ein Format und kein «schoener Text»

Es gibt eine feine Verwechslung, in die fast alle tappen: Raw values werden als «menschlich lesbare Namen» benutzt. Manchmal stimmt das, aber nicht immer.

Stellen Sie sich vor, Sie haben Buchgenres. Fuer eine Datei moechten Sie stabile Codes wie "sci-fi", "non-fiction", dem Benutzer aber «Science-Fiction» und «Non-Fiction» anzeigen. Das sind unterschiedliche Aufgaben, und sie zu mischen ist unpraktisch.

Machen wir es so: raw values fuer das Format, und den schoenen Text ueber eine computed property. (Computed properties in enum sind uns als Idee schon begegnet: Ein Typ kann Verhalten enthalten.)

import Foundation

enum Genre: String {
    case sciFi = "sci-fi"
    case nonFiction = "non-fiction"

    var title: String {
        switch self {
        case .sciFi: return "Science-Fiction"
        case .nonFiction: return "Non-Fiction"
        }
    }
}

Jetzt haben wir ein stabiles Format:

import Foundation

let g: Genre = .sciFi
print(g.rawValue) // sci-fi
print(g.title)    // Science-Fiction

Und das erspart Schmerzen, wenn Sie ploetzlich .sciFi in .scienceFiction umbenennen moechten, damit der Code besser lesbar ist: Der raw value bleibt "sci-fi", und Datei sowie Benutzereingabe brechen nicht.

6. Der Zugriff self.init(rawValue:) in eigenen Initializern

Manchmal moechten Sie einen Initializer nicht direkt als init?(rawValue:) schreiben, sondern zum Beispiel «aus einem String, aber mit Bereinigung». Innerhalb eines solchen Initializers verwendet man oft den Trick: erst den Wert vorbereiten, dann self.init(rawValue: prepared) aufrufen.

In der Dokumentation und in Beispielen zu Standardansaetzen der Codierung trifft man genau auf diese Idee: zuerst rawValue vorbereiten, dann in self.init(rawValue: rawValue) delegieren.

Wir koennen etwas Aehnliches fuer Lernzwecke bauen, zum Beispiel fuer ein Genre:

import Foundation

enum Genre: String {
    case sciFi = "sci-fi"
    case nonFiction = "non-fiction"

    init?(userInput: String) {
        let cleaned = userInput.trimmingCharacters(in: .whitespacesAndNewlines)
        let normalized = cleaned.lowercased()
        self.init(rawValue: normalized)
    }
}

Verwendung:

import Foundation

let input = "  SCI-FI "
let genre = Genre(userInput: input)
print(genre?.rawValue ?? "nil") // sci-fi

Die Idee ist folgende: Wir geben dem Menschen einen «intelligenten Eingang», aber die finale Entscheidung trifft weiterhin init?(rawValue:).

7. Typische Fehler

Fehler Nr. 1: zu erwarten, dass Enum(rawValue:) immer einen Wert zurueckgibt, und Optional zu ignorieren.
Das ist ein Klassiker: Ein Student schreibt let cmd: Command = Command(rawValue: input) und wundert sich, warum der Compiler meckert. Der Compiler hat recht: Die Eingabe kann beliebig sein, also ist das Ergebnis ein Command?. Das behebt man nicht mit einem !, sondern mit sauberem Entpacken ueber if let/guard let oder mit einem sinnvollen Standardwert ueber ?? (falls ein Standardwert wirklich passt).

Fehler Nr. 2: ! bei externer Eingabe verwenden.
Command(rawValue: input)! sieht wie ein schneller Weg zum Glueck aus, ist in der Praxis aber ein schneller Weg zum Programmabsturz bei jedem Tippfehler des Benutzers. Externe Eingaben sind per Definition unzuverlaessig, und init?(rawValue:) ist absichtlich als failable initializer gebaut, also als ein Initializer, der gueltig mit nil enden kann.

Fehler Nr. 3: den Namen des case mit dem raw value verwechseln, besonders bei String.
Solange raw values automatisch generiert werden, scheint es, als waere das alles dasselbe. Aber sobald Sie case sciFi = "sci-fi" setzen, wird auf einmal offensichtlich: Der Case-Name ist fuer den Code da, der raw value fuer das Format. Wenn Sie die Benutzereingabe mit "sciFi" statt mit "sci-fi" vergleichen, werden Befehl/Genre nicht erkannt, und Sie schauen lange auf den Bildschirm mit dem Ausdruck: «aber ich war doch fast richtig».

Fehler Nr. 4: den Erkennungsfehler mit ?? dort verstecken, wo ein klares unknown gebraucht wird.
Manchmal macht man let cmd = Command(rawValue: input) ?? .help und denkt, man habe den UX verbessert. In Wirklichkeit verschlechtert das Verhalten oft: Der Benutzer hat "lisst" eingegeben, und die Anwendung zeigt stillschweigend help an, als haette der Benutzer selbst um Hilfe gebeten. Wenn Sie eine CLI bauen, ist es in der Regel korrekter, ehrlich «unknown command» zu sagen und den Hinweis separat anzuzeigen.

Fehler Nr. 5: raw value als «schoenen Text» missbrauchen und spaeter an Lokalisierung und Bearbeitung leiden.
Wenn Sie den raw value als «Science-Fiction» speichern, binden Sie das Datenformat an die Sprache der Benutzeroberflaeche und an Satzzeichen. Heute fuegen Sie einen Punkt hinzu, morgen aendern Sie die Gross-/Kleinschreibung, und ploetzlich lassen sich alte Daten nicht mehr laden. Viel verlaesslicher ist es, den raw value als stabilen Code ("sci-fi") zu halten und den schoenen Text ueber eine computed property wie title auszugeben.

1
Aufgabe
Swift SELF, Level 25, Lektion 1
Gesperrt
Versandcodes
Versandcodes
1
Aufgabe
Swift SELF, Level 25, Lektion 1
Gesperrt
Zahlungsstatus
Zahlungsstatus
1
Aufgabe
Swift SELF, Level 25, Lektion 1
Gesperrt
App-Menü
App-Menü
1
Aufgabe
Swift SELF, Level 25, Lektion 1
Gesperrt
Der erste Befehl
Der erste Befehl
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION