Avatar ·

Как реализовать аргументы функций по умолчанию в Rust?

Привет, коллеги! Я новичок в Rust и перехожу с Python, где можно легко писать функции с аргументами по умолчанию:

def greet(name, greeting="Привет"):
    print(f"{greeting}, {name}!")

Теперь пытаюсь сделать то же самое в Rust, но компилятор ругается. Вот мой код:

fn greet(name: &str, greeting: &str = "Привет") {
    println!("{}, {}!", greeting, name);
}

Компилятор выдаёт ошибку: expected identifier, found `=`. Я понимаю, что в Rust нет синтаксиса для аргументов по умолчанию, но как тогда решить эту задачу? Мне нужно, чтобы функция могла работать как с одним аргументом (тогда приветствие по умолчанию "Привет"), так и с двумя.

Я уже пробовал использовать перегрузку функций, но Rust её не поддерживает. Пытался сделать две функции с разными именами — это работает, но код получается громоздким. Есть ли более идиоматичный способ? Может быть, использовать структуры или перечисления? Или есть какой-то трюк с типами-опциями?

Заранее спасибо за помощь! Очень хочется писать чистый и лаконичный код на Rust.

<<>>

Решение с использованием типа Option<T> и паттерна Builder

В Rust действительно нет встроенной поддержки аргументов по умолчанию, как в Python или JavaScript. Однако есть несколько идиоматичных способов достичь того же результата. Рассмотрим два основных подхода.

1. Использование Option<T> (простой способ)

Самый простой метод — использовать тип Option<&str> и обрабатывать случай None внутри функции:

fn greet(name: &str, greeting: Option<&str>) {
    let greeting = greeting.unwrap_or("Привет");
    println!("{}, {}!", greeting, name);
}

fn main() {
    // Вызов с двумя аргументами
    greet("Анна", Some("Здравствуйте"));
    // Вызов с одним аргументом (используется значение по умолчанию)
    greet("Борис", None);
}

Преимущества: простота, минимум кода.
Недостатки: вызывающий код должен явно писать Some(...) или None, что не очень удобно.

2. Паттерн Builder (рекомендуемый для сложных случаев)

Для функций с множеством параметров лучше использовать структуру-билдер:

struct GreetOptions<'a> {
    greeting: &'a str,
    punctuation: &'a str,
}

impl<'a> Default for GreetOptions<'a> {
    fn default() -> Self {
        GreetOptions {
            greeting: "Привет",
            punctuation: "!",
        }
    }
}

fn greet(name: &str, options: GreetOptions) {
    println!("{}{} {}", options.greeting, options.punctuation, name);
}

// Удобный макрос для вызова
macro_rules! greet {
    ($name:expr) => {
        greet($name, GreetOptions::default())
    };
    ($name:expr, $($key:ident: $value:expr),*) => {{
        let mut opts = GreetOptions::default();
        $(opts.$key = $value;)*
        greet($name, opts)
    }};
}

fn main() {
    // Использование со значениями по умолчанию
    greet!("Елена");
    
    // Использование с кастомными настройками
    greet!("Иван", greeting: "Приветствую", punctuation: "!!!");
}

3. Использование трейта Into<Option<T>> (продвинутый способ)

Можно сделать API более удобным, используя обобщённые типы:

fn greet<T: Into<Option<&'a str>>>(name: &str, greeting: T) {
    let greeting: Option<&str> = greeting.into();
    let greeting = greeting.unwrap_or("Привет");
    println!("{}, {}!", greeting, name);
}

// Реализация для &str (позволяет передавать строку напрямую)
impl<'a> From<&'a str> for Option<&'a str> {
    fn from(s: &'a str) -> Self {
        Some(s)
    }
}

fn main() {
    greet("Ольга", "Здравствуйте"); // явное приветствие
    greet("Пётр", None);            // значение по умолчанию
}

Рекомендация: Для простых случаев используйте Option с unwrap_or. Для сложных API с множеством опций — паттерн Builder. Rust ценит явность и безопасность, поэтому отсутствие аргументов по умолчанию — это осознанный дизайн языка.

<<>>

Альтернативный подход: перегрузка через разные имена функций

Хотя Rust не поддерживает перегрузку функций в классическом смысле, вы можете создать несколько функций с разными именами, которые вызывают одну основную. Это особенно полезно, когда нужно строго разграничить API:

fn greet_with(name: &str, greeting: &str) {
    println!("{}, {}!", greeting, name);
}

fn greet(name: &str) {
    greet_with(name, "Привет");
}

fn main() {
    greet("Мария");              // Использует "Привет" по умолчанию
    greet_with("Дмитрий", "Добрый вечер"); // Явное приветствие
}

Плюсы:

  • Максимально явный и читаемый код
  • Не требует использования Option или сложных структур
  • Хорошо документируется — имена функций говорят сами за себя

Минусы:

  • Может привести к размножению функций при большом количестве параметров
  • Не подходит для случаев, когда нужно комбинировать несколько опциональных параметров

Этот подход особенно хорош для публичных API библиотек, где важна ясность и предсказуемость. Например, в стандартной библиотеке Rust часто используются функции вроде new() и with_capacity() для разных вариантов инициализации.