在结构定义上指定`Fn`特性,而不修复其中一个`Fn`参数

时间:2018-06-03 21:33:33

标签: generics rust traits

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

struct Foo<F> {
    func: F,
}

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

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

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

  1. for<>语法在这里没有帮助。除了它还没有用于非寿命参数的事实之外,它是通用的(&#34;对于所有&#34;)而不是存在的(&#34;存在&#34;)。这样就出来了。

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

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

    但这不起作用:除P绑定外,未使用参数where,因此编译器会抱怨。

    可以通过添加PhantomData<P>字段来解决此问题,但这不是必需的,更重要的是,用户不能再轻松使用结构构造函数语法了。

  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
    
  4. 有没有办法达到我的目的?

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

    首先,一旦&#34;隐含特征限制&#34;实现了RFC,这允许我省略所有impl块的重复特征边界。其次,使用此绑定,它可以帮助编译器进行类型推断。考虑一下:

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

    如果绑定是可能的(我使用上面的PhantomData&#34;解决方案&#34;),编译器可以更容易地推断出闭包的第一个参数的类型。如果只在impl块上指定了特征边界,则编译器会遇到困难。

2 个答案:

答案 0 :(得分:6)

不是在结构上加上约束,最简单和最好的方法是将约束放在需要使用该函数的所有方法的实现上:

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);
    }
}
  

首先,一旦实现了“隐含特征边界”RFC,这允许我省略所有impl块的重复特征边界。

所以听起来你主要担心的是删除重复边界。如果这是问题所在,您可以尝试将具有相同边界的所有方法分组到一个公共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等。

答案 1 :(得分:5)

解决方案#2是我所知道的唯一方法,可以在struct上使用边界。在我看来,使它成为没有结构的边界,如Peter Hall suggests,通常是可取的,因为它只将边界放在它们真正有意义的地方,但如果你发现这是繁重的,额外的type参数是您唯一的选择。

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

第二个参数是必要的。 Fn - 实现类型的参数类型是Fn特征的参数,因此原则上您可以同时拥有impl Fn(i32, i32) for X和{{1}就像你可以同时拥有impl Fn(i32, String) for Ximpl AsRef<i32> for X一样。

事实上,如果你不太难看,那就是HRTB的工作原理:一个函数可以为某些特定的生命周期实现impl AsRef<String> for X {{ 1}},或者它可以实现Fn(&'x i32),这意味着它实现了无数个可能的'x特征。

但您发现为for<'a> Fn(&'a i32)添加参数的问题:参数未使用。

  

通过添加Fn字段可以解决此问题,但这不是必需的

The compiler peers inside structs to determine the variance of their parameters.在这种情况下,假设P是引用类型。将PhantomData<P>传递给期望P的函数是否安全?反过来呢?

(正如链接的答案所述,约束 - Foo<_, &'static T>条款 - 不计入确定方差,这就是为什么Foo<_, &'a T>在这里是必要的。)

where成员不应该<{1}},因为PhantomData不包含PhantomData。它包含一个函数,它将PhantomData<P>作为参数。相反,您应该使用Foo<_, P>,它向编译器发出信号,PP的方差与PhantomData<fn(P)>的方差相同 - 函数(指针)取Foo<F, P>。换句话说,P中的fn(P)是逆变的。对于人类读者来说,这似乎是多余的 - 毕竟,我们已经拥有P成员,而Foo必须在P中具有逆变性。但是,编译器并不是很聪明,无法得出结论,所以你必须拼出来。

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

这让我想到你的最后反对意见:

  

更重要的是,用户不能再轻松使用结构构造函数语法了。

不幸的是,除了&#34;写一个很好的构造函数&#34;我无法想到解决这个问题的方法。也许更聪明的编译器有一天会解除这个负担,但就目前来说,F就是我们所拥有的。