Introduction
Varargs were introduced in Java 5 and provide a short-hand for methods that support an arbitrary number of parameters of one type. This is signified by three dots (...
).
public boolean match(TokenType... types) {
for (TokenType type : types) {
if (check(type)) {
return true;
}
}
return false;
}
And are called as a comma separated list.
match(PLUS, MINUS)
Rust doesn't have a direct equivalent to Java's ...
(varargs) syntax. But there are 4 ways we can achieve the same result.
1. Vectors (Don't use)
We can use vectors to pass the list of arguments.
fn match_token(types: Vec<TokenType>) -> bool {
for &token_type in types {
if check(token_type) {
return true;
}
}
false
}`
And then call it like -
match_token(vec![PLUS, MINUS])
2. Slices (Best)
We can use slices (&[T]) to pass an arbitrary number of arguments.
fn match_token(types: &[TokenType]) -> bool {
for &token_type in types {
if check(token_type) {
return true;
}
}
false
}
Usage is -
match_token(&[PLUS, MINUS])
Using &[T] is better than using Vec in most cases.
Avoids heap allocation: vec![…] creates a new vector on the heap every time you call the function. Whereas, &[T] is a borrowed reference to a static array — no allocation involved.
Better performance: &[T] just points to existing data, no copying or memory allocation. Vec! has to allocate, construct, and then drop the vector.
More idiomatic for fixed/short lists: We're not modifying the list, just reading it — so a reference is semantically correct.
3. Macros (Less common)
Macros can give Java-style variadic syntax, but are usually overkill unless you need extreme flexibility.
macro_rules! match_token {
(expr, $( $token_type:expr ),+ ) => {{
let mut matched = false;
$(
if check($token_type) {
matched = true;
break;
}
)+
matched
}};
}
Usage is -
match_token!(PLUS, MINUS)
Macros are powerful, but they come with tradeoffs like harder debugging and less type inference. Use macros if you want concise call sites or compile-time efficiency. Prefer slices (&[T]) if you want more readable, testable, and debuggable code.
4. Const Generics (Cleanest!)
This approach is very clean and performant but only works with fixed-length arrays - i.e. size known at compile time. This is fine if the list is hardcoded at the call sites.
pub fn match_token<const N: usize>(types: [TokenType; N]) -> bool {
for token_type in types {
if check(token_type) {
return true;
}
}
false
}
Here <const N: usize> lets Rust know the size of the array at compile time, and [TokenType; N] is the fixed-size array. The usage is -
match_token([PLUS, MINUS])
I think this is the cleanest approach! We do not need to take a reference (&[TokenType]) — it's owned, but with zero allocation, and inline. This avoids runtime heap allocations and allows full compile-time optimization.
Conclusion
Use slices when the number of arguments is not known at compile time, and use const generics when the size of the array is known at compile time.