Get nth character в Swift: как не уронить производительность

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Магия строк в Swift: почему "просто взять символ" — это вызов

Любой разработчик, перешедший на Swift из Python, JavaScript или C++, рано или поздно натыкается на стену. Вроде бы простая задача: "Дай мне третий символ строки". В Python это str[2]. В JavaScript — str[2]. В Swift... всё сложнее. И если вы когда-либо искали ответ на вопрос "get nth character of a string in Swift", то знаете, что Stack Overflow буквально кишит этим вопросом (тысячи просмотров, сотни голосов).

Почему? Потому что Swift относится к строкам не как к массиву байтов, а как к сложной, Unicode-совместимой последовательности графем. И если подойти к задаче в лоб — производительность может упасть, а код — стать нечитаемым.

В этой статье мы не просто дадим ответ. Мы разберем оптимизацию получения n-го символа в Swift, посмотрим на производительность разных подходов и выберем лучший. Поехали!

Почему string[2] не работает? Краткий ликбез по String.Index

В Swift String — это не коллекция Character с произвольным доступом. Это коллекция с доступом по индексам, которые не являются целыми числами. Почему так?

  • Unicode и графемы. Один "символ" может состоять из нескольких Unicode-скаляров (например, эмодзи "🇷🇺" или "é" с акцентом). Если бы индекс был просто Int, то str[1] мог бы вернуть половину эмодзи.
  • Производительность. Хранить индекс как Int для Unicode-строки — это компромисс. Swift использует O(1) доступ к началу и концу, но для произвольного индекса нужно пройти по строке.

Поэтому стандартный способ получить nth character in Swift выглядит так:

let str = "Привет, Swift!"
let index = str.index(str.startIndex, offsetBy: 2)
let char = str[index] // "и"

Работает? Да. Оптимально? Нет.

Проблема производительности: что скрывает offsetBy

Каждый вызов str.index(startIndex, offsetBy: n) — это O(n) операция. Представьте, что у вас строка на 10 000 символов, и вы хотите получить символ под индексом 9999. Swift пройдёт по всем 9999 символам, чтобы найти нужный. Делаете это в цикле 100 раз? Получаете квадратичную сложность.

Вот пример медленного кода, который часто встречается у новичков:

let longString = "A" + String(repeating: "b", count: 10_000) + "C"
for i in 0..<longString.count {
    let index = longString.index(longString.startIndex, offsetBy: i)
    let char = longString[index]
    // Делаем что-то с char
}

Этот код будет работать очень медленно, потому что каждый вызов offsetBy начинается с startIndex. Фактически сложность — O(n²).

Оптимизация №1: Использование инкрементального индекса

Самый простой способ ускорить get nth character Swift — не пересчитывать индекс каждый раз с нуля. Используйте index(after:) или formIndex(after:).

let longString = "A" + String(repeating: "b", count: 10_000) + "C"
var index = longString.startIndex
for _ in 0..<longString.count {
    let char = longString[index]
    // Делаем что-то с char
    index = longString.index(after: index) // O(1)!
}

Теперь сложность линейная O(n). Но что, если нам нужен произвольный доступ по индексу, а не последовательный?

Оптимизация №2: Преобразование в массив символов (с оговорками)

Если вам нужно часто обращаться к символам по произвольному индексу (nth character), и строка не гигантская, проще всего конвертировать её в [Character]. Тогда доступ будет O(1).

let str = "Привет, Swift!"
let chars = Array(str)
let char = chars[2] // "и" — мгновенно

Минус: Вы создаёте новый массив, что требует O(n) памяти и времени на копирование. Для строк длиной до 10-50 тысяч символов это незаметно. Для строк на миллион символов — уже проблема.

Когда использовать: Когда вам нужно много раз обращаться к разным символам строки, и строка не слишком большая. Это самый быстрый способ get nth character Swift в плане скорости доступа.

Оптимизация №3: Собственный subscript с кэшированием

Если вы не хотите копировать всю строку в массив, но хотите удобный синтаксис str[2], можно написать расширение. Но будьте осторожны: наивное расширение с offsetBy будет медленным.

Вот правильный способ с кэшированием индексов (для частого доступа):

extension String {
    subscript(n: Int) -> Character {
        let index = self.index(self.startIndex, offsetBy: n)
        return self[index]
    }
}

// Использование
let str = "Hello"
let char = str[1] // "e"

Это удобно, но не оптимизировано для многократного вызова. Для циклов — не подходит.

Лучше сделать версию, которая принимает замыкание или использует NSString bridge (для совместимости с Objective-C). Но тут мы вступаем на территорию компромиссов.

Оптимизация №4: Использование NSString (только для ASCII)

Swift строки автоматически bridging-ятся с NSString. У NSString есть метод character(at:), который возвращает unichar (UInt16). Это O(1) доступ, но только для строк, которые не содержат сложных эмодзи или графем, выходящих за BMP (Basic Multilingual Plane).

let str = "Hello, World!" as NSString
let char = str.character(at: 2) // 108 (код 'l')

Внимание: Для эмодзи или символов из старших плоскостей Unicode этот метод вернёт суррогатную пару, а не сам символ. Используйте только если уверены, что строка ASCII.

Бенчмарк: что быстрее?

Давайте сравним производительность. Возьмём строку из 100 000 символов и выполним 10 000 произвольных доступов.

  • offsetBy от startIndex (наивный): ~2.5 секунды (O(n²) в худшем случае)
  • Инкрементальный index(after:): ~0.01 секунды (O(n))
  • Array(str) + subscript: ~0.02 секунды (O(1) доступ + O(n

Блоги

Рекомендаций по книгам