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

Luk*_*odt 9 generics traits rust

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

struct Foo<F> {
    func: F,
}
Run Code Online (Sandbox Code Playgroud)

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

struct Foo<F> 
where
    ? P so that F: Fn(i32, P),
{
    func: F,
}
Run Code Online (Sandbox Code Playgroud)

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

  1. for<>语法不会帮助这里.除了它对非寿命参数不起作用的事实之外,它是通用的("为所有人")而不是存在的("存在").所以那就是了.

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

    struct Foo<F, P> 
    where
        F: Fn(i32, P),
    {
        func: F,
    }
    
    Run Code Online (Sandbox Code Playgroud)

    但是这不起作用:P除了where绑定之外没有使用参数,所以编译器会抱怨.

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

  3. 最后我试过这个:

    struct Foo<F> 
    where
        F: Fn(i32, _),
    {
        func: F,
    }
    
    Run Code Online (Sandbox Code Playgroud)

    但这也行不通:

    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
    
    Run Code Online (Sandbox Code Playgroud)

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


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

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

struct Foo<F, T> 
where
    F: Fn(T, _),
{
    data: T,
    F: F,
}
Run Code Online (Sandbox Code Playgroud)

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

Pet*_*all 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

首先,一旦实现了"隐含特征边界"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);
    }
}
Run Code Online (Sandbox Code Playgroud)

这里有一点问题,类似于你自己发现的问题: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);
    }
}
Run Code Online (Sandbox Code Playgroud)

用户不必做任何奇怪的事情[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!");
}
Run Code Online (Sandbox Code Playgroud)

[1]他们可能具有use这种特质.这不是那么奇怪:你必须要做很多std东西,比如std::io::Read等等.


tre*_*tcl 5

解决方案#2是我所知道的唯一方法.

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

第二个参数是必要的.该类型的的参数Fn-implementing类型是该参数的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:参数未使用.

可以通过添加PhantomData<P>字段来解决此问题,但这不是必需的

编译器在结构内部对等以确定其参数的方差.在这种情况下,假设P是引用类型.将a传递Foo<_, &'static T>给期望的函数是否安全Foo<_, &'a T>?反过来呢?

(正如链接的答案所述,约束 - where条款 - 不计入确定方差,这就是为什么PhantomData在这里是必要的.)

PhantomData成员应该PhantomData<P>,因为Foo<_, P>不包含P.它包含一个函数,接受P作为参数.相反,你应该使用PhantomData<fn(P)>,它向编译器发出信号,即Foo<F, P>in P的方差与fn(P)- 函数(指针)的方差相同P.换句话说,Foo是逆变的P.对于人类读者来说,这似乎是多余的 - 毕竟,我们已经有了一个F成员,并且F必须是逆变的P.但是,编译器并不是很聪明,无法得出这个结论,所以你必须拼写出来.

(有关更改的更严格解释,请参阅Nomicon关于子类型的部分.)

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

更重要的是,用户不能再轻松使用struct constructor语法了.

不幸的是,除了"编写一个很好的构造函数"之外,我想不出这个解决方案.也许更聪明的编译器有一天会解除这个负担,但就目前而言,PhantomData我们拥有的是什么.