Введение: Почему «длина строки» — это не просто цифра?
Представьте, что вы пишете приложение для iOS. Пользователь вводит своё имя в текстовое поле. Всё просто? А если имя содержит эмодзи, диакритические знаки (например, «café») или иероглифы? Внезапно простая задача «получить длину строки» превращается в детектив с элементами Unicode.
В Swift работа со строками — это не просто подсчёт символов. Это путешествие в мир графемных кластеров, Unicode-скаляров и наследия Objective-C (да-да, NSString всё ещё с нами). Если вы только начинаете изучать Swift или переходите с других языков, эта статья для вас. Мы разберём, как правильно определить длину строки, какие подводные камни вас ждут и когда использовать count, а когда — NSString.
Этот материал основан на реальном популярном вопросе со Stack Overflow. Тысячи разработчиков спотыкались на этой теме. Давайте не будем повторять их ошибки. Поехали!
1. Основы: свойство count у String
Самый очевидный способ получить длину строки в Swift — использовать свойство count. Оно возвращает количество «символов» (Character) в строке. Но что такое символ для Swift?
let greeting = "Привет, мир!"
print(greeting.count) // 12 (включая пробел и восклицательный знак)
Всё работает, как ожидалось. Но давайте усложним задачу:
let emojiString = "👨👩👧👦"
print(emojiString.count) // 1 (!!!)
Один? Серьёзно? А визуально это целая семья из четырёх человек. В чём подвох?
Ответ кроется в том, как Swift обрабатывает строки. count считает не байты и не отдельные Unicode-символы, а графемные кластеры (extended grapheme clusters). Это последовательность одного или более Unicode-скаляров, которые воспринимаются пользователем как один символ. Эмодзи «семья» состоит из нескольких частей, но Swift считает их одним символом, потому что так их видит человек.
Это поведение — не баг, а фича. Swift спроектирован с учётом интернационализации. Если бы count считал каждый Unicode-скаляр, строка «café» (с одним символом é) могла бы дать разный результат в зависимости от того, как é закодировано: как один символ U+00E9 или как комбинация e + ударение (U+0065 + U+0301).
let cafe1 = "café" // é как один символ
let cafe2 = "cafe\u{0301}" // e + ударение
print(cafe1.count) // 4
print(cafe2.count) // 4 (хотя в строке 5 скаляров)
Swift считает оба варианта как 4 символа, потому что пользователь видит «café» из 4 букв. Это и есть магия графемных кластеров.
2. Альтернативный подход: NSString и свойство length
Иногда вам нужно получить не «человеческое» количество символов, а количество единиц кодировки UTF-16. Например, при работе с устаревшими API, Core Text или при передаче данных в форматах, которые ожидают длину в UTF-16 (как некоторые сетевые протоколы).
Здесь на помощь приходит NSString — старый добрый класс из Objective-C. Swift-строки автоматически мостятся (bridge) к NSString, поэтому вы можете использовать его свойство length:
let swiftString = "👨👩👧👦"
let nsString = swiftString as NSString
print(nsString.length) // 11 (количество единиц UTF-16)
Почему 11? Потому что составной эмодзи «семья» в UTF-16 занимает 11 единиц (каждый компонент кодируется суррогатной парой или отдельным символом). Если вы работаете с текстом, который будет обрабатываться старыми C-библиотеками или API, ожидающими NSString, используйте length. Но помните: это не количество символов, которые видит пользователь.
Ещё один нюанс: NSString не учитывает графемные кластеры. Для строки «café» (второй вариант) length вернёт 5, хотя count — 4. Вывод: выбирайте метод в зависимости от контекста.
3. Подсчёт Unicode-скаляров и UTF-8
Swift даёт вам доступ к разным представлениям строки. Иногда нужно знать, сколько байт займёт строка в кодировке UTF-8 (например, при отправке по сети). Для этого используйте свойство utf8:
let text = "Hello"
print(text.utf8.count) // 5 (каждый символ — 1 байт в UTF-8)
let emoji = "😀"
print(emoji.utf8.count) // 4 (эмодзи занимает 4 байта в UTF-8)
Аналогично есть utf16 (количество единиц UTF-16) и unicodeScalars (количество Unicode-скаляров):
let family = "👨👩👧👦"
print(family.unicodeScalars.count) // 7 (семь скаляров, которые образуют один графемный кластер)
Эти методы полезны, когда вам нужно точно знать, сколько места занимает строка в памяти или в потоке данных. Но для отображения пользователю всегда используйте count.
4. Практический пример: валидация длины пароля
Допустим, вы пишете форму регистрации. Пароль должен быть не менее 8 символов. Как правильно проверить длину? Если вы используете NSString.length, пользователь, вставивший эмодзи, сможет обойти ограничение (потому что один эмодзи может занимать 2 или 4 единицы UTF-16).
func isValidPassword(_ password: String) -> Bool {
// Используем count, чтобы учитывать графемные кластеры
return password.count >= 8
}
let testPassword = "😀😀😀😀" // 4 эмодзи, но визуально — 4 символа
print(isValidPassword(testPassword)) // false (правильно: 4 < 8)
// Если бы мы использовали NSString.length:
let nsTest = testPassword as NSString
print(nsTest.length >= 8) // true (4 эмодзи = 16 единиц UTF-16) — ОШИБКА!
Вывод: для валидации пользовательского ввода всегда используйте count. Исключение — если вы жёстко контролируете кодировку (например, только ASCII).
5. Подводные камни и советы
5.1. Производительность
Свойство count у String имеет сложность O(n), где n — количество графемных кластеров. Это означает, что для очень длинных строк (миллионы символов) вызов count может быть медленным. В большинстве приложений это не