Первый символ строки в Rust: разбираем популярный вопрос

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Первый символ строки в Rust: разбираем популярный вопрос со Stack Overflow

Вы когда-нибудь задумывались, почему простой, казалось бы, вопрос "Как получить первый символ из строки?" в Rust вызывает столько споров и обсуждений? На Stack Overflow этот вопрос собрал тысячи просмотров и десятки ответов. И это не случайно.

В отличие от многих других языков, где строки — это просто последовательности байт, Rust подходит к этому вопросу с особой тщательностью. Причина кроется в Unicode и UTF-8. В Rust строки (тип &str и String) — это всегда корректные последовательности UTF-8. А значит, один "символ" может занимать от 1 до 4 байт. Просто взять первый байт — неверно, это может быть лишь частью символа.

В этой статье мы разберем все основные способы получить первый символ строки в Rust, сравним их производительность, удобство использования и разберем подводные камни. Поехали!

Почему нельзя просто взять [0]?

Если вы пришли из мира C, Java или Python, ваша первая мысль может быть такой:

let s = "Привет, мир!";
let first = s[0]; // Ошибка!

В Rust это не скомпилируется. Строки индексируются по байтам, а не по символам. s[0] попытается получить первый байт. Для латиницы это сработает (символ 'H' занимает 1 байт), но для кириллицы 'П' занимает 2 байта. Rust не позволяет вам случайно получить половину символа — это одна из его ключевых фич безопасности.

Вместо этого нам нужно использовать специальные методы, которые учитывают Unicode.

Метод 1: .chars().next() — самый безопасный и понятный

Это наиболее рекомендуемый способ получить первый символ. Метод .chars() возвращает итератор по символам (тип char), где каждый символ — это полноценная Unicode-единица. Затем мы берем первый элемент итератора.

fn main() {
    let text = "Привет";
    
    let first_char: Option<char> = text.chars().next();
    
    match first_char {
        Some(c) => println!(&quot;Первый символ: {}&quot;, c),
        None => println!(&quot;Строка пустая&quot;),
    }
}

Результат: Первый символ: П

Преимущества:

  • Работает с любыми Unicode-символами, включая эмодзи, иероглифы и спецсимволы.
  • Возвращает Option<char>, что заставляет вас обрабатывать случай пустой строки.
  • Очень читаемый код.

Недостатки:

  • Итератор .chars() проходит по всей строке (O(n) в худшем случае), даже если вам нужен только первый символ. Однако на практике это очень быстро, так как итератор ленивый.

Метод 2: .bytes().next() — для работы с байтами (осторожно!)

Иногда вам действительно нужен первый байт, а не символ. Например, при работе с бинарными протоколами или ASCII-текстом. В этом случае используйте .bytes().

fn main() {
    let text = &quot;Hello&quot;;
    
    let first_byte: Option<u8> = text.bytes().next();
    
    match first_byte {
        Some(b) => println!(&quot;Первый байт: {} (символ '{}')&quot;, b, b as char),
        None => println!(&quot;Строка пустая&quot;),
    }
}

Результат: Первый байт: 72 (символ 'H')

Предупреждение: Если строка содержит многобайтовые символы (кириллица, эмодзи), первый байт не будет полным символом. Для строки "Привет" .bytes().next() вернет 208, что является первым байтом двухбайтового символа 'П'. Это не то же самое, что сам символ.

Когда использовать: Только когда вы уверены, что работаете с ASCII-строкой, или вам нужен именно байт для низкоуровневых операций.

Метод 3: Срез по границам символов — &s[..]

Rust позволяет брать срезы строк, но только по границам символов. Если попытаться взять срез, который разрывает символ, программа запаникует (panic).

fn main() {
    let text = &quot;Привет&quot;;
    
    // Берем срез от начала до 2 байт (символ 'П' занимает 2 байта)
    let first_char_slice = &text[..2];
    println!(&quot;Срез: {}&quot;, first_char_slice);
    
    // А вот так делать НЕЛЬЗЯ — panic!
    // let bad_slice = &text[..1]; // panic: byte index 1 is not a char boundary
}

Результат: Срез: П

Преимущества:

  • Очень быстрый — не нужно создавать итератор.
  • Не выделяет память, просто создает новый срез.

Недостатки:

  • Вы должны знать, сколько байт занимает первый символ. Для ASCII это 1, для кириллицы — 2, для некоторых эмодзи — 4.
  • Легко ошибиться и вызвать panic.
  • Не подходит для строк с неизвестной кодировкой.

Как это сделать правильно? Используйте метод .char_indices(), который возвращает пары (позиция в байтах, символ):

fn main() {
    let text = &quot;Привет&quot;;
    
    if let Some((start, first_char)) = text.char_indices().next() {
        let end = start + first_char.len_utf8();
        let slice = &text[start..end];
        println!(&quot;Первый символ (срез): {}&quot;, slice);
    }
}

Это безопасно, но сложнее, чем .chars().next().

Сравнение производительности

Давайте проведем небольшой бенчмарк. Возьмем длинную строку (1 млн символов) и измерим время получения первого символа разными способами.

use std::time::Instant;

fn main() {
    // Создаем длинную строку из 'A'
    let long_string = &quot;A&quot;.repeat(1_000_000);
    
    // 1. .chars().next()
    let start = Instant::now();
    for _ in 0..1000 {
        let _first = long_string.chars().next();
    }
    println!(&quot;.chars().next(): {:?}&quot                

Блоги

Рекомендаций по книгам