Как создать функцию с переменным числом аргументов в Rust?
Решение: Макросы и трейты — два основных подхода
В 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) без квадратных скобок, то только макросы.