Почему 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.
Частые ошибки и как их избежать
Давайте разберём типичные грабли, на которые наступают новички: