将元组展开为函数参数是否可行?

61

如果我想解包元组并将其作为参数传递,是否有办法做到这一点:

//Does not compile
fn main() {
    let tuple = (10, Vec::new());
    foo(tuple);
}
fn foo(a: i32, b: Vec<i32>) {
    //Does stuff.
}

不必像这样做:

fn main() {
    let tuple = (10, Vec::new());
    foo(tuple.0, tuple.1);
}
fn foo(a: i32, b: Vec<i32>) {
    //Does stuff.
}
4个回答

40

在夜间编译器上:

#![feature(fn_traits)]

fn main() {
    let tuple = (10, Vec::new());
    std::ops::Fn::call(&foo, tuple);
}
fn foo(a: i32, b: Vec<i32>) {
}

据我所知,目前没有稳定的方法可以做到这一点。


1
但是有的,看看我的回答。 - ljedrz
21
我会尽力为您翻译这句话:@ljedrz 我认为他不想/无法更改函数签名。 - mcarton
2
在那种情况下,是的,我也看不到其他的方式了。 - ljedrz

40

有一种方法,使用模式匹配的魔法:

fn main() {
    let tuple = (10, Vec::new());
    foo(tuple);
}

fn foo((a, b): (i32, Vec<i32>)) {
    // do stuff
}
根据Rust参考手册所述:

与let绑定一样,函数参数是不可反驳的模式,因此任何在let绑定中有效的模式也适用于参数。

因此,您可以指定以下参数:

(a, b): (i32, Vec<i32>)

就像您在let语句中所做的那样。


10
你能再详细解释一下吗?这不就像调用一个只有一个参数,但这个参数恰好是一个元组的函数吗? - piotao
3
是的,foo 是一个接受元组作为参数的单参数函数。恰巧在函数参数中,该元组会立即进行模式匹配和解包操作。 - Colonel Thirty Two
1
这个提案没有解决“拆包元组并将其作为参数传递”的需求(重点在于“参数”是复数形式)。 - Joël

16
let (a, b) = (10, Vec::new());
foo(a, b);

16
我赞成这个回答,但如果它明确回答问题而不仅仅提供替代代码,那么它会更好。 - trent

0
这可以在稳定的Rust中使用通用函数或特质来完成。下面是一个示例,它使用特质将2元组传递给接受两个参数的函数。
fn main() {
    let tuple = (0, "hello");
    takes2.call(tuple);
}

fn takes2(a: u8, b: &str) {}

trait Call2<A, B, Z> {
    fn call(self, args: (A, B)) -> Z;
}

impl<F, A, B, Z> Call2<A, B, Z> for F
where
    F: FnOnce(A, B) -> Z,
{
    fn call(self, (a, b): (A, B)) -> Z {
        self(a, b)
    }
}

这在大多数情况下可能有些过度,但如果你需要在很多地方做这种事情,使用这个通用代码可能是值得的。
相同的思路可以扩展到任意大小的元组。如果你真的想要疯狂一点,甚至可以使用宏来定义类似的特性,适用于任意数量的参数。
extern crate paste; // FYI

fn main() {
    let pair = (0, "hello");
    let triple = (0, "hello", 1024);
    let quad = (0, "hello", 1024, 3.14);
    takes2.call(pair);
    takes3.call(triple);
    takes4.call(quad);
}

fn takes2(a: u8, b: &str) {}

fn takes3(a: u8, b: &str, c: usize) {}

fn takes4(a: u8, b: &str, c: usize, d: f64) {}

define_tuple_calls!(A, B, C, D);

macro_rules! define_tuple_calls {
    () => {};
    ($A:ident $(, $T:ident)* $(,)?) => {
        paste::paste! {
            trait [<Call $A $($T)*>]<$A, $($T,)* Z> {
                fn call(self, args: ($A, $($T,)*)) -> Z;
            }
            
            impl<F, $A, $($T,)* Z> [<Call $A $($T)*>]<$A, $($T,)* Z> for F
            where
                F: FnOnce($A, $($T,)*) -> Z,
            {
                #[allow(non_snake_case)]
                fn call(self, ($A, $($T,)*): ($A, $($T,)*)) -> Z {
                    self($A, $($T,)*)
                }
            }
        }
        define_tuple_calls!($($T,)*);
    };
}
use define_tuple_calls;

如果你想传递一组元组和独立的参数,就像take4.call((0, "hello"), 1024, 3.14)这样,情况会变得相当复杂。我可以看到一种方法,你可以将输入转换为嵌套的元组((0, "hello"), 1024, 3.14),然后将其作为参数传递给方法,如take4.call(((0, "hello"), 1024, 3.14))。调用方法需要更通用。它不再接受(A, B, C, D),而是接受impl FlattensTo<(A, B, C, D)>,其中FlattensTo<T>是一个类似于Into<T>的特质,你需要定义并实现它,以支持任意组合的嵌套元组或值。要全面实现这一点而不产生大量冗余代码,你可能需要编写一个过程宏。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接