Skip to content

Строки и Unicode

This content is not available in your language yet.

Для англоязычного читателя эта глава была бы скучной: в ASCII символ равен байту, и говорить не о чем. Для нас всё интереснее — каждая русская буква занимает два байта, и Mojo не даёт об этом забыть.

Хорошая новость: язык не прячет сложность, а заставляет один раз принять решение — и дальше всё работает предсказуемо.

Привычное len(s) для строки в Mojo просто запрещено:

error: `len(String/StringSlice)` is not supported because Mojo strings are UTF-8 encoded, so a single length is ambiguous
Что это значит

Компилятор отказывается угадывать, что вы имели в виду под «длиной». Для строки «Привет» есть три честных ответа: 12 байт, 6 кодовых точек и 6 видимых знаков.

Как исправить

Скажите явно, что считаете:

  • s.byte_length() — сколько байт занимает строка в памяти;
  • len(s.codepoints()) — сколько символов Unicode;
  • len(s.graphemes()) — сколько знаков видит человек.

Для латиницы все три числа совпадают, поэтому в англоязычных учебниках разницы не видно. Для кириллицы — уже нет:

СтрокаБайтКодовых точекВидимых знаков
"hello"555
"Привет"1266
"👨‍👩‍👧"1851

Последняя строка — эмодзи «семья». Один видимый знак, который на самом деле склеен из трёх человечков и двух невидимых соединителей. Отсюда и разница между «кодовой точкой» и «видимым знаком».

Привычный питоновский срез не работает:

🐍 Python
s = "Привет"
print(s[0:3]) # При
🔥 Mojo
var s = String("Привет")
print(s[codepoint=0:3]) # При
print(s[byte=0:4]) # Пр

Четыре байта — это всего две русские буквы. Именно так и появляются «кракозябры»: обрезали строку на середине символа.

error: String does not support direct positional slicing like `s[a:b]` because Mojo strings are UTF-8 encoded, and the same range can mean different things. Use `s[byte=a:b]` to slice by raw UTF-8 byte positions, or `s[codepoint=a:b]` to slice by Unicode code points.
Что это значит

Срез без указания единицы измерения.

Как исправить

Решение прямо в сообщении. Практическое правило: работаете с текстом для человека — codepoint; работаете с данными, протоколом или результатом find()byte.

Резать по графемам (s[grapheme=…]) в 1.0 нельзя — только перебирать их через s.graphemes().

Тонкость, о которой стоит знать до того, как она испортит вам вечер:

def main():
var text = String("мама мыла раму")
print(text.find("мыла")) # 9
print(len("мама ".codepoints())) # 5
Результат

9 5

Подстрока начинается с шестого символа, но find вернул 9 — потому что считает байты. Это не ошибка: так результат согласован со срезом по байтам, и их можно использовать вместе:

var position = text.find("мыла")
print(text[byte=position : position + 8]) # мыла

Восемь байт — это четыре русские буквы. Если попытаться подставить тот же индекс в срез по кодовым точкам, получится сдвиг.

def main():
for symbol in String("ёжик").codepoints():
print(symbol, end="|")
print()
Результат

ё|ж|и|к|

Аналогично работают .graphemes() — по видимым знакам и .bytes() — по сырым байтам. Для эмодзи и букв с диакритикой правильный выбор почти всегда .graphemes(): только он не разрежет знак на части.

cyrillic.mojo
def main():
var text = String("мама мыла раму")
# Три разных «длины» — выбираем осознанно
print("байт:", text.byte_length())
print("символов:", len(text.codepoints()))
print("видимых знаков:", len(text.graphemes()))
# Поиск возвращает позицию в БАЙТАХ, и срез по байтам с ней согласован
var position = text.find("мыла")
print("позиция подстроки:", position)
print("вырезали:", text[byte=position : position + 8])
# Срез по символам — когда важны буквы, а не байты
print("первое слово:", text[codepoint=0:4])
# Перебор по символам
for symbol in String("ёжик").codepoints():
print(symbol, end="|")
print()
# Регистр работает и для кириллицы, включая букву ё
print(String("ёжик").upper(), String("ЁЖИК").lower())
# Эмодзи: один видимый знак может занимать много байт
var family = String("👨‍👩‍👧")
print("эмодзи — байт:", family.byte_length())
print("эмодзи — кодовых точек:", len(family.codepoints()))
print("эмодзи — видимых знаков:", len(family.graphemes()))
Результат

байт: 26 символов: 14 видимых знаков: 14 позиция подстроки: 9 вырезали: мыла первое слово: мама ё|ж|и|к| ЁЖИК ёжик эмодзи — байт: 18 эмодзи — кодовых точек: 5 эмодзи — видимых знаков: 1

Приятная часть: почти вся привычная работа со строками переносится без изменений.

def main():
var s = String("мама мыла раму")
print(s.replace("раму", "окно"))
print(s.startswith("мама"), s.endswith("раму"))
print(s.count("ма"), s.rfind("ма"))
print("мыла" in s)
print(String(" тест ").strip())
print(String("ПРИВЕТ").lower(), String("привет").upper())
Результат

мама мыла окно True True 2 4 True тест привет ПРИВЕТ

Регистр корректно работает и для кириллицы, включая букву «ё»: "ёжик".upper() даёт ЁЖИК.

Разбиение и склейка тоже на месте:

def main():
var parts = String("а,б,в").split(",")
print(len(parts))
print(String(", ").join(parts))
Результат

3 а, б, в

Многострочные строки в тройных кавычках и экранирование \n, \t работают как в Python.

def main():
var one = String("ё") # одна кодовая точка
var two = String("\u0435\u0308") # «е» плюс комбинирующие точки
print(one.byte_length(), two.byte_length())
print(len(one.graphemes()), len(two.graphemes()))
print(one == two)
Результат

2 4 1 1 False

Выглядят одинаково, занимают разное место и не равны при сравнении. Такое приходит из внешних источников: копипаст с сайтов, выгрузки из систем, данные с macOS, где исторически предпочитают второй вариант.

Готовой функции нормализации в стандартной библиотеке Mojo 1.0 найти не удалось. Практический совет: если данные приходят извне и вы их сравниваете — приводите к одной форме на входе, например средствами Python через interop, где есть unicodedata.normalize.

def main():
var result = String("")
for i in range(3):
result += String(i)
print(result)
Результат

012

Работает, но помните: каждое += может приводить к перевыделению памяти. Для длинных циклов лучше собрать части в List[String] и склеить одним join — это одна аллокация вместо сотни.

🎯 Проверь себя

Почему `text.find(«мыла»)` вернул 9, если это шестой символ строки?

Потому что find возвращает позицию в байтах, а каждая русская буква занимает два байта: «мама » — это 9 байт. Такой результат согласован со срезом s[byte=...], их удобно использовать вместе.

Сколько байт, кодовых точек и знаков в строке «👨‍👩‍👧»?

18 байт, 5 кодовых точек и 1 видимый знак. Эмодзи «семья» склеен из трёх фигур и двух невидимых соединителей — поэтому для эмодзи и букв с диакритикой перебирать нужно через .graphemes().

Что выбрать для среза: byte или codepoint?

Для текста, который читает человек, — codepoint: он не разрежет букву пополам. Для работы с данными и вместе с результатом find()byte. Смешивать их в одном вычислении нельзя.

Дальше — функции: сигнатуры, перегрузка и разница между аргументами и параметрами, которой в Python нет.

Примеры проверены на Mojo 1.0.0

Тексты курса — CC BY-NC-SA 4.0, код примеров — Apache 2.0