Avatar ·

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

Всем привет! Я только начал изучать Rust после C++ и Python, и столкнулся с проблемой. В C++ я могу написать функцию с переменным числом аргументов через `...` (variadic templates), а в Python — через `*args`. Например, я хочу создать функцию `sum_all`, которая складывает любое количество целых чисел: ```rust // Как мне это сделать в Rust? fn sum_all(...) -> i32 { // что-то здесь } fn main() { println!("{}", sum_all(1, 2, 3)); // 6 println!("{}", sum_all(10, 20, 30, 40)); // 100 } ``` Я читал, что Rust не поддерживает классические variadic functions, как в C. Но есть макросы? Или можно использовать срезы? Я пробовал передавать вектор, но это неудобно — каждый раз нужно создавать `vec!`. Есть ли идиоматичный способ сделать это в Rust? Мне нужно именно решение для произвольного количества аргументов одного типа (целые числа). Буду благодарен за подробный пример! P.S. Я знаю, что для суммирования есть `.sum()`, но это просто пример — мне нужно понять общий принцип. <<>>

Решение: Макросы и трейты — два основных подхода

В Rust нет прямого аналога variadic functions из C/C++, но есть два мощных идиоматичных способа: макросы для точного контроля и трейты для безопасной работы с разными типами. Рассмотрим оба.

Способ 1: Макросы (рекомендуемый для однотипных аргументов)

Макросы позволяют генерировать код с переменным числом аргументов. Самый простой пример — макрос, который принимает любое количество целых чисел:

macro_rules! sum_all {
    // Базовый случай: один аргумент
    ($x:expr) => {
        $x
    };
    // Рекурсивный случай: первый аргумент + остальные
    ($x:expr, $($rest:expr),+) => {
        $x + sum_all!($($rest),+)
    };
}

fn main() {
    println!("{}", sum_all!(1, 2, 3));        // 6
    println!("{}", sum_all!(10, 20, 30, 40)); // 100
    println!("{}", sum_all!(5));              // 5
}

Как это работает:

  • $x:expr — захватывает один аргумент как выражение.
  • $($rest:expr),+ — захватывает один или более аргументов, разделённых запятыми.
  • Макрос рекурсивно разворачивается, пока не останется один аргумент.

Для более сложных сценариев используйте макросы с повторением:

macro_rules! print_all {
    ($($arg:expr),*) => {
        $(
            println!("{}", $arg);
        )*
    };
}

fn main() {
    print_all!(1, "hello", 3.14); // Работает с разными типами
}

Способ 2: Трейты и обобщения (типобезопасный)

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

trait Summable {
    fn sum(self) -> i32;
}

// Реализация для кортежей из двух элементов
impl Summable for (i32, i32) {
    fn sum(self) -> i32 {
        self.0 + self.1
    }
}

// Реализация для кортежей из трёх элементов
impl Summable for (i32, i32, i32) {
    fn sum(self) -> i32 {
        self.0 + self.1 + self.2
    }
}

fn main() {
    println!("{}", (1, 2).sum());       // 3
    println!("{}", (1, 2, 3).sum());    // 6
}

Для произвольного количества аргументов одного типа проще всего использовать срезы или векторы:

fn sum_all_slice(args: &[i32]) -> i32 {
    args.iter().sum()
}

fn main() {
    println!("{}", sum_all_slice(&[1, 2, 3]));        // 6
    println!("{}", sum_all_slice(&[10, 20, 30, 40])); // 100
}

Продвинутый вариант: макрос с разными типами

Если нужно смешивать типы, используйте макрос с повторением и трейтами:

use std::fmt::Display;

macro_rules! print_variadic {
    ($($arg:expr),*) => {
        $(
            print!("{} ", $arg);
        )*
        println!();
    };
}

fn main() {
    print_variadic!(42, "hello", 3.14, true);
    // Вывод: 42 hello 3.14 true 
}

Рекомендации

  • Для простых случаев с одним типом — используйте срезы (&[T]).
  • Для максимальной гибкости и читаемости — макросы.
  • Для типобезопасности и сложных сценариев — трейты.
  • Избегайте макросов для критически важного кода, если можно обойтись срезами.
<<>>

Альтернативный подход: использование типажа IntoIterator и коллекций

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

fn sum_all(iter: I) -> i32
where
    I: IntoIterator,
{
    iter.into_iter().sum()
}

fn main() {
    // Через массив (самый лёгкий способ)
    println!("{}", sum_all([1, 2, 3]));
    
    // Через вектор
    println!("{}", sum_all(vec![10, 20, 30, 40]));
    
    // Через диапазон
    println!("{}", sum_all(1..=5));
}

Преимущества:

  • Не требует макросов — работает с любым типом, реализующим IntoIterator.
  • Поддерживает ленивые вычисления (диапазоны, итераторы).
  • Компилятор проверяет типы на этапе компиляции.

Недостаток: нужно явно указывать коллекцию (массив [1, 2, 3] или vec![1, 2, 3]). Но это на самом деле не так уж и неудобно, а код становится более явным и безопасным.

Для вашего примера с суммированием это идеальное решение. Если же вам критично именно синтаксис sum_all(1, 2, 3) без квадратных скобок, то только макросы.