Перейти к содержимому
Теория · Строки и символы · урок 2 из 7

Символы и Юникод

В прошлом уроке мы считали крупные бусины на нитке. Теперь присмотримся к мелким, которые висят под ними.

Компьютер хранит в памяти только числа. Память делится на ячейки по восемь бит, и каждая такая ячейка — байт: в ней помещается целое число от 0 до 255. Те же восемь бит в типе Int8 хранят числа от -128 до 127, потому что там половину значений отдали отрицательным числам, а байты строки отрицательными не бывают. Чтобы сохранить символ, его записывают одним или несколькими байтами. Это и есть мелкие бусины: под c висит одна, а под é — две.

У строки два разных размера, и Swift умеет называть оба:

SWIFT
let word = "café"
print(word.count) // 4
print(word.utf8.count) // 5

Давайте разберём вывод:

  1. word.count считает символы, то есть крупные бусины. Их 4.
  2. word.utf8.count считает байты, то есть мелкие бусины. Их 5: буква é занимает два байта.

Юникод и UTF-8

Символов в мире больше ста тысяч: латиница, кириллица, иероглифы, эмодзи. Юникод — общая таблица, в которой у каждого символа есть свой номер. Номеров в ней больше миллиона, а в один байт помещается только число до 255.

Поэтому номер символа записывают одним или несколькими байтами по особому правилу. Такое правило называют кодировкой. Swift хранит строки в кодировке UTF-8, и в ней номер символа занимает от одного до четырёх байтов:

  • латинская буква, цифра или пробел — 1 байт;
  • буква европейского языка с надстрочным знаком, как é или ü, и буква кириллицы — 2 байта;
  • китайский иероглиф — обычно 3 байта;
  • эмодзи — обычно 4 байта.

utf8 показывает строку как ряд этих байтов. Поэтому word.utf8.count — число байтов, которые занимает строка в UTF-8.

Символ Character

Тип Character хранит один символ — одну крупную бусину, сколько бы мелких под ней ни висело. Цикл for-in умеет перебирать строку по символам:

SWIFT
for ch in word {
    print(ch, terminator: " ") // c a f é
}
print()

Разберём, что здесь происходит:

  1. for ch in word проходит строку от первого символа до последнего.
  2. В константу ch по очереди попадают c, a, f и é, и у неё тип Character.
  3. é приходит целиком, одним символом, а не двумя байтами по отдельности.
  4. Пустой print() заканчивает строку вывода.

У символа, как и у строки, есть utf8: ch.utf8.count — сколько байтов занимает этот символ. Нажимайте «Следующий шаг» и следите за bytes: на трёх первых символах счётчик растёт на один, а на é — сразу на два.

ВживуюСимволы и байты строки caféшаг 1 из 11 · рамок: 1

В строке четыре символа: c, a, f и é.

Следите за bytes: на символе é счётчик растёт сразу на два

Что показал виджет:

  • c, a и f занимают по одному байту, поэтому сначала байтов столько же, сколько символов;
  • é — один символ из двух байтов, и с этого места числа расходятся;
  • в итоге count равен 4, а байтов 5.

Этот вопрос часто задают на собеседовании: почему count у строки Swift не равен числу байтов? Хороший ответ: count считает символы так, как их видит человек, а один символ может занимать в UTF-8 несколько байтов. Число байтов даёт utf8.count.

Один символ из нескольких номеров

Иногда символ складывается даже из нескольких номеров Юникода. Эмодзи 👍🏽 — это номер знака «палец вверх» и номер оттенка кожи. На экране он один, и для Swift это тоже один символ:

SWIFT
let thumb = "👍🏽"
print(thumb.count, thumb.utf8.count) // 1 8

Каждый из двух номеров занимает четыре байта, поэтому utf8.count равен 8. А count даёт 1: Character — это символ так, как его видит человек.

Здесь картинка с бусинами неточна. Крупные бусины на настоящей нитке видны сразу, и сосчитать их можно на глаз. Swift так не может: чтобы узнать count, он проходит строку от начала и по байтам решает, где кончается один символ и начинается следующий.

Частая ошибка

Длину ника проверили через utf8.count, как будто это число букв:

SWIFT
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 — символ так, как его видит человек, даже если он собран из нескольких номеров Юникода.