JavaRush /Курсы /Swift SELF /split и сборка строки обратно

split и сборка строки обратно

Swift SELF
7 уровень , 5 лекция
Открыта

1. Зачем нам вообще split

Строка — штука удобная, но немного коварная: снаружи она выглядит как аккуратный текст, а внутри часто скрывает структуру. Пользователь вводит команду, CSV-строчку, путь к файлу или «имя фамилия возраст», а программа получает одну длинную строку. И дальше возникает вопрос: мы хотим работать с ней как с единым текстом или как с набором частей?

Представьте строку как батон хлеба. Вкусный, цельный, но если вам нужно сделать бутерброды, батон придётся нарезать. split — это как раз «нож», который нарезает строку на кусочки по заданному разделителю. А дальше уже вы решаете: вам удобнее держать эти кусочки отдельно (как список токенов), или после пары проверок лучше вернуться к цельной строке.

Небольшая схема того, что обычно происходит в CLI-вводе:

flowchart TD
    A["readLine() -> String"] --> B["Нормализация (trim/lowercased)"]
    B --> C["split(separator:) -> токены"]
    C --> D["Проверки формата (кол-во частей, пустые значения)"]
    D --> E["Логика (что делать)"]
    E --> F["Вывод результата (иногда сборка строки обратно)"]

2. split(separator:) в самом простом виде

Когда впервые видишь split, кажется: «О, сейчас получу список строк и всё будет хорошо». Почти так, но есть важный нюанс: результат split — это не сразу [String], а части типа Substring. На практике вы можете воспринимать это как «кусочки исходной строки».

Начнём с самой простой формы: делим по пробелу.

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

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

Обратите внимание на двойной пробел между two и three: по умолчанию split «не любит пустоту» и выбрасывает пустые кусочки (как будто говорит: «Я не буду хранить “ничего” между двумя пробелами»).

Чуть более «жизненный» пример — пользователь вводит команду:

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

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

И вот тут уже появляется мысль: «Класс, у меня есть tokens[0] — команда, а дальше аргументы». Это ровно тот момент, когда список токенов становится удобнее цельной строки.

3. Substring: почему это не String и что с ним делать

С первого взгляда Substring выглядит как String: его можно печатать, у него много знакомых методов. Но по смыслу это «кусок существующей строки», а не отдельная самостоятельная строка. Исторически в Swift строки и их подстроки (срезы) устроены так, что срезы возвращаются как Substring, и это отражено в дизайне стандартной библиотеки: срезы строк дают Substring, и есть общий протокол StringProtocol, который делает API похожим.

В рамках нашего уровня вам не нужно уходить в детали памяти. Практическая модель такая:

split возвращает части строки типа Substring, и если вам нужна настоящая строка String, вы делаете String(part).

Пример:

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

Почему не всегда надо сразу превращать всё в String? Потому что часто вам достаточно сравнить токены, проверить их количество, вывести — и всё. Но как только вы хотите хранить кусочек отдельно (в переменной надолго) или активно собирать новые строки, привычнее перейти к String.

4. Параметры split: maxSplits и omittingEmptySubsequences

Когда split используется для реальных данных, «просто разделить по пробелу» часто оказывается недостаточно. У split есть параметры, которые позволяют сделать поведение предсказуемым: ограничить число разбиений и управлять пустыми частями. В сигнатуре split это выглядит примерно так: maxSplits и omittingEmptySubsequences.

maxSplits: «режь только один раз»

maxSplits — это ограничение на количество разрезов, а не «сколько частей будет». Если разрезов 1, то частей будет максимум 2 (если разделитель вообще нашёлся).

Это невероятно полезно для команд вида:

  • set name=Alice Bob (значение может содержать пробелы),
  • add Buy milk and bread (текст задачи — это «хвост» строки).

Например, хотим разделить команду и «всё остальное» только один раз:

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

Если бы мы делили без maxSplits, то получили бы слишком много частей, и нам пришлось бы потом «склеивать хвост обратно». Иногда это нормально, но часто проще сразу сделать одно разбиение.

omittingEmptySubsequences: «пустые части — это тоже данные»

По умолчанию split выбрасывает пустые части. Это удобно для обычного текста, но плохо для форматов, где пустота — значимая, например CSV.

Сравните:

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

А теперь сохраняем пустые части:

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])|") // ||

Здесь мы специально печатаем с |...|, чтобы увидеть пустые значения. Это важный приём отладки строк: если вы не видите границы, вы начинаете верить в «призраки пробелов».

5. Токены или строка: как выбрать представление

После split у вас на руках появляется структура: набор частей. И вот здесь главный вопрос лекции: оставлять ли эти части как токены, или лучше вернуть всё обратно в строку.

Когда удобнее держать данные как список токенов

В список токенов (то есть в «набор кусочков») разумно превращать строку тогда, когда вы собираетесь делать что-то структурное: разбирать команду, проверять формат, извлекать поля, сравнивать отдельные элементы. Токены дают вам «ручки», за которые можно держаться: tokens[0] — команда, tokens[1] — ключ, tokens[2] — значение, и так далее.

Полезно мыслить не абстрактно, а в формате «какая задача — какая форма данных». Вот таблица, которую можно держать в голове:

Сценарий Что у вас на входе Удобнее представить как Почему
Команда пользователя (add …, set …) строка токены нужно выделить команду и аргументы
Простая валидация (a=b) строка токены легко проверить parts.count == 2
Подсчёт слов строка токены «слово» = часть после split по пробелу
Печать сообщения как есть строка строка структура не нужна, важен исходный текст
Поиск подстроки (contains) строка строка дробление только мешает

Особенно важно: как только вы переходите в токены, вы почти автоматически начинаете работать с индексами (parts[0], parts[1]). А значит, вы обязаны помнить про границы списка: нельзя читать parts[1], если частей меньше двух.

6. Сборка строки обратно без joined: прозрачно и управляемо

В Swift для склейки строк есть удобный метод joined. Но в рамках сегодняшнего дня мы намеренно собираем строку обратно вручную, чтобы вы лучше почувствовали механику: где ставится разделитель, когда появляются лишние пробелы, и почему «слепить строку» — это всегда про контроль деталей.

Простейшая ручная сборка с разделителем

Допустим, у нас есть слова, и мы хотим сделать строку через дефис.

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

Здесь два ключевых момента. Сначала мы добавляем "-" только если это не первый элемент. Потом добавляем сам токен, превращая Substring в String. Такая схема почти универсальна: «разделитель только между элементами».

Сборка с «умным» разделителем

Иногда нужно собрать обратно предложение, но так, чтобы не получить двойные пробелы.

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

Да, это похоже на «нормализацию пробелов», но полезно именно как упражнение: вы видите, как split выбрасывает лишние пустые куски, а ручная сборка делает один пробел между словами.

7. Мини‑пример: ProfileCLI с командами set/show/exit

Давйте соберём маленькую консольную программу, которая «живёт» в цикле, принимает команды и хранит состояние в нескольких переменных. Это не полноценное приложение (для него нам ещё не хватает массивов и структур данных), но это отличный тренажёр для split: вы увидите, где токены реально удобнее строки, и почему maxSplits — ваш будущий лучший друг в парсинге команд.

Заготовка состояния и цикл ввода

Мы заведём профиль: имя, город и «о себе». Команды будут такими:

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

Сразу договоримся: чтобы команда была менее капризной, мы будем тримить ввод. Для trimmingCharacters(in:) нам нужен 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
    }

    // Разбор команд будет дальше...
}

Заметьте, как terminator: "" помогает сделать «приглашение ввода» в той же строке, где пользователь печатает.

Команды без аргументов: show и exit

Тут токены нам не нужны вообще: достаточно сравнить строку как есть (или в нижнем регистре).

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")
}

Здесь идеальный пример «когда строка — это строка»: команда однословная, split бы только добавил шагов.

Команда set …: режем по пробелу один раз

А вот set — уже структура: set + аргумент. Причём аргумент может содержать пробелы (например, в about), поэтому режем только один раз: 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) // например: name=Alice
}

Здесь мы сознательно делаем проверку parts.count < 2 до того, как берём parts[1]. Это стиль «сначала границы — потом индексы».

Разбор key=value по =, тоже один раз

Внутри payload у нас формат ключ=значение. Опять используем maxSplits: 1, потому что значение тоже может содержать = (в жизни бывает, например, в URL или в каких-то заметках).

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

И теперь финальная сборка: обновим нужную переменную.

Полная версия

Я разобью на логические блоки внутри одного цикла, но постараюсь не делать простыню.

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)")
    }
}

В этой версии видно главное: мы несколько раз выбираем, где лучше «строка», а где лучше «токены».

Сначала exit/show — чистая строка. Потом set … — токены по пробелу, потому что нужно вытащить команду. Потом key=value — снова токены, потому что нужно разделить ключ и значение. А вот значение мы возвращаем в String, потому что это уже «данные», которые мы хотим хранить в переменной профиля.

8. Типичные ошибки при работе с split и сборкой строки обратно

Ошибка №1: ожидать, что split вернёт [String], и удивляться Substring.
Это один из самых частых «почему компилятор недоволен». Модель простая: split отдаёт подстроки типа Substring, а если вам нужна настоящая строка, делайте String(part). Это естественно вытекает из того, как устроены строки и их срезы в Swift.

Ошибка №2: обращаться к parts[1], не проверив parts.count.
Почти любая реальная строка может оказаться неожиданной: пользователь ввёл пустоту, лишний пробел, вообще другую команду. Если вы сделали split, первое правило — проверяйте количество частей до индексации. Иначе программа упадёт ровно в тот момент, когда вы будете показывать её «другу/преподавателю/коту».

Ошибка №3: путать смысл maxSplits и ожидать «ровно N частей».
maxSplits ограничивает количество разрезов. Если разрезов 1, частей будет максимум 2. Если разделитель не встретился — будет 1 часть. Поэтому всегда полезно думать не «сколько частей хочу», а «сколько раз я разрешаю резать».

Ошибка №4: забывать, что по умолчанию пустые части выбрасываются.
Это поведение очень удобное для обычного текста, но для CSV и похожих форматов может разрушить смысл данных. Если пустое поле — валидная информация, включайте omittingEmptySubsequences: false, иначе вы даже не узнаете, что поле было пустым. Смысл этого параметра прямо отражён в API split.

Ошибка №5: собирать строку обратно «в лоб» и получать лишний разделитель в начале или конце.
Классический баг: в цикле каждый раз добавляют "-" или " " и получают строку вида "-one-two-three-". Лечится простым правилом: разделитель добавляем только между элементами (if i > 0 { ... }). Это банально, но именно такие «банальности» делают вывод аккуратным и предсказуемым.

1
Задача
Swift SELF, 7 уровень, 5 лекция
Недоступна
Разбор фразы
Разбор фразы
1
Задача
Swift SELF, 7 уровень, 5 лекция
Недоступна
Пара настроек
Пара настроек
1
Задача
Swift SELF, 7 уровень, 5 лекция
Недоступна
CSV поля
CSV поля
1
Задача
Swift SELF, 7 уровень, 5 лекция
Недоступна
Создание слага
Создание слага
1
Опрос
Строки Swift, 7 уровень, 5 лекция
Недоступен
Строки Swift
Синтаксис строк и методы работы
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ