Avatar ·

Как ничего не делать в "пустом" или "остаточном" случае при сопоставлении строки с образцом в Rust?

Всем привет! Я только начал изучать Rust и столкнулся с проблемой, которая кажется мне очень странной. Я пишу простую функцию, которая принимает строку и что-то делает только для определённых значений. Для всех остальных значений я хочу, чтобы ничего не происходило, то есть просто пропустить этот случай. Вот мой код: ```rust fn process_command(command: &str) { match command { "start" => println!("Starting..."), "stop" => println!("Stopping..."), "restart" => println!("Restarting..."), // здесь я хочу ничего не делать для всех остальных команд } } ``` Но компилятор выдаёт ошибку: `non-exhaustive patterns: `_` not covered`. Я понимаю, что нужно добавить catch-all pattern, но я не хочу ничего выводить или выполнять. Когда я добавляю `_ => {}` или `_ => ()`, это работает, но мне кажется, что есть более идиоматичный способ. Я пробовал писать просто `_ =>` без тела, но это синтаксическая ошибка. Подскажите, как правильно обработать "пустой" case? Может быть, есть какой-то специальный синтаксис для "ничего не делать"? Или `_ => {}` — это нормально? Заранее спасибо! P.S. Я знаю, что можно использовать `if let`, но мне нужно именно `match`, потому что случаев много и они разные. <<>>

Правильный и идиоматичный способ: пустой блок или единичный тип

В Rust нет специального "пустого" синтаксиса для ветки match. Использование _ => {} (пустой блок) или _ => () (единичный тип) — это абсолютно нормальный и идиоматичный подход. Компилятор оптимизирует такие пустые ветки, и они не влияют на производительность.

fn process_command(command: &str) {
    match command {
        "start" => println!("Starting..."),
        "stop" => println!("Stopping..."),
        "restart" => println!("Restarting..."),
        _ => {} // Ничего не делаем для всех остальных строк
    }
}

Почему нельзя просто опустить ветку?

В Rust каждое выражение match должно быть исчерпывающим (exhaustive). Это значит, что компилятор проверяет, покрыты ли все возможные варианты. Для строки &str (которая может содержать любое значение) невозможно перечислить все варианты, поэтому обязателен catch-all pattern _. Это одна из ключевых фич безопасности Rust — вы не можете случайно забыть обработать какой-то случай.

Альтернативы для особых случаев

Хотя _ => {} — стандартное решение, вот несколько ситуаций, когда стоит рассмотреть другие подходы:

  • Использование if let для одного интересующего случая — если вам нужно обработать только одно значение (или один паттерн), if let более лаконичен:
fn process_command(command: &str) {
    if let "start" = command {
        println!("Starting...");
    }
    // Для всех остальных команд — ничего
}
  • Использование макроса cfg! или атрибутов — если ветка должна быть пустой только в определённых условиях сборки, можно добавить #[allow(unused_variables)]:
fn process_command(command: &str) {
    match command {
        "start" => println!("Starting..."),
        #[cfg(debug_assertions)]
        "debug" => println!("Debug mode"),
        _ => {} // В релизе просто игнорируем
    }
}
  • Возврат из функции — если ваша функция возвращает что-то, пустая ветка должна возвращать значение того же типа. Для () это работает автоматически:
fn process_command(command: &str) -> Result<(), String> {
    match command {
        "start" => {
            println!("Starting...");
            Ok(())
        }
        "stop" => {
            println!("Stopping...");
            Ok(())
        }
        _ => Ok(()) // Возвращаем Ok, но ничего не делаем
    }
}

Когда пустой блок — плохая идея?

Избегайте _ => {} если вы ожидаете, что в будущем появятся новые значимые варианты. В этом случае лучше использовать _ => unreachable!() или _ => panic!("unexpected command: {command}"), чтобы явно указать, что этот случай не должен возникать. Тогда, если он всё же возникнет, программа упадёт с понятной ошибкой, а не будет молча игнорировать проблему.

Вывод

Ваш код с _ => {} — это правильное решение. Не пытайтесь избавиться от этой ветки — она обеспечивает безопасность и читаемость. Rust не позволяет вам "ничего не делать" без явного указания, что вы осознанно игнорируете остальные варианты. Это фича, а не баг.

<<>>

Использование паттерна-отрицания и охранников (guards) для тонкой настройки

Если у вас есть много конкретных вариантов, но вы хотите явно указать, что некоторые из них должны игнорироваться, можно использовать паттерны с условиями. Это полезно, когда вы хотите обработать не все остальные значения, а только определённые подмножества.

fn process_command(command: &str) {
    match command {
        "start" => println!("Starting..."),
        "stop" => println!("Stopping..."),
        // Игнорируем команды, начинающиеся с "debug_"
        c if c.starts_with("debug_") => {},
        // Игнорируем пустые строки
        "" => {},
        // Для всех остальных — тоже ничего, но с явным комментарием
        _ => {}
    }
}

Такой подход делает код самодокументируемым: читатель сразу видит, какие именно случаи намеренно игнорируются. Это особенно полезно, когда вы работаете с перечислениями (enum) или сложными структурами, где некоторые варианты не имеют смысла для данной логики.

Ещё один трюк — использовать вложенный match или match внутри if let для разделения логики:

fn process_command(command: &str) {
    let is_known = match command {
        "start" | "stop" | "restart" => true,
        _ => false,
    };
    
    if is_known {
        match command {
            "start" => println!("Starting..."),
            "stop" => println!("Stopping..."),
            "restart" => println!("Restarting..."),
            _ => unreachable!() // Безопасно, так как is_known гарантирует
        }
    }
    // Если команда неизвестна — просто ничего не делаем
}

Этот подход разделяет проверку "известности" команды и её обработку, что может улучшить читаемость в сложных сценариях. Но для простых случаев _ => {} остаётся самым прямым и эффективным решением.