Перейти к содержанию

Строки, заимствование и владение в Rust

В первом уроке мы определили статистику «Меры» и получили простую JavaScript-функцию. Теперь напишем вычисление на Rust. Главное новое понятие — владение: у значения есть владелец, а функция может временно заимствовать данные без создания собственной копии. Нам достаточно понять этот механизм на тексте, чтобы перейти к браузерному модулю.

Здесь рассматриваем чистую функцию Rust, ещё без атрибута экспорта Wasm. Она принимает &str: заимствованное представление корректного UTF-8-текста. Вход должен оставаться действительным во время вызова, а результат содержит обычные числа и не сохраняет ссылку на строку после завершения операции.

Заимствование вместо передачи владения

Тип String владеет своим содержимым, а &str позволяет читать строку, не забирая её у вызывающего кода. Это не инструкция скопировать текст в новый буфер для каждого вызова. Компилятор проверяет допустимость использования ссылки. Официальная книга Rust объясняет модель владения; в нашем примере она нужна для простого чтения данных.

Полный начальный тип результата и функция находятся в src/lib.rs:

pub struct Stats {
    pub bytes: usize,
    pub scalars: usize,
    pub lines: usize,
    pub non_empty_lines: usize,
    pub max_line_scalars: usize,
}

pub fn analyze(text: &str) -> Stats {
    let mut stats = Stats {
        bytes: text.len(),
        scalars: text.chars().count(),
        lines: 0,
        non_empty_lines: 0,
        max_line_scalars: 0,
    };
    if text.is_empty() {
        return stats;
    }
    for raw in text.split('\n') {
        let row = raw.strip_suffix('\r').unwrap_or(raw);
        let length = row.chars().count();
        stats.lines += 1;
        if row.chars().any(|ch| ch != ' ' && ch != '\t') {
            stats.non_empty_lines += 1;
        }
        stats.max_line_scalars = stats.max_line_scalars.max(length);
    }
    stats
}

Метод split выдаёт представления частей строки. Мы не вызываем to_string для каждой строки: отдельный владеющий текст здесь не нужен. strip_suffix также возвращает представление исходного содержимого. Если завершающего CR нет, unwrap_or(raw) выбирает исходную строку. Такое применение unwrap_or отличается от безусловного unwrap, способного прервать выполнение при отсутствии значения.

Байты и скалярные значения

У &str метод len измеряет байты UTF-8. Итератор chars обходит Unicode-скалярные значения. Для "кот" ожидаются шесть байт и три значения. Наивный перенос JavaScript text.length в Rust text.len() дал бы разные единицы измерения, хотя названия методов выглядят похоже.

Подстрока Rust должна проходить по корректным границам UTF-8. Нельзя брать &text[0..1], рассчитывая получить первую русскую букву: один байт может быть только частью её кодирования. Наш алгоритм не индексирует текст по произвольному байту. Он использует разбиение по известному символу и итератор для подсчёта.

Если нужна первая скалярная величина, можно вызвать text.chars().next(). Для видимого пользовательского знака этого иногда недостаточно: комбинируемый акцент или emoji-последовательность потребуют понятия графемы и отдельного алгоритма. «Мера» не обещает подсчитать графемы, поэтому не прячет такую зависимость за словом «символ».

Пустой вход и завершающий перевод строки

Метод split('\n') для пустого текста возвращает часть, но наш договор требует ноль строк. Поэтому функция возвращает начальный результат до цикла. Для непустого текста с завершающим LF цикл сохраняет последнюю пустую часть. Таким образом, "a\n" имеет две строки и одну непустую.

Обратите внимание на различие с методом lines(). Он имеет собственные правила терминаторов и не подходит автоматически любому пользовательскому договору. Мы выбрали явное разбиение, чтобы одинаковое поведение было легко повторить в JavaScript. Подробности методов строк находятся в стандартной библиотеке Rust.

В "a\r\nb" исходные байты включают CR и LF, а длина первой отдельной строки после удаления CR равна одному. Это не противоречие: поля измеряют разные свойства. В интерфейсе подписи должны сообщать эти единицы, иначе пользователь будет пытаться сложить длины строк и получить исходный размер файла.

Почему не сохраняем все строки

Для пяти агрегатов нам не требуется массив всех подстрок или копия каждого значения. Цикл обновляет счётчики и максимум, используя текущую строку. Если позднее понадобится гистограмма, добавим отдельный агрегат. Хранить полную промежуточную коллекцию «на всякий случай» увеличило бы объём памяти без нового результата.

При этом алгоритм обходит некоторые данные больше одного раза: общий chars().count() и подсчёт по строкам. Это понятный исходный вариант. Его нельзя назвать самым быстрым без измерения. Прежде чем соединять всё в сложный проход, полезно подтвердить, что именно вычисление, а не загрузка, декодирование или копирование занимает существенное время.

Тип usize соответствует размерному типу платформы. Для экспортируемого браузерного договора позже преобразуем значения в согласованный набор u32, потому что вход ограничен восемью MiB. Это не универсальная рекомендация переводить любые большие счётчики в u32: граница допустима благодаря конкретному ограничению задачи.

Владение результатом

Объект Stats создаётся внутри функции и возвращается вызывающему коду. Он не содержит ссылок на входной текст. Поэтому после получения чисел источник можно освободить или использовать для другой операции, не оставляя скрытую зависимость результата от его жизни.

Если вместо чисел вернуть список заимствованных фрагментов, появится другая связь времени жизни. Такой список может быть полезен внутри Rust, но его экспорт в JavaScript потребует отдельного решения. Для нашего интерфейса достаточен компактный агрегат, который легко сериализовать в стабильный внешний формат.

Для самостоятельного рассмотрения замените исходный пример на " \t\nкот\n". По договору ожидаются три строки, одна непустая и максимальная длина три. Пробел и табуляция сохраняются при измерении длины первой строки, но не делают её непустой. Этот случай хорошо показывает, почему два поля нельзя вычислять одним и тем же условием.

Изменяемый счётчик и неизменяемый текст

Ключевое слово mut у stats разрешает менять числа результата. Оно не превращает заимствованный вход в изменяемую строку. Цикл читает данные, а каждый новый агрегат изменяется независимо. Такое разделение помогает увидеть, какие значения действительно требуют записи, вместо добавления изменяемости всем переменным по привычке.

Внутри цикла row существует достаточно долго для подсчёта. Мы не сохраняем его в глобальном состоянии и не возвращаем указатель JavaScript. Поэтому читатель может рассуждать локально: вход живёт во время вызова, строки заимствуются из него, итоговые числа не зависят от дальнейшей жизни исходного буфера.

Мы перенесли смысл статистики в Rust, сохранив заимствование и явные единицы измерения. В следующем уроке добавим ограничение входа и управляемую ошибку, а затем оформим браузерный экспорт через wasm-bindgen. Учебная функция пока не выполнялась и требует отдельного запуска при технической приёмке.

Оглавление серии