在不固定“Fn”参数之一的情况下在结构定义上指定“Fn”特征绑定

Specify `Fn` trait bound on struct definition without fixing one of the `Fn` parameters

我有一个包含函数对象的结构:

struct Foo<F> {
    func: F,
}

我想添加一个 Fn 特征绑定到结构定义。问题是:我确实关心第一个参数(它必须是 i32),但不关心第二个参数。我真正想写的是这样的:

struct Foo<F> 
where
    ∃ P so that F: Fn(i32, P),
{
    func: F,
}

所以在英语中:类型 F 必须是一个有两个参数的函数,第一个是 i32 (第二个可以是任何东西)。上面的语法显然是无效的。我考虑了三种可能的解决方案:

  1. for<> 语法在这里无济于事。除了它不适用于非生命周期参数之外,它是通用的("for all")而不是存在的("there exists")。这样就结束了。

  2. 另一种可能性是向结构添加类型参数。我已经不喜欢那个解决方案了,因为参数本身并不属于结构。

    struct Foo<F, P> 
    where
        F: Fn(i32, P),
    {
        func: F,
    }
    

    但这不起作用:参数 P 未使用,除了在 where 绑定中,因此编译器会报错。

    这个问题可以通过添加一个PhantomData<P>字段来解决,但这应该不是必需的,更重要的是,用户不能再轻易使用struct constructor语法。

  3. 最后我试了这个:

    struct Foo<F> 
    where
        F: Fn(i32, _),
    {
        func: F,
    }
    

    但这也行不通:

    error[E0121]: the type placeholder `_` is not allowed within types on item signatures
     --> src/main.rs:3:20
      |
    3 |         F: Fn(i32, _),
      |                    ^ not allowed in type signatures
    

有没有办法实现我想要的?


旁注:为什么我要将特征绑定到结构上,而不是仅在重要的 impl 块上绑定?

首先,一旦 "implied trait bounds" RFC 被实施,这允许我从所有 impl 块中省略重复的特征边界。其次,有了这个绑定,它可以帮助编译器进行类型推断。考虑一下:

struct Foo<F, T> 
where
    F: Fn(T, _),
{
    data: T,
    F: F,
}

如果绑定是可能的(我用上面的 PhantomData "solution" 试过),编译器可以更容易地推断出闭包第一个参数的类型。如果仅在 impl 块上指定特征边界,编译器就会遇到困难。

解决方案 #2 是我所知道的唯一可以在结构上使用边界的方法。在我看来,让它在结构上 没有 边界的情况下工作,因为 通常是更可取的,因为它只把边界放在真正有意义的地方,但是如果你发现那很麻烦,额外的类型参数是您唯一的选择。

  1. The other possibility is to add a type parameter to the struct. I already don't like that solution, because the parameter doesn't inherently belong to the struct.

第二个参数是必需的。 Fn 实现类型的参数类型是 Fn 特征 的 参数,所以原则上你可以同时拥有 impl Fn(i32, i32) for Ximpl Fn(i32, String) for X,正如您可以同时拥有 impl AsRef<i32> for Ximpl AsRef<String> for X.

事实上,如果您不仔细研究,这就是 HRTB 已经工作的方式:一个函数可以为某些 特定的 实现 Fn(&'x i32)生命周期 'x,或者它可以实现 for<'a> Fn(&'a i32),这意味着它可以实现无限数量的 Fn 特征。

但是你发现为P添加参数的问题:该参数未被使用

This problem can be solved by adding a PhantomData<P> field, but this shouldn't be necessary

The compiler peers inside structs to determine the variance of their parameters. 在这种情况下,假设 P 是引用类型。将 Foo<_, &'static T> 传递给需要 Foo<_, &'a T> 的函数是否安全?反过来呢?

(如链接答案所述,约束 - where 子句 - 不计入确定方差,这就是此处需要 PhantomData 的原因。)

但是 PhantomData 成员 不应该 PhantomData<P>,因为 Foo<_, P> 不包含 P。它包含一个 函数,该函数将 P 作为参数 。相反,您应该使用 PhantomData<fn(P)>,它向编译器发出信号,表明 PFoo<F, P> 的方差与 fn(P) 的方差相同——一个函数(指针)服用 P。换句话说,FooP 中是逆变的。对于人类 reader,这似乎是多余的——毕竟,我们已经有一个 F 成员,并且 F 必须在 P 中是逆变的。但是,好吧,编译器还不够聪明,无法得出这个结论,所以你必须把它拼出来。

(有关方差的更严格解释,请参阅 the section of the Nomicon on subtyping。)

这让我得出你最后的反对意见:

and more importantly, users cannot use the struct constructor syntax easily anymore.

不幸的是,除了"write a nice constructor function",我想不出解决这个问题的方法。也许更聪明的编译器有一天会减轻这一负担,但就目前而言,PhantomData 就是我们所拥有的。

与其对结构施加约束,最简单和最好的方法是对需要使用该函数的所有方法的实现施加约束:

struct Foo<F, T> {
    data: T,
    f: F,
}

impl<F, T> Foo<F, T> {
    fn call_f<P>(&self, arg: P)
    where
        T: Copy,
        F: Fn(T, P)
    {
        (self.f)(self.data, arg);
    }
}

First, once the "implied trait bounds" RFC is implemented, this allows me to omit the duplicate trait bounds from all the impl blocks.

听起来您主要关心的是删除重复边界。如果这是问题所在,您可以尝试将具有相同边界的所有方法分组到一个公共 impl 中,这样您仍然只写一次:

impl<F, T, P> Foo<F, T> 
where
    T: Copy,
    F: Fn(T, P),
{
    fn call_f(&self, arg: P) {
        (self.f)(self.data, arg);
    }
}

这里有一个小问题,类似于您自己发现的问题:unconstrained type parameter: P。然而,既然我们已经到了这里,您可以通过引入一个特征来非常简单地解决它(您可以根据您的特定用例为其命名):

trait FIsAFunction<F, T, P> {
    fn call_f(&self, arg: P);
}

impl<F, T, P> FIsAFunction<F, T, P> for Foo<F, T> 
where
    T: Copy,
    F: Fn(T, P),
{
    fn call_f(&self, arg: P){
        (self.f)(self.data, arg);
    }
}

而且用户不必做任何奇怪的事情[1]:

fn main() {
    fn callback(x: u32, y: &str) {
        println!("I was given {:?} and {:?}", x, y)
    }
    let foo = Foo { data: 1u32, f: callback };
    foo.call_f("hello!");
}

[1] 他们可能需要 use 这个特质。 所以 不奇怪:你已经必须用很多 std 东西来做到这一点,比如 std::io::Read