闭包参数上未使用的类型参数

12

这样可以工作:

struct Foo<T, F>
where
    F: Fn() -> Option<T>,
{
    f: F,
}

但是这会导致编译错误:

struct Bar<I, T, F>
where
    F: Fn(I) -> Option<T>,
{
    f: F,
}

error[E0392]: parameter `I` is never used
 --> src/lib.rs:1:12
  |
1 | struct Bar<I, T, F>
  |            ^ unused parameter
  |
  = help: consider removing `I`, referring to it in a field, or using a marker such as `std::marker::PhantomData`

error[E0392]: parameter `T` is never used
 --> src/lib.rs:1:15
  |
1 | struct Bar<I, T, F>
  |               ^ unused parameter
  |
  = help: consider removing `T`, referring to it in a field, or using a marker such as `std::marker::PhantomData`

为什么在闭包的返回类型中使用类型参数可以,但在其参数中不行?

我可以通过将闭包存储为特质对象来解决此问题:

struct Bar<I, T> {
    f: Box<Fn(I) -> Option<T>>,
}

但如果可能的话,我想避免这种情况。


2
长话短说,这基本上是关于方差的RFC 738 - Paolo Falabella
为什么在闭包的返回类型中使用类型参数是可以的,但在其参数中却不行?我怀疑这与闭包返回类型是在相应特质上用关联类型建模有关,而参数类型是该特质的类型参数有关,但我不确定。 - Vladimir Matveev
@VladimirMatveev 是的,这在我链接的rfc末尾提到了,在相关类型规则中。 - Paolo Falabella
2个回答

7
@VladimirMatveev所说,闭包的返回类型是关联类型。
关联类型与类型参数不同,因为它的值是在你实现一个trait时确定的,而不是在调用中使用它时确定的。
Fn(I) -> Option<T>中,一旦你有了输入(类型为I)和实现(在传递的闭包中定义的特定操作),Option<T>输出就确定了。
然而,对于I来说是不同的。你需要在结构体中使用该类型,或者使用PhantomData字段向编译器展示“理论上如何使用它”。
use std::marker::PhantomData;

struct Bar<I, T, F>
where
    F: Fn(I) -> Option<T>,
{
    f: F,
    _marker: PhantomData<I>,
}
PhantomData 的作用仅在于检查类型,但是在生成的代码中会被擦除,因此不会占据结构体中的任何内存空间(这也是它被称为“幻影”的原因)。
需要使用它的原因在 RFC 738 on variance 中有详细解释。我将尝试在此处给出一个更短(并且希望是正确的)版本。
在 Rust 中,大多数情况下(但并非总是!),您可以使用较长的生命周期来替代较短的生命周期。
fn foo<'short, 'long>(_a: &'short i32, b: &'long i32)
where
    'long: 'short,
{
    let _shortened: &'short i32 = b; // we're binding b to a shorter lifetime
}

fn foo2<'short, 'long>(_a: &'short i32, b: &'long Cell<&'long i32>)
where
    'long: 'short,
{
    let _shortened: &Cell<&'short i32> = b;
}

(游乐场)

RFC解释了为什么Cell期望完全相同的(而不是更长的)生命周期,但现在我建议您只信任编译器,因为允许foo2编译将不安全。

现在假设你有一个

struct Foo<T> { t: T }

T可以是任何类型,包括持有引用的类型。
特别地,T可以是像&i32&Cell<&i32>这样的类型。
与我们上面的foo函数一样,Rust可以通过检查T的类型来推断是否允许我们将较短的生命周期分配给它 (playground)。

然而,当你有一个未使用的类型参数时,推断就没有任何字段可以检查以了解它应该如何允许类型在生命周期方面的行为。

如果你有:

struct Foo<T>; // unused type parameter!

Rust要求您使用PhantomType指定,如果您希望您的T表现得像& i32Cell一样。您可以编写:

struct Foo<T> {
    marker: PhantomData<T>, // this is what you usually want
                            // unless you're working with unsafe code and
                            // raw pointers
}

或者您可以编写以下内容:
struct Foo<T> {
    marker: PhantomData<Cell<T>>
}

在第一个例子中,我认为你应该使用 PhantomData<fn(I)>。我会这样做以确保它是正确的。 - bluss
@bluss 好的,这让我有点头疼,但我认为你已经有了一个具体的 f: F,所以你不需要一个 PhantomData 来告诉 I 如何像闭包的参数一样行事。你想要约束的是 I 本身。我不确定在这个阶段你是否应该对你想要与 Bar 使用的闭包做出决定。(例如,如果我打算使用像 Fn(Cell<&i32>) 这样的闭包,我是否需要 PhantomType<Cell<&i32>>,或者在这种情况下没有区别?) - Paolo Falabella
我的头也疼,但我认为fn(I)和普通的II方面具有相反的方差。我的意思是,即使它们是相同的,我知道fn(I)是正确的,因为这就是类型的行为方式--像需要I的函数一样。 :-) - bluss
我认为基本上可以这样分解。根据类型变量 X,我们有协变:Xfn() -> X,逆变:fn(X),不变:Cell<X> - bluss

3

使用 trait 对象进行动态分发的替代方法是使用 std::marker::PhantomData

use std::marker::PhantomData;

struct Bar<I, T, F>
where
    F: Fn(I) -> Option<T>,
{
    f: F,
    _i: PhantomData<I>,
    _t: PhantomData<T>,
}

(游乐场)

使用 PhantomData 就可以 "实例化" 一个 PhantomData,例如:

let phantom: PhantomData<T> = PhantomData;

能否详细解释一下在这种情况下为什么需要使用PhantomData?文档中说:“一个类型好像存储了类型T的值,即使它没有”,但是结构体Bar似乎并不会存储任何IF类型的值。 - Shepmaster
我真的不知道。我想这可能是类型系统的限制。此外,从“所有权”部分来看,最好将其设置为PhantomData<*const T>,但我不确定。 - Jorge Israel Peña
你只需要翻译文本内容,不需要解释它。为什么只需要“_i”而不是“_t”? - Paolo Falabella
在这种情况下,PhantomData将使结构体中使用的IT有效地消除未使用的类型参数错误,而不会增加结构体大小的任何开销。 - Denilson Amorim

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