Shanraq.org Shanraq.org
Текст, UTF-8 и видимые буквы
IT

Rust: с нуля до своего органайзера Урок 25 из 35

Текст, UTF-8 и видимые буквы

Урок 25. Различать байты, значения `char` и видимые буквы при работе с названием задачи.

Текст переведён с помощью ИИ.

Предварительно: уроки 3, 9 и 22–24. Если String и &str смешиваются, вернитесь к уроку 22.

Образ и опорная карта

Адрес на бумаге можно считать по строчкам, словам или буквам — это разные вопросы. Компьютер тоже хранит текст и считает его единицы по-разному. UTF-8 кодирует текст в байтах; один видимый знак может занимать несколько байтов. char в Rust — одно скалярное значение Unicode, то есть один допустимый кодовый пункт, а не всегда одна воспринимаемая человеком буква. Графема — видимая единица письма, которая может состоять из нескольких таких значений. Образ ограничен: глазами легко узнать букву, но число её байтов без правил UTF-8 не угадать.

Текст → байты .len() → скалярные значения .chars() → видимые графемы. Скажите вслух, на какой вопрос отвечает каждое число. Метод .chars() последовательно отдаёт значения char; полученную последовательность называют итератором. .count() проходит по ней и считает элементы. Это не подсчёт графем. Устройство итераторов подробно разберём позже; сейчас достаточно знать, что два вызова вместе считают значения char, не создавая новый String.

Одна буква, два байта

В organizer сохраните прежний src/main.rs, замените его целиком и выполните cargo run рядом с Cargo.toml. Другие файлы и зависимости не меняются. Сначала предскажите два числа; блок результата не включает сообщения Cargo.

В коде .len() для String возвращает число байтов, а .chars().count() — число скалярных значений Unicode. Русская Я кодируется UTF-8 двумя байтами, хотя это одно значение char и одна видимая буква.

fn main() {
    let title = String::from("Я");
    println!("Байты: {}", title.len());
    println!("Значения char: {}", title.chars().count());
}
Байты: 2
Значения char: 1

len() не ошибся: он ответил на вопрос о месте хранения, а не о длине слова для читателя. Для латинской A оба счёта равны 1, но по одному такому примеру нельзя делать правило для всего текста.

Когда видимая буква состоит из двух значений

Следующая запись "e\u{301}" — отдельный пример. \u{301} в строковом литерале означает кодовый пункт Unicode 301 в шестнадцатеричной записи; это знак ударения, добавляемый к предыдущей e. Обратная косая черта начинает специальную запись внутри кавычек, u указывает Unicode, а {301} задаёт код. Это не подстановка переменной как {title} в println!. Вместе e и знак выглядят как одна буква, но дают два значения char и три байта UTF-8.

fn main() {
    let title = "e\u{301}";
    println!("Байты: {}", title.len());
    println!("Значения char: {}", title.chars().count());
}
Байты: 3
Значения char: 2

Следовательно, .chars().count() тоже не всегда отвечает на вопрос «сколько видимых букв?». Стандартная библиотека Rust не предоставляет готовый счёт графем; для него нужна отдельная обработка текста. В нашем органайзере не будем ставить ограничение «столько-то букв», пока не выберем точное правило.

Почему нельзя взять title[0]

Индекс ноль не сообщает, хотим мы первый байт, значение char или графему. Русская буква занимает несколько байтов; взятый отдельно первый байт не является целым символом. Поэтому этот отдельный пример отклоняется с E0277:

fn main() {
    let title = String::from("Я");
    println!("{}", title[0]);
}

Для последовательного чтения значений используйте .chars(). Срезы текста из урока 23 используют байтовые границы: попытка разрезать UTF-8 внутри буквы остановит программу с panic. Пока не вычисляйте такие границы наугад. Для хранения названия по-прежнему подходит String, а для чтения — &str.

Проверка понимания

  1. Что считает "Я".len()?
  2. Считает ли .chars().count() графемы?
  3. Почему title[0] не является безопасным способом получить первую букву?

Проверьте: байты; нет, значения Unicode char; неизвестно, какую единицу текста должен означать индекс. Воспроизведите опорную карту без страницы.

Задание

Обязательное. Создайте String с Я и литерал "e\u{301}". Для каждого выведите число байтов и значений char в таком порядке: Я: байтов 2, значений char 1 и e с ударением: байтов 3, значений char 2. Рассчитайте оба числа методами, не вписывайте готовые ответы в println!.

Своё наблюдение. Замените Я на A: оба числа станут 1. Верните русский пример.

Подсказка

Для каждого текста используйте .len() и .chars().count() как два аргумента println!. Литерал имеет тип &str, но оба метода работают и для него.

Эталон после попытки

fn main() {
    let title = String::from("Я");
    let combined = "e\u{301}";
    println!("Я: байтов {}, значений char {}", title.len(), title.chars().count());
    println!(
        "e с ударением: байтов {}, значений char {}",
        combined.len(),
        combined.chars().count()
    );
}
Я: байтов 2, значений char 1
e с ударением: байтов 3, значений char 2

Результат различает место в памяти и число значений, но не объявляет их числом видимых букв. Официальный разбор UTF-8 и строк.

Предыдущий урок · Оглавление · Следующий урок

Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом

Комментарии (0)

Пока нет комментариев. Будьте первым.