如何使用不同的泛型参数约束泛型约束?

How can i constrain a generic constraint using a different generic argument?

提问人:SkyPPeX 提问时间:11/1/2022 最后编辑:Chayim FriedmanSkyPPeX 更新时间:11/1/2022 访问量:136

问:

所以我有一个结构。pub struct Foo<TFn, TArg, TReturn> where TFn: Fn(TArg) -> TReturn { func: TFn }

这让我习惯了 C# 泛型,但为什么它在 rust 中不起作用? 我希望字段“func”的类型为 Fn,其中参数的类型为“TArg”,返回值的类型为“TReturn”。

编译器抱怨参数“TArg”和“TReturn”从未使用过,但它们有助于定义 TFn 值的签名。

我尝试删除“从未使用过”的参数,只是在约束中显式写入类型。这很好用。

Rust 编译器错误 语法错误

评论

0赞 Chayim Friedman 11/1/2022
相关:特征边界是否应该在 struct 和 impl 中重复?

答:

2赞 Aleksander Krauze 11/1/2022 #1

在 rust 中,你的结构必须使用它所泛型的所有泛型类型。并使用 mean,它们必须出现在至少一种类型的字段中。您可以使用特殊类型 PhantomData 解决您的问题。它是一种标记类型,用于向编译器提供附加信息,并在编译时删除。这些文档甚至为您提供了如何使用它来解决“未使用的类型参数”错误的示例。TLDR 在这里:

use std::marker::PhantomData;

pub struct Foo<TFn, TArg, TReturn>
where
    TFn: Fn(TArg) -> TReturn
{
    func: TFn,
    _marker: PhantomData<(TArg, TRetur)> // This line tells the compiler that
                                         // this struct should act like it owned
                                         // type (Targ, TReturn)
}

当你想创建结构的实例时,只需把作为一个值:PhantomData

let s = Foo { func: f, _marker: PhantomData };

评论

1赞 Chayim Friedman 11/1/2022
最好从 struct 声明中删除约束。
0赞 isaactfa 11/1/2022
虽然这确实会让编译器闭嘴,但它不拥有 a 或 a,也不应该像它一样行事。我在下面发布了一个考虑差异的答案。FooTArgTReturn
2赞 isaactfa 11/1/2022 #2

@Aleksander克劳兹的答案是正确的,但没有考虑到一个利基问题:方差。考虑一下:

fn x<'s>(&'s str) -> String { // ...

x是一个可以处理任何存在的函数。特别是,这意味着可以处理任何寿命于 ,例如。这是真的,因为是 的子类型。在 Rust 中,作为 (written) 的子类型意味着只要我能接受 a ,我也可以接受 a ,因为子类型可以做它的超类型可以做的任何事情,甚至更多(例如,活得更久)。&str'sx&str's&'static str&'static str&'s strTUT: UUT

子类型最常出现在 Rust 中的生命周期中,如果寿命超过(即长于),则生命周期是生命周期的子类型。所以如果我能接受一个,我也可以接受一个只要。换言之,是 if 的子类型是 的子类型。这称为协方差,是所谓的方差的一个实例。'a'b'a'b&'b T&'a T'a: 'b&'a T&'b T'a'b

但是,函数在这方面很有趣。如果我有类型,并且是 if 的子类型,而不是相反。也就是说,如果我需要一个可以处理长生存期的函数,那么一个可以处理较短生存期的函数也可以,因为我可以传递的任何参数都将是它期望的参数的子类型。这称为逆变,是我们非常希望函数具有的属性。fn(&'a T)fn(&'b T)fn(&'a T)fn(&'b T)'b'a

你的类型或多或少是一个函数,所以我们希望它表现得像一个函数,并在它的参数上是逆变的。但事实并非如此。它是协变的。这是因为 Rust 中的结构体从其字段继承了它的方差。类型在 和 上是协变的(因为类型是),因此将在 上协变。为了让它像函数一样运行,我们可以用适当的类型标记它:.这将是逆变和协变(函数在其返回类型上是协变的;我希望从上面的解释中得出结论)。FooPhantomData<(TArg, TReturn)>TArgTReturn(TArg, TReturn)FooTArgPhantomData<fn(TArg) -> TReturn>TArgTReturn

我写了一个小例子(尽管是人为的例子)来演示不正确的方差如何破坏应该工作的代码:

use std::marker::PhantomData;

pub struct Foo<TFn, TArg, TReturn>
{
    func: TFn,
    // this makes `Foo` covariant over `TArg`
    _marker: PhantomData<(TArg, TReturn)>
}

impl<TFn, TArg, TReturn> Bar<TFn, TArg, TReturn>
where
    TFn: Fn(TArg) -> TReturn,
{
    // presumably this is how one might use a `Foo`
    fn call(&self, arg: TArg) -> TReturn {
        (self.func)(arg)
    }
}

// `foo_factory` will return a `Foo` that is covariant over the lifetime `'a`
// of its argument
fn foo_factory<'a>(_: &'a str) -> Foo<fn(&'a str) -> String, &'a str, String> {
    // We only care about the type signatures here
    panic!()
}

fn main() {
    let long_lifetime: &'static str = "hello";

    // we make a `Foo` that is covariant over the `'static` lifetime
    let mut foo = foo_factory(long_lifetime);

    foo.call("world");

    {
        let short_lifetime = String::from("world");
        // and because it's covariant, it can't accept a shorter lifetime
        // than `'static`
        // even though this should be perfectly fine, it won't compile
        foo = foo_factory(&short_lifetime);
    }

    foo.call("world");
}

但是,如果我们修复方差:

pub struct Foo<TFn, TArg, TReturn> {
    func: TFn,
    // `Foo` is now _contravariant_ over `TArg` and covariant over `TReturn`
    _marker: PhantomData<fn(TArg) -> TReturn>,
}

上面的函数现在将像人们预期的那样编译得很好。main

有关 Rust 中的方差以及它与数据结构和丢弃检查的关系的更多信息,我建议查看关于它的 'nomicon 章节和 PhantomData 章节。

评论

0赞 SkyPPeX 11/2/2022
这确实是我想要的。在我看来,这几乎像是一种疏忽,我试图做的事情不起作用,但从这个解释中,我理解了允许会导致问题,因为编译器将不得不默认为协方差或逆方差。我想它也可以在某个地方用一个关键字来解决,只是为了便于使用。无论如何,感谢您的详细解释。最后,结构体最终做了比这里显示的更多的事情,它使用了结构体字段中的所有泛型,这实际上使我的代码最终更加灵活。