Avatar ·

Как получить длину строки в Swift? (count не работает как в других языках)

Всем привет! Я только начал изучать Swift после Python и столкнулся с непонятной ситуацией. В Python я привык писать `len("hello")` и получать 5. В Swift я попробовал сделать так: ```swift let text = "Привет, мир!" print(text.length) ``` Но Xcode выдал ошибку: "Value of type 'String' has no member 'length'". Тогда я вспомнил, что в некоторых языках используют свойство `length`, но в Swift это не работает. Потом я нагуглил, что нужно использовать свойство `count`: ```swift print(text.count) ``` Но тут я заметил странное поведение. Если строка содержит эмодзи или символы с акцентами (например, "café" или "😊"), то `count` показывает не то, что я ожидаю. Например: ```swift let emoji = "👨‍👩‍👧‍👦" print(emoji.count) // Выводит 1 ``` А я ожидал, что будет 4 (папа, мама, девочка, мальчик). Или с флагом: ```swift let flag = "🇷🇺" print(flag.count) // Тоже 1 ``` Почему так происходит? Как правильно получить количество символов в строке, чтобы учитывались все эти эмодзи и составные символы? И есть ли способ получить "сырую" длину, как в C или Java? <<>>

Понимание строк в 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-методы — они безопаснее и понятнее.