Символы и Юникод
В прошлом уроке мы считали крупные бусины на нитке. Теперь присмотримся к мелким, которые висят под ними.
Компьютер хранит в памяти только числа. Память делится на ячейки по восемь бит, и каждая такая ячейка — байт: в ней помещается целое число от 0 до 255. Те же восемь бит в типе Int8 хранят числа от -128 до 127, потому что там половину значений отдали отрицательным числам, а байты строки отрицательными не бывают. Чтобы сохранить символ, его записывают одним или несколькими байтами. Это и есть мелкие бусины: под c висит одна, а под é — две.
У строки два разных размера, и Swift умеет называть оба:
let word = "café"
print(word.count) // 4
print(word.utf8.count) // 5Давайте разберём вывод:
word.countсчитает символы, то есть крупные бусины. Их4.word.utf8.countсчитает байты, то есть мелкие бусины. Их5: букваéзанимает два байта.
Юникод и UTF-8
Символов в мире больше ста тысяч: латиница, кириллица, иероглифы, эмодзи. Юникод — общая таблица, в которой у каждого символа есть свой номер. Номеров в ней больше миллиона, а в один байт помещается только число до 255.
Поэтому номер символа записывают одним или несколькими байтами по особому правилу. Такое правило называют кодировкой. Swift хранит строки в кодировке UTF-8, и в ней номер символа занимает от одного до четырёх байтов:
- латинская буква, цифра или пробел — 1 байт;
- буква европейского языка с надстрочным знаком, как
éилиü, и буква кириллицы — 2 байта; - китайский иероглиф — обычно 3 байта;
- эмодзи — обычно 4 байта.
utf8 показывает строку как ряд этих байтов. Поэтому word.utf8.count — число байтов, которые занимает строка в UTF-8.
Символ Character
Тип Character хранит один символ — одну крупную бусину, сколько бы мелких под ней ни висело. Цикл for-in умеет перебирать строку по символам:
for ch in word {
print(ch, terminator: " ") // c a f é
}
print()Разберём, что здесь происходит:
for ch in wordпроходит строку от первого символа до последнего.- В константу
chпо очереди попадаютc,a,fиé, и у неё типCharacter. éприходит целиком, одним символом, а не двумя байтами по отдельности.- Пустой
print()заканчивает строку вывода.
У символа, как и у строки, есть utf8: ch.utf8.count — сколько байтов занимает этот символ. Нажимайте «Следующий шаг» и следите за bytes: на трёх первых символах счётчик растёт на один, а на é — сразу на два.
1let word = "café"2var bytes = 03for ch in word {4 bytes += ch.utf8.count5 print(ch, bytes)6}7print(word.count, bytes)
В строке четыре символа: c, a, f и é.
Что показал виджет:
c,aиfзанимают по одному байту, поэтому сначала байтов столько же, сколько символов;é— один символ из двух байтов, и с этого места числа расходятся;- в итоге
countравен4, а байтов5.
Этот вопрос часто задают на собеседовании: почему count у строки Swift не равен числу байтов? Хороший ответ: count считает символы так, как их видит человек, а один символ может занимать в UTF-8 несколько байтов. Число байтов даёт utf8.count.
Один символ из нескольких номеров
Иногда символ складывается даже из нескольких номеров Юникода. Эмодзи 👍🏽 — это номер знака «палец вверх» и номер оттенка кожи. На экране он один, и для Swift это тоже один символ:
let thumb = "👍🏽"
print(thumb.count, thumb.utf8.count) // 1 8Каждый из двух номеров занимает четыре байта, поэтому utf8.count равен 8. А count даёт 1: Character — это символ так, как его видит человек.
Здесь картинка с бусинами неточна. Крупные бусины на настоящей нитке видны сразу, и сосчитать их можно на глаз. Swift так не может: чтобы узнать count, он проходит строку от начала и по байтам решает, где кончается один символ и начинается следующий.
Частая ошибка
Длину ника проверили через utf8.count, как будто это число букв:
let nick = "Zoë"
let isShort = nick.utf8.count <= 3 // ✗
print(isShort)Ник из трёх букв не прошёл проверку «не длиннее трёх символов»: программа печатает false. Буква ë занимает два байта, и utf8.count равен 4. Символы считает count: nick.count <= 3 даёт true.
Главное
- В UTF-8 латинская буква занимает один байт,
éи буква кириллицы — два, эмодзи — четыре и больше. countсчитает символы, аutf8.count— байты: у"café"это4и5.for ch in sперебирает строку по символам, и уchтипCharacter.Character— символ так, как его видит человек, даже если он собран из нескольких номеров Юникода.