Avatar ·

Как правильно работать с подстрокой (substring) в Swift? Проблемы с String.Index и Range

Всем привет! Я начал изучать Swift недавно, и у меня возникла проблема с получением подстроки из строки. В других языках, например в Python или JavaScript, это делается очень просто: `"Hello"[0:4]` или `"Hello".substring(0, 4)`. Но в Swift я столкнулся с какими-то странными вещами — `String.Index`, `Range`, и методы вроде `prefix()`, `suffix()`, `dropFirst()`. Вот пример, который у меня не работает: ```swift let text = "Привет, мир!" let substring = text[0...5] // Ошибка: 'subscript(_:)' is unavailable ``` Я пытался сделать так, как привык, но компилятор ругается. Потом я узнал, что в Swift нельзя просто использовать целые числа для индексов строк, потому что символы могут быть разной длины (например, эмодзи). Но как тогда получить первые 5 символов или диапазон от 3 до 7 символа? Я пробовал такой код: ```swift let startIndex = text.index(text.startIndex, offsetBy: 0) let endIndex = text.index(text.startIndex, offsetBy: 5) let range = startIndex..>>

Почему в Swift индексы строк работают не как в других языках?

Вы абсолютно правы: сложность связана с поддержкой Unicode. В Swift строка (String) — это коллекция графем-кластеров (расширенных графемных кластеров). Один символ может состоять из нескольких Unicode-скаляров. Например, эмодзи "👍🏽" состоит из двух частей: сам символ и модификатор тона кожи. Если бы мы использовали целочисленные индексы, то могли бы "разорвать" такой составной символ, что привело бы к некорректным данным. Поэтому Swift использует String.Index — специальный тип, который безопасно перемещается по строке с учётом Unicode.

1. Основные методы для работы с подстроками

Вот наиболее часто используемые подходы:

Первые N символов

let text = "Привет, мир!"
let first5 = text.prefix(5) // "Приве" — тип Substring
let first5String = String(text.prefix(5)) // "Приве" — тип String
print(first5String)

Последние N символов

let last4 = text.suffix(4) // "мир!" — Substring
let last4String = String(text.suffix(4)) // "мир!" — String

Диапазон по индексам (ваш пример)

let startIndex = text.index(text.startIndex, offsetBy: 3)
let endIndex = text.index(text.startIndex, offsetBy: 7)
let range = startIndex..<endIndex
let substring = text[range] // "вет," — Substring
let result = String(substring) // "вет," — String

Важно: offsetBy: может выбросить исключение, если индекс выходит за границы строки. Используйте безопасные варианты:

if let endIndex = text.index(text.startIndex, offsetBy: 7, limitedBy: text.endIndex) {
    let safeRange = startIndex..<endIndex
    print(text[safeRange])
}

2. Что такое Substring и чем он отличается от String?

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

let sub = text.prefix(5) // Substring
let newString = String(sub) // String — отдельная копия

3. Удобные расширения (для новичков)

Вы можете добавить свои методы, чтобы писать короче:

extension String {
    subscript(range: Range<Int>) -> String {
        let start = index(startIndex, offsetBy: range.lowerBound)
        let end = index(startIndex, offsetBy: range.upperBound)
        return String(self[start..<end])
    }
    
    func substring(from: Int, to: Int) -> String {
        return self[from..<to]
    }
}

// Использование:
let text = "Привет, мир!"
print(text[0...5]) // "Привет" — теперь работает!
print(text.substring(from: 3, to: 7)) // "вет,"

4. Работа с NSRange (для совместимости с Objective-C)

Если вам нужно использовать старый API (например, NSAttributedString), преобразуйте в NSString:

let nsRange = NSRange(location: 0, length: 5)
let substring = (text as NSString).substring(with: nsRange) // "Приве"

Итог

  • Используйте prefix(), suffix(), dropFirst() для простых случаев.
  • Для произвольного диапазона — String.Index через index(_:offsetBy:).
  • Всегда преобразуйте Substring в String, если планируете хранить результат.
  • Добавьте свои расширения, если хотите синтаксис как в Python.

Главное — помните: сложность индексов оправдана корректной работой с Unicode. Эмодзи, иероглифы и диакритические знаки будут обработаны правильно.

<<>>

Альтернативный подход: использование NSString и NSRange

Если вам нужно быстро перейти от опыта работы с другими языками (например, Python или JavaScript) и вы не хотите углубляться в String.Index, можно временно использовать NSString из Objective-C. Этот подход особенно удобен, когда вы работаете с фиксированными позициями символов (например, парсинг логов или работа с ASCII-текстом).

let text = "Hello, World!"
let nsText = text as NSString
let substring = nsText.substring(with: NSRange(location: 0, length: 5)) // "Hello"
print(substring) // String

Плюсы:

  • Привычный синтаксис с целыми числами.
  • Не нужно создавать расширения.
  • Работает с любыми старыми Objective-C API.

Минусы:

  • Некорректно работает с составными Unicode-символами (например, эмодзи могут быть разорваны).
  • Требуется импорт Foundation (обычно уже есть).
  • Считается менее «свифтовым» подходом.

Используйте этот метод только для простых строк (латиница, цифры) или когда скорость разработки важнее корректности Unicode. Для production-кода рекомендую освоить нативный Swift-подход из первого ответа.