CodeGym /Kurse /Swift SELF /split und das erneute Zusammenbauen von Strings

split und das erneute Zusammenbauen von Strings

Swift SELF
Level 7, Lektion 5
Verfügbar

1. Wozu brauchen wir split ueberhaupt?

Ein String ist praktisch, aber auch ein wenig tückisch: Von aussen sieht er wie sauberer Text aus, doch innen verbirgt sich oft eine Struktur. Ein Benutzer gibt einen Befehl, eine CSV-Zeile, einen Dateipfad oder «Vorname Nachname Alter» ein, und das Programm erhaelt einen langen String. Und dann stellt sich die Frage: Wollen wir damit als einheitlichem Text arbeiten oder als Sammlung von Teilen?

Stellen Sie sich einen String wie einen Brotlaib vor. Lecker, ganz, aber wenn Sie Sandwiches machen wollen, muessen Sie ihn schneiden. split ist genau dieses «Messer», das den String anhand eines angegebenen Trennzeichens in Stuecke schneidet. Danach entscheiden Sie: Ist es bequemer, diese Stuecke getrennt zu halten (wie eine Liste von Tokens), oder ist es nach ein paar Pruefungen besser, wieder zum ganzheitlichen String zurueckzukehren?

Eine kleine Skizze dessen, was bei CLI-Eingaben normalerweise passiert:

flowchart TD
    A["readLine() -> String"] --> B["Normalisierung (trim/lowercased)"]
    B --> C["split(separator:) -> tokens"]
    C --> D["Formatpruefungen (Anzahl der Teile, leere Werte)"]
    D --> E["Logik (was tun)"]
    E --> F["Ausgabe des Ergebnisses (manchmal erneutes Zusammenbauen des Strings)"]

2. split(separator:) in der einfachsten Form

Wenn man split zum ersten Mal sieht, denkt man: «Ah, jetzt bekomme ich eine Liste von Strings, und alles wird gut.» Fast so, aber es gibt einen wichtigen Unterschied: Das Ergebnis von split ist nicht sofort [String], sondern Teile vom Typ Substring. In der Praxis koennen Sie sich das als «Stuecke des Original-Strings» vorstellen.

Beginnen wir mit der einfachsten Form: Trennung am Leerzeichen.

let line = "one two  three"
let parts = line.split(separator: " ")

print(parts) // ["one", "two", "three"]

Achten Sie auf das doppelte Leerzeichen zwischen two und three: Standardmaessig mag split «keine Leere» und verwirft leere Stuecke, als ob es sagen wuerde: «Ich werde zwischen zwei Leerzeichen kein “Nichts” speichern.»

Ein etwas «lebendigeres» Beispiel: Der Benutzer gibt einen Befehl ein:

let input = "set name Alice"
let tokens = input.split(separator: " ")

print(tokens[0]) // set
print(tokens[1]) // name
print(tokens[2]) // Alice

Und genau hier taucht der Gedanke auf: «Klasse, ich habe tokens[0] - den Befehl, und danach die Argumente.» Das ist genau der Moment, in dem eine Liste von Tokens bequemer wird als ein ganzheitlicher String.

3. Substring: warum das kein String ist und was man damit macht

Auf den ersten Blick sieht Substring wie String aus: Man kann ihn ausgeben, er hat viele vertraute Methoden. Vom Sinn her ist er aber ein «Stueck eines existierenden Strings» und kein eigenstaendiger String. Historisch sind Strings und ihre Unterstrings (Slices) in Swift so aufgebaut, dass Slices als Substring zurueckgegeben werden. Das spiegelt sich im Design der Standardbibliothek wider: String-Slices liefern Substring, und es gibt das gemeinsame Protokoll StringProtocol, das die API aehnlich macht.

Auf unserem Niveau muessen Sie nicht in die Speicherdetails eintauchen. Das praktische Modell ist folgendes:

split gibt String-Teile vom Typ Substring zurueck, und wenn Sie einen echten String brauchen, schreiben Sie String(part).

Beispiel:

let line = "email=user@example.com"
let parts = line.split(separator: "=")

let key = String(parts[0])
let value = String(parts[1])

print(key)   // email
print(value) // user@example.com

Warum sollte man nicht immer sofort alles in String umwandeln? Weil es oft reicht, Tokens zu vergleichen, ihre Anzahl zu pruefen oder sie auszugeben. Sobald Sie jedoch ein Stueck separat speichern wollen oder aktiv neue Strings zusammensetzen, ist es vertrauter, zu String zu wechseln.

4. Parameter von split: maxSplits und omittingEmptySubsequences

Wenn split fuer echte Daten verwendet wird, reicht «einfach am Leerzeichen trennen» oft nicht aus. split hat Parameter, mit denen sich das Verhalten vorhersagbar machen laesst: Man kann die Anzahl der Trennungen begrenzen und mit leeren Teilen umgehen. In der Signatur von split sieht das etwa so aus: maxSplits und omittingEmptySubsequences.

maxSplits: «Schneide nur einmal»

maxSplits ist eine Begrenzung fuer die Anzahl der Schnitte, nicht fuer die Anzahl der Teile. Wenn es 1 Schnitt gibt, dann gibt es hoechstens 2 Teile (falls das Trennzeichen ueberhaupt gefunden wurde).

Das ist unglaublich nuetzlich fuer Befehle wie:

  • set name=Alice Bob (der Wert darf Leerzeichen enthalten),
  • add Buy milk and bread (der Aufgabentext ist der «Schwanz» der Zeile).

Wir wollen zum Beispiel den Befehl und «alles andere» nur einmal trennen:

let input = "add Buy milk and bread"
let parts = input.split(separator: " ", maxSplits: 1)

print(parts[0]) // add
print(parts[1]) // Buy milk and bread

Wuerden wir ohne maxSplits trennen, bekamen wir zu viele Teile, und muessten den Rest spaeter wieder «zusammenkleben». Manchmal ist das in Ordnung, aber oft ist es einfacher, sofort nur einen Schnitt zu machen.

omittingEmptySubsequences: «Leere Teile sind auch Daten»

Standardmaessig verwirft split leere Teile. Das ist bequem fuer normalen Text, aber schlecht fuer Formate, in denen Leere bedeutungsvoll ist, etwa CSV.

Vergleichen Sie:

let csv = "a,,b,"
let partsDefault = csv.split(separator: ",")
print(partsDefault) // ["a", "b"]

Und jetzt speichern wir die leeren Teile:

let csv = "a,,b,"
let parts = csv.split(separator: ",", omittingEmptySubsequences: false)

print("|\(parts[0])|") // |a|
print("|\(parts[1])|") // ||
print("|\(parts[2])|") // |b|
print("|\(parts[3])|") // ||

Hier drucken wir absichtlich mit |...|, um leere Werte sichtbar zu machen. Das ist ein wichtiger Trick beim Debuggen von Strings: Wenn Sie die Grenzen nicht sehen, beginnen Sie an «Geister-Leerzeichen» zu glauben.

5. Tokens oder String: wie man die Darstellung waehlt

Nach split haben Sie eine Struktur in der Hand: eine Menge von Teilen. Und genau hier kommt die zentrale Frage der Lektion: Soll man diese Teile als Tokens behalten, oder sollte man alles wieder zu einem String zusammensetzen?

Wann ist es bequemer, Daten als Liste von Tokens zu halten?

Es ist sinnvoll, einen String in eine Token-Liste umzuwandeln, wenn Sie etwas Strukturelles tun wollen: einen Befehl parsen, ein Format pruefen, Felder extrahieren, einzelne Elemente vergleichen. Tokens geben Ihnen «Griffe», an denen man sich festhalten kann: tokens[0] ist der Befehl, tokens[1] ist der Schluessel, tokens[2] ist der Wert, und so weiter.

Es ist nuetzlich, nicht abstrakt zu denken, sondern in der Form «welche Aufgabe - welche Datenform». Hier ist eine Tabelle, die man im Kopf behalten kann:

Szenario Was Sie als Eingabe haben Bequemer darzustellen als Warum
Befehl des Benutzers (add …, set …) String Tokens man muss Befehl und Argumente trennen
Einfache Validierung (a=b) String Tokens man kann leicht parts.count == 2 pruefen
Wortzaehlung String Tokens «Wort» = Teil nach split am Leerzeichen
Nachricht unveraendert ausgeben String String Struktur ist nicht noetig, der Originaltext ist wichtig
Substring-Suche (contains) String String Aufteilung hilft hier nur im Weg

Besonders wichtig: Sobald Sie in Tokens wechseln, arbeiten Sie fast automatisch mit Indizes (parts[0], parts[1]). Das heisst, Sie muessen die Grenzen der Liste im Kopf behalten: Man darf parts[1] nicht lesen, wenn es weniger als zwei Teile gibt.

6. String ohne joined wieder zusammensetzen: transparent und kontrollierbar

In Swift gibt es fuer das Verketten von Strings die praktische Methode joined. Aber fuer die heutige Uebung setzen wir den String absichtlich von Hand wieder zusammen, damit Sie die Mechanik besser spueren: Wo das Trennzeichen gesetzt wird, wann zusaetzliche Leerzeichen entstehen und warum das «Zusammenkleben» eines Strings immer mit Kontrolle ueber Details zu tun hat.

Einfaches manuelles Zusammenbauen mit Trennzeichen

Nehmen wir an, wir haben Woerter und wollen daraus einen String mit Bindestrichen machen.

let words = "one two three".split(separator: " ")

var result = ""
for i in 0..<words.count {
    if i > 0 { result += "-" }
    result += String(words[i])
}

print(result) // one-two-three

Hier gibt es zwei Schluesselideen. Zuerst fuegen wir "-" nur dann hinzu, wenn es nicht das erste Element ist. Danach fuegen wir das Token selbst hinzu und wandeln Substring in String um. Ein solches Schema ist fast universell: «Trennzeichen nur zwischen den Elementen».

Zusammenbauen mit einem «klugen» Trennzeichen

Manchmal muss man einen Satz wieder zusammensetzen, aber ohne doppelte Leerzeichen zu bekommen.

let raw = "Swift   is   fun"
let parts = raw.split(separator: " ")

var normalized = ""
for i in 0..<parts.count {
    if i > 0 { normalized += " " }
    normalized += String(parts[i])
}

print(normalized) // Swift is fun

Ja, das sieht nach «Normalisierung von Leerzeichen» aus, aber als Uebung ist es besonders nuetzlich: Sie sehen, wie split ueberfluessige leere Stuecke verwirft und wie die manuelle Zusammenfuehrung genau ein Leerzeichen zwischen den Woertern erzeugt.

7. Mini-Beispiel: ProfileCLI mit den Befehlen set/show/exit

Lassen Sie uns ein kleines Konsolenprogramm bauen, das in einer Schleife «lebt», Befehle entgegennimmt und den Zustand in mehreren Variablen speichert. Das ist keine vollwertige Anwendung (dafuer fehlen uns noch Arrays und Datenstrukturen), aber es ist ein hervorragender Trainingsplatz fuer split: Sie werden sehen, wo Tokens wirklich bequemer sind als Strings und warum maxSplits Ihr kuenftiger bester Freund beim Parsen von Befehlen ist.

Geruest fuer Zustand und Eingabeschleife

Wir legen ein Profil an: Name, Stadt und «ueber mich». Die Befehle sind:

  • set name=Alice
  • set city=Kazan
  • set about=I love Swift
  • show
  • exit

Wir einigen uns gleich darauf: Damit der Befehl weniger kaprizioes ist, trimmen wir die Eingabe. Fuer trimmingCharacters(in:) brauchen wir Foundation.

import Foundation

var name = "Unknown"
var city = "Unknown"
var about = ""

while true {
    print("> ", terminator: "")
    let line = readLine() ?? ""
    let cleaned = line.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)

    if cleaned.isEmpty {
        continue
    }

    // Die Befehlsanalyse kommt weiter unten...
}

Beachten Sie, wie terminator: "" hilft, die «Eingabeaufforderung» in derselben Zeile anzuzeigen, in der der Benutzer tippt.

Befehle ohne Argumente: show und exit

Hier brauchen wir Tokens ueberhaupt nicht: Es reicht, den String so wie er ist zu vergleichen (oder in Kleinbuchstaben).

import Foundation

var name = "Unknown"
var city = "Unknown"
var about = ""

while true {
    print("> ", terminator: "")
    let line = readLine() ?? ""
    let cleaned = line.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)

    let cmd = cleaned.lowercased()

    if cmd == "exit" {
        print("Bye!") // Bye!
        break
    }

    if cmd == "show" {
        print("Name: \(name)")
        print("City: \(city)")
        print("About: \(about)")
        continue
    }

    print("Unknown command")
}

Hier ist das der perfekte Fall fuer «wenn der String einfach ein String ist»: Ein einwoertiger Befehl, und split wuerde nur zusaetzliche Schritte erzeugen.

Befehl set ...: einmal am Leerzeichen schneiden

Und jetzt ist set bereits eine Struktur: set + Argument. Das Argument kann Leerzeichen enthalten (zum Beispiel in about), deshalb schneiden wir nur einmal: maxSplits: 1.

import Foundation

var name = "Unknown"
var city = "Unknown"
var about = ""

while true {
    print("> ", terminator: "")
    let line = readLine() ?? ""
    let cleaned = line.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)

    let parts = cleaned.split(separator: " ", maxSplits: 1)

    if parts[0].lowercased() != "set" {
        print("Unknown command")
        continue
    }

    if parts.count < 2 {
        print("Usage: set key=value")
        continue
    }

    let payload = String(parts[1])
    print(payload) // z. B.: name=Alice
}

Hier machen wir bewusst die Pruefung parts.count < 2, bevor wir parts[1] nehmen. Das ist der Stil «zuerst Grenzen - dann Indizes».

Analyse von key=value anhand von =, ebenfalls nur einmal

Innerhalb von payload haben wir das Format Schluessel=Wert. Wieder verwenden wir maxSplits: 1, weil auch der Wert ein = enthalten kann (im echten Leben kommt das vor, zum Beispiel in URLs oder in irgendwelchen Notizen).

import Foundation

let payload = "about=I love Swift = yes"
let kv = payload.split(separator: "=", maxSplits: 1)

print(kv[0]) // about
print(kv[1]) // I love Swift = yes

Und jetzt die finale Zusammenfuegung: Wir aktualisieren die benoetigte Variable.

Vollstaendige Version

Ich teile es in logische Bloecke innerhalb einer einzigen Schleife auf, aber ich versuche, keine Textwueste daraus zu machen.

import Foundation

var name = "Unknown"
var city = "Unknown"
var about = ""

while true {
    print("> ", terminator: "")
    let line = readLine() ?? ""
    let cleaned = line.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)

    if cleaned.isEmpty { continue }

    let lower = cleaned.lowercased()
    if lower == "exit" {
        print("Bye!") // Bye!
        break
    }

    if lower == "show" {
        print("Name: \(name)")
        print("City: \(city)")
        print("About: \(about)")
        continue
    }

    let parts = cleaned.split(separator: " ", maxSplits: 1)
    if parts[0].lowercased() != "set" {
        print("Unknown command")
        continue
    }

    if parts.count < 2 {
        print("Usage: set key=value")
        continue
    }

    let payload = String(parts[1])
    let kv = payload.split(separator: "=", maxSplits: 1)

    if kv.count < 2 {
        print("Usage: set key=value")
        continue
    }

    let key = kv[0].lowercased()
    let value = String(kv[1]).trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)

    if key == "name" {
        name = value
        print("OK") // OK
    } else if key == "city" {
        city = value
        print("OK") // OK
    } else if key == "about" {
        about = value
        print("OK") // OK
    } else {
        print("Unknown key: \(key)")
    }
}

In dieser Version sieht man das Wesentliche: Wir entscheiden mehrmals, wo ein String sinnvoll ist und wo Tokens besser sind.

Zuerst exit/show - reiner String. Dann set ... - Tokens am Leerzeichen, weil wir den Befehl herausziehen muessen. Dann key=value - wieder Tokens, weil Schluessel und Wert getrennt werden muessen. Und den Wert wandeln wir wieder in String um, weil das bereits «Daten» sind, die wir in der Profilvariablen speichern wollen.

8. Typische Fehler beim Arbeiten mit split und beim erneuten Zusammenbauen von Strings

Fehler Nr. 1: erwarten, dass split [String] zurueckgibt, und sich ueber Substring wundern.
Das ist einer der haeufigsten Faelle von «Warum ist der Compiler unzufrieden?». Das Modell ist einfach: split liefert Unterstrings vom Typ Substring, und wenn Sie einen echten String brauchen, machen Sie String(part). Das folgt natuerlich daraus, wie Strings und ihre Slices in Swift aufgebaut sind.

Fehler Nr. 2: auf parts[1] zugreifen, ohne parts.count zu pruefen.
Fast jeder echte String kann unerwartet sein: Der Benutzer gibt Leere ein, ein ueberfluessiges Leerzeichen oder ueberhaupt einen anderen Befehl. Wenn Sie split verwendet haben, gilt als erstes: Pruefen Sie die Anzahl der Teile vor dem Zugriff per Index. Sonst stuerzt das Programm genau in dem Moment ab, in dem Sie es Ihrem «Freund/Lehrer/Kater» zeigen wollen.

Fehler Nr. 3: die Bedeutung von maxSplits verwechseln und «genau N Teile» erwarten.
maxSplits begrenzt die Anzahl der Schnitte. Wenn es 1 Schnitt gibt, dann gibt es hoechstens 2 Teile. Wenn das Trennzeichen nicht vorkommt, gibt es 1 Teil. Deshalb ist es immer sinnvoller, nicht an «wie viele Teile will ich?» zu denken, sondern an «wie oft darf ich schneiden?».

Fehler Nr. 4: vergessen, dass leere Teile standardmaessig verworfen werden.
Dieses Verhalten ist fuer normalen Text sehr bequem, aber fuer CSV und aehnliche Formate kann es die Bedeutung der Daten zerstoeren. Wenn ein leeres Feld eine gueltige Information ist, aktivieren Sie omittingEmptySubsequences: false, sonst erfahren Sie nicht einmal, dass das Feld leer war. Der Sinn dieses Parameters ist direkt in der API von split sichtbar.

Fehler Nr. 5: einen String «roh» zusammensetzen und einen ueberfluessigen Trenner am Anfang oder Ende bekommen.
Klassischer Bug: In einer Schleife fuegt man jedes Mal "-" oder " " hinzu und erhaelt einen String wie "-one-two-three-". Das laesst sich mit einer einfachen Regel beheben: Das Trennzeichen wird nur zwischen den Elementen eingefuegt (if i > 0 { ... }). Das ist banal, aber genau solche «Banalitaeten» machen die Ausgabe sauber und vorhersagbar.

1
Aufgabe
Swift SELF, Level 7, Lektion 5
Gesperrt
Phrasenanalyse
Phrasenanalyse
1
Aufgabe
Swift SELF, Level 7, Lektion 5
Gesperrt
Einstellungspaar
Einstellungspaar
1
Aufgabe
Swift SELF, Level 7, Lektion 5
Gesperrt
CSV-Felder
CSV-Felder
1
Aufgabe
Swift SELF, Level 7, Lektion 5
Gesperrt
Erstellung eines Slugs
Erstellung eines Slugs
1
Umfrage/Quiz
Swift-Strings, Level 7, Lektion 5
Nicht verfügbar
Swift-Strings
Syntax von Strings und Arbeitsmethoden
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION