Default function arguments в Rust: элегантные решения без перегрузок

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

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

Начать курс

Почему в Rust нет аргументов по умолчанию — и как это обойти

Если вы пришли в Rust из Python, JavaScript или C++, то наверняка столкнулись с неожиданностью: в Rust нет встроенной поддержки аргументов по умолчанию. Нельзя просто написать fn greet(name = "мир") — компилятор скажет вам «нет».

Но не спешите расстраиваться! Rust предлагает другие, более гибкие и безопасные подходы. В этом туториале мы разберём 3 проверенных способа реализовать поведение аргументов по умолчанию. Вы узнаете, как использовать Option<T>, структуры с настройками и паттерн Builder — и какой метод выбрать для вашей задачи.

Статья основана на реальном вопросе с Stack Overflow (более 50 000 просмотров) — мы адаптировали его для образовательного формата. Поехали!

Способ 1. Используем Option<T> — самый простой вариант

Самый прямолинейный способ — принимать аргументы как Option<T> и внутри функции подставлять значения по умолчанию.

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

fn main() {
    greet(Some("Анна")); // Привет, Анна!
    greet(None);         // Привет, мир!
}

Плюсы:

  • Минимум кода
  • Понятно даже новичку
  • Не требует дополнительных структур

Минусы:

  • Каждый вызов требует оборачивания в Some(...) или None
  • Если параметров несколько — код становится громоздким

Этот подход отлично подходит для функций с 1-2 необязательными параметрами. Но если их больше — читаемость страдает.

Способ 2. Структура с настройками — чисто и масштабируемо

Когда параметров становится 3, 5 или 10, на помощь приходит структура-конфигурация. Вы создаёте отдельный тип, который хранит все настройки, и реализуете для него трейт Default.

struct GreetConfig {
    name: String,
    greeting: String,
    excitement_level: u8,
}

impl Default for GreetConfig {
    fn default() -> Self {
        Self {
            name: "мир".to_string(),
            greeting: "Привет".to_string(),
            excitement_level: 1,
        }
    }
}

fn greet(config: GreetConfig) {
    let excitement = "!".repeat(config.excitement_level as usize);
    println!("{}, {}{}", config.greeting, config.name, excitement);
}

fn main() {
    // Используем значения по умолчанию
    greet(GreetConfig::default());
    // Вывод: Привет, мир!

    // Меняем только то, что нужно
    greet(GreetConfig {
        name: "Анна".to_string(),
        ..Default::default()
    });
    // Вывод: Привет, Анна!

    // Полностью кастомный вызов
    greet(GreetConfig {
        name: "Мир".to_string(),
        greeting: "Здравствуй".to_string(),
        excitement_level: 3,
    });
    // Вывод: Здравствуй, Мир!!!
}

Плюсы:

  • Максимальная гибкость
  • Можно добавлять новые поля без изменения сигнатуры функции
  • Синтаксис ..Default::default() очень читаемый

Минусы:

  • Требует больше boilerplate-кода
  • Нужно явно реализовывать трейт Default

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

Способ 3. Builder Pattern — для тех, кто любит цепочки

Паттерн Builder — это классика Rust-экосистемы. Вы создаёте структуру-строитель, которая накапливает параметры, а в конце вызывает метод build().

struct Greeter {
    name: String,
    greeting: String,
    excitement_level: u8,
}

struct GreeterBuilder {
    name: Option<String>,
    greeting: Option<String>,
    excitement_level: Option<u8>,
}

impl GreeterBuilder {
    fn new() -> Self {
        Self {
            name: None,
            greeting: None,
            excitement_level: None,
        }
    }

    fn name(mut self, name: &str) -> Self {
        self.name = Some(name.to_string());
        self
    }

    fn greeting(mut self, greeting: &str) -> Self {
        self.greeting = Some(greeting.to_string());
        self
    }

    fn excitement(mut self, level: u8) -> Self {
        self.excitement_level = Some(level);
        self
    }

    fn build(self) -> Greeter {
        Greeter {
            name: self.name.unwrap_or_else(|| "мир".to_string()),
            greeting: self.greeting.unwrap_or_else(|| "Привет".to_string()),
            excitement_level: self.excitement_level.unwrap_or(1),
        }
    }
}

fn main() {
    let greeter = GreeterBuilder::new()
        .name("Анна")
        .excitement(3)
        .build();

    let excitement = "!".repeat(greeter.excitement_level as usize);
    println!("{}, {}{}", greeter.greeting, greeter.name, excitement);
    // Вывод: Привет, Анна!!!
}

Плюсы:

  • Максимальная читаемость даже при 10+ параметрах
  • Можно добавлять валидацию в build()
  • Иммутабельные цепочки вызовов — безопасно

Минусы:

  • Много кода для простых случаев
  • Избыточен для 1-2 параметров

Builder — стандарт де-факто для сложных конфигураций в Rust (вспомните std::process::Command или reqwest).

Что выбрать? Практические рекомендации

Вот простая шкала, которая поможет принять решение:

  • 1-2 необязательных параметра — используйте Option<T>
  • 3-5 параметров — структура с Default
  • 6+ параметров или сложная логика — Builder Pattern

Но есть и альтернативные методы, о которых стоит знать:

  • Макросы — можно написать макрос, который генерирует код для аргументов по умолчанию. Минус: сложность поддержки.
  • Крейт derive_builder — автоматически генерирует Builder для вашей структуры. Отлично подходит для больших проектов.
  • Перегрузка через трейты — теоретически можно определить несколько трейтов с разными сигнатурами, но это скорее академический интерес.

Почему Rust не добавля

Блоги

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