Почему в Rust превращение первой буквы строки в заглавную — такая нетривиальная задача?
Почему в Rust нет простого .capitalize()
Отличный вопрос! Вы столкнулись с одним из ключевых отличий Rust от языков со сборкой мусора: Rust ставит во главу угла корректность и производительность, а не удобство написания кода для 90% случаев. Проблема в том, что "сделать первую букву заглавной" — это на самом деле сложная операция, если учитывать Unicode.
Основные подводные камни
- Изменение длины строки: как вы заметили с
"straße"→"STRASSE"(один символ превращается в два). В Python это скрыто за кулисами, а Rust хочет явности. - Графемные кластеры: эмодзи или буквы с диакритикой (например,
"é") могут состоять из нескольких Unicode-скаляров. Разбивка по.chars()может разорвать такой кластер. - Локаль: в турецком языке буква
"i"заглавная —"İ"(с точкой), а не"I". Rust не учитывает локаль по умолчанию.
Идиоматичное решение с крейтом unicode_casing
Лучший способ — использовать специализированный крейт. Вот пример с unicode_casing (часть экосистемы ICU4X):
// Cargo.toml
// [dependencies]
// unicode_casing = "0.1"
use unicode_casing::CharExt;
fn capitalize_first(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
Some(first) => {
// to_uppercase() возвращает итератор, т.к. результат может быть длиннее 1 символа
let upper: String = first.to_uppercase().collect();
upper + chars.as_str()
}
None => String::new(),
}
}
fn main() {
assert_eq!(capitalize_first("hello"), "Hello");
assert_eq!(capitalize_first("straße"), "Straße"); // Правильно! ß → SS
assert_eq!(capitalize_first("привет"), "Привет");
// Для эмодзи: первый символ-эмодзи остаётся без изменений (как и ожидается)
assert_eq!(capitalize_first("😀hello"), "😀hello");
}
Почему это работает корректно?
to_uppercase()возвращает итератор, а не один char, потому что результат может быть длиннее (как в случае с ß).chars.as_str()возвращает оставшуюся часть строки без выделения новой памяти (это срез исходной строки).- Мы не трогаем остальные символы, поэтому графемные кластеры не разрываются.
Что делать, если не хочется добавлять крейт?
Стандартная библиотека Rust намеренно не включает функции, зависящие от Unicode-свойств (кроме базовых), чтобы держать размер стандартной библиотеки минимальным. Но вы можете использовать тот же подход со встроенным to_uppercase() — он уже корректно обрабатывает изменение длины. Главное — не пытайтесь взять s[0] (индексация по байтам сломает многобайтовые символы) и не используйте .nth(0) без учёта графем.
Альтернатива для полного контроля
Если вам нужно учитывать локаль (например, турецкий язык), используйте крейт icu (ICU4X):
use icu::locid::locale;
use icu::casemap::CaseMapper;
let mapper = CaseMapper::new();
let result = mapper.uppercase("istanbul", &locale!("tr").into());
assert_eq!(result, "İSTANBUL");
В итоге: Rust не делает "простых" вещей, потому что они часто оказываются сложными. Но он даёт вам инструменты, чтобы сделать их правильно, явно и без сюрпризов в продакшне.
<<Более простой (но менее корректный) способ
Если вы уверены, что ваши строки содержат только ASCII-символы (латиница, цифры, базовые знаки препинания), то можно использовать упрощённый подход без внешних крейтов. Это будет быстрее и не потребует зависимостей, но сломается на любом не-ASCII символе.
fn capitalize_first_ascii(s: &str) -> String {
let mut bytes = s.as_bytes().to_vec();
if let Some(first) = bytes.first_mut() {
if first.is_ascii_lowercase() {
*first = first.to_ascii_uppercase();
}
}
// Безопасно, т.к. мы не меняем длину и не нарушаем UTF-8
String::from_utf8(bytes).unwrap()
}
fn main() {
assert_eq!(capitalize_first_ascii("hello"), "Hello");
// Некорректно для Unicode:
assert_eq!(capitalize_first_ascii("straße"), "Straße"); // ß не изменился!
assert_eq!(capitalize_first_ascii("привет"), "привет"); // Кириллица не тронута
}
Когда это уместно?
- Вы обрабатываете только английские идентификаторы (например, имена полей в JSON).
- Производительность критична, и вы готовы пожертвовать корректностью для редких случаев.
- Вы пишете код для embedded-систем, где каждый крейт на счету.
Итог
Выбирайте подход в зависимости от контекста: для пользовательского ввода всегда используйте Unicode-корректное решение с крейтом. Для внутренних форматов данных (где вы контролируете содержимое) ASCII-версия может быть оправдана. Rust даёт вам выбор, а не навязывает "универсальное" решение, которое может быть неверным в 5% случаев.