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 { ... }). Это банально, но именно такие «банальности» делают вывод аккуратным и предсказуемым.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ