Как получить длину строки в Swift? (count не работает как в других языках)
Понимание строк в Swift: Unicode и графемные кластеры
Отличный вопрос! Ты столкнулся с одной из ключевых особенностей Swift, которая отличает его от многих других языков. Проблема в том, что Swift работает со строками на уровне Unicode-совместимости, а не на уровне байтов или отдельных кодовых точек.
В большинстве языков (C, Java, Python до версии 3) строка — это просто массив char или байтов. Swift же пошёл дальше: он считает за один "символ" то, что пользователь воспринимает как один символ. Это называется расширенный графемный кластер (extended grapheme cluster).
Давай разберём твой пример с эмодзи семьи "👨👩👧👦". На самом деле это один графемный кластер, состоящий из 7 Unicode-скаляров:
- 👨 (U+1F468) — мужчина
- (U+200D) — zero-width joiner (соединитель)
- 👩 (U+1F469) — женщина
- (U+200D) — ещё один соединитель
- 👧 (U+1F467) — девочка
- (U+200D) — соединитель
- 👦 (U+1F466) — мальчик
Swift объединяет их в один символ, потому что они визуально отображаются как единое целое. Свойство count возвращает количество именно таких графемных кластеров. Это правильное поведение для большинства реальных задач: когда пользователь видит на экране один символ, он ожидает, что код посчитает его за один.
Как получить "сырую" длину?
Если тебе нужно количество Unicode-скаляров (то есть отдельных кодовых точек), используй свойство unicodeScalars:
let emoji = "👨👩👧👦"
print(emoji.unicodeScalars.count) // Выведет 7
print(emoji.count) // Выведет 1
let flag = "🇷🇺"
print(flag.unicodeScalars.count) // Выведет 2 (региональный индикатор R и U)
print(flag.count) // Выведет 1
А если тебе нужно количество байтов в UTF-8 (как в C или Java), используй:
let text = "Привет"
print(text.utf8.count) // 12 (каждая буква — 2 байта)
print(text.utf16.count) // 6 (каждая буква — 1 единица UTF-16)
print(text.count) // 6
Работа с индексами
Из-за этой особенности нельзя обращаться к символам по целочисленному индексу, как в массиве. Вместо этого используй String.Index:
let str = "Hello, Swift!"
let index = str.index(str.startIndex, offsetBy: 7)
print(str[index]) // Выведет "S"
// Для безопасного доступа:
if let safeIndex = str.index(str.startIndex, offsetBy: 7, limitedBy: str.endIndex) {
print(str[safeIndex])
}
Итерация по символам
Чтобы перебрать все видимые символы (графемные кластеры), просто используй for-in:
let family = "👨👩👧👦"
for char in family {
print(char) // Выведет только один элемент — всю семью
}
// Если нужно разбить на отдельные Unicode-скаляры:
for scalar in family.unicodeScalars {
print(scalar.value) // Выведет числовые коды всех 7 скаляров
}
Почему это важно?
Такое поведение предотвращает множество багов. Представь, что ты считаешь длину текста для ограничения ввода в текстовом поле. Если бы Swift считал Unicode-скаляры, пользователь мог бы ввести строку из 5 символов, но она занимала бы 10 байт (например, иероглифы). А с графемными кластерами ты всегда получаешь то, что видит пользователь.
Итог: string.count — это количество видимых символов. Это то, что нужно в 99% случаев. Для специальных задач используй unicodeScalars.count или utf8.count.
Альтернативный подход: работа с NSString для обратной совместимости
Если ты привык к Objective-C или работаешь с legacy кодом, можно использовать мост к NSString. В Swift строки автоматически мостятся к NSString, поэтому ты можешь вызывать методы Objective-C:
import Foundation
let text = "Привет, мир!"
// Получить "старую" длину (количество UTF-16 единиц)
print((text as NSString).length) // 13
// Получить символ по индексу (как в C)
let char = (text as NSString).character(at: 0) // U+041F — 'П'
print(String(UnicodeScalar(char)!))
Однако будь осторожен: NSString.length считает количество 16-битных единиц, а не видимых символов. Для эмодзи это даст неверный результат с точки зрения пользователя:
let emoji = "👨👩👧👦"
print((emoji as NSString).length) // Выведет 11 (а не 1)
Поэтому используй этот подход только если тебе нужна совместимость с Objective-C API (например, работа с NSRange). В современном Swift всегда предпочитай count и String.Index.
Ещё один вариант — использовать NSString для поиска подстроки по индексу:
let range = NSRange(location: 0, length: 3)
if let substring = (text as NSString).substring(with: range) as String? {
print(substring) // "При"
}
Но повторюсь: для нового кода лучше использовать встроенные Swift-методы — они безопаснее и понятнее.
Ответов пока нет.
Войдите чтобы оставить ответ