Rust: Как сравнить String со строковыми литералами (match)

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

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

Начать курс

Почему Rust не даёт просто сравнить String с "привет"?

Если вы только начинаете изучать Rust и уже столкнулись с необходимостью сопоставить переменную типа String с каким-нибудь строковым литералом вроде "hello" или "world", то наверняка видели странную ошибку компилятора. Вы пишете что-то вроде:

let user_input = String::from("start");
match user_input {
    "start" => println!("Начинаем!"),
    "stop"  => println!("Останавливаемся!"),
    _       => println!("Неизвестная команда"),
}

И компилятор выдаёт что-то вроде: "expected struct `String`, found `&str`". Знакомо?

На Stack Overflow вопрос "How to match a String against string literals?" набрал тысячи просмотров — это настоящая боль новичков. В этой статье мы разберём, почему так происходит, и покажем три простых способа решить проблему. Вы узнаете, как работает система владения и заимствования в Rust на практике, и перестанете бояться ошибок компилятора.

Природа конфликта: String vs &str

Чтобы понять проблему, нужно вспомнить основы. В Rust есть два основных типа для работы с текстом:

  • String — это владеющая строка, которая хранится в куче. Она может изменяться, расти и уменьшаться. Пример: let s = String::from("hello");
  • &str — это строковый срез. Он является ссылкой на часть строки (или на строковый литерал, который хранится прямо в бинарнике программы). Пример: "hello" — это именно &'static str.

Когда вы пишете match user_input { "start" => ... }, вы пытаетесь сравнить String с &str. Компилятор говорит: "Я не знаю, как их сопоставлять напрямую, типы разные!". И это хорошо — Rust защищает вас от неявных преобразований, которые могли бы привести к потере производительности или скрытым багам.

Но как же тогда быть? Есть три простых и элегантных решения.

Решение 1: Заимствование через as_str()

Самый прямой и понятный способ — получить из String строковый срез (&str) с помощью метода as_str(). Этот метод возвращает ссылку на содержимое строки, не копируя её. Выглядит это так:

fn main() {
    let command = String::from("start");

    match command.as_str() {
        "start" => println!("Запуск системы..."),
        "stop"  => println!("Остановка..."),
        "pause" => println!("Пауза..."),
        _       => println!("Неизвестная команда: {}", command),
    }
}

Почему это работает? as_str() возвращает &str, который теперь можно сравнивать с литералами. Rust видит, что оба операнда — &str, и сопоставление с образцом работает идеально.

Плюсы: простота, читаемость, нулевые накладные расходы (заимствование, а не копирование).

Минусы: нужно помнить про вызов метода. Но это мелочь.

Решение 2: Сравнение в условии if

Если вам не нужна вся мощь match, можно использовать обычные if с методом eq() или оператором ==. Rust умеет сравнивать String и &str через трейт PartialEq, который реализован для этой пары типов.

fn main() {
    let user_input = String::from("admin");

    if user_input == "admin" {
        println!("Доступ разрешён!");
    } else if user_input == "guest" {
        println!("Гостевой доступ");
    } else {
        println!("Неверный логин");
    }
}

Это работает, потому что компилятор автоматически приводит String к &str при сравнении. Но в match такого приведения нет — там требуется точное совпадение типов.

Когда использовать: если у вас 2-3 варианта и нет сложных вложенных условий. Для длинных списков лучше match.

Решение 3: Преобразование литералов в String

Можно пойти с другой стороны — превратить строковые литералы в String. Это делается с помощью to_string() или String::from():

fn main() {
    let action = String::from("delete");

    match action {
        ref s if s == "create" => println!("Создание..."),
        ref s if s == "delete" => println!("Удаление..."),
        ref s if s == "update" => println!("Обновление..."),
        _ => println!("Неизвестное действие"),
    }
}

Но это не самый красивый вариант. Лучше использовать match с as_str(), как в первом решении. Преобразование литералов в String оправдано только если вы собираетесь хранить эти строки в коллекции или изменять их.

Важно: не делайте match user_input { "start".to_string() => ... }. Это создаст новую String на каждой проверке — напрасная аллокация в куче. Rust это не запрещает, но это плохая практика.

А как же enum? Лучшая практика для команд

Если вы пишете что-то серьёзное (парсер команд, обработчик сообщений), подумайте о том, чтобы не сравнивать строки вообще. В Rust принято преобразовывать строки в перечисление enum на этапе ввода. Это даёт:

  • Безопасность типов (компилятор проверит, что вы обработали все варианты)
  • Производительность (сравнение целых чисел, а не строк)
  • Читаемость кода

Вот как это выглядит:

#[derive(Debug, PartialEq)]
enum Command {
    Start,
    Stop,
    Restart,
    Unknown(String),
}

impl Command {
    fn from_str(input: &str) -> Self {
        match input {
            "start"   => Command::Start,
            "stop"    => Command::Stop,
            "restart" => Command::Restart,
            other     => Command::Unknown(other.to_string()),
        }
    }
}

fn main() {
    let raw_input = String::from("start");
    let cmd = Command::from_str(raw_input.as_str());

    match cmd {
        Command::Start => println!("Стартуем!"),
        Command::Stop => println!("Стоп!"),
        Command::Restart => println!("Рестарт!"),
        Command::Unknown(s) => println!("Неизвестная команда: {}", s),
    }
}

Обратите внимание: from_str принимает &str, поэтому мы снова используем as_str(). Но теперь вся логика сравнения вынесена в одно место, а основной код работает с безопасным enum.

Частые ошибки и как их избежать

Давайте разберём типичные грабли, на которые наступают новички:

Ошибка 1:

Блоги

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