Строки и Unicode
Для англоязычного читателя эта глава была бы скучной: в 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" | 5 | 5 | 5 |
"Привет" | 12 | 6 | 6 |
"👨👩👧" | 18 | 5 | 1 |
Последняя строка — эмодзи «семья». Один видимый знак, который на самом деле склеен из трёх человечков и двух невидимых соединителей. Отсюда и разница между «кодовой точкой» и «видимым знаком».
Срезы: обязательно указать единицу
Заголовок раздела «Срезы: обязательно указать единицу»Привычный питоновский срез не работает:
s = "Привет"print(s[0:3]) # При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())) # 59 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(): только он не разрежет знак на части.
Всё вместе
Заголовок раздела «Всё вместе»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
Что работает как в Python
Заголовок раздела «Что работает как в Python»Приятная часть: почти вся привычная работа со строками переносится без изменений.
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 нет.
Тексты курса — CC BY-NC-SA 4.0, код примеров — Apache 2.0