Rust 中 Box<dyn FnOnce(T)> 的协方差

fak*_*ake 6 contravariance lifetime variance rust

我有一个函数需要一个短暂的对象。我希望我总是能够将一个长寿的对象传递给它。但当我尝试对其进行编码时,出现了一个奇怪的错误:

type F<'arg> = Box<dyn FnOnce(&'arg ())>;
fn contravar<'small, 'large: 'small>(f: F<'small>) -> F<'large> {
    f
}
Run Code Online (Sandbox Code Playgroud)

操场

特别:

error: lifetime may not live long enough
 --> src/lib.rs:3:5
  |
2 | fn contravar<'small, 'large: 'small>(f: F<'small>) -> F<'large> {
  |              ------  ------ lifetime `'large` defined here
  |              |
  |              lifetime `'small` defined here
3 |     f
  |     ^ function was supposed to return data with lifetime `'large` but it is returning data with lifetime `'small`
  |
  = help: consider adding the following bound: `'small: 'large`
Run Code Online (Sandbox Code Playgroud)

它的论证似乎F是不变的,但我猜它是逆变的。我错过了什么吗?有没有办法让F<'arg> 真正逆变'arg

编辑:看起来“问题”是 Rust 希望以相同的方式对待所有通用特征(包括 Fn/FnMut/FnOnce)。我的观点是,这三个是并且应该被特殊对待,特别是考虑到它们是引用闭包的唯一方式。因此我打开了一个问题

Fra*_*gné 5

Rust Reference 的关于子类型和方差的页面记录了从 Rust 1.63.0 开始,fn(T) -> ()在 上是逆变的T,在 上dyn Trait<T> + 'a不变的T

FnOnceFnMutFn是特征,因此dyn FnOnce(&'a ())不幸的是,这意味着在 上是不变的&'a ()

// Compiles
pub fn contravariant<'a, 'b: 'a>(x: fn(&'a ())) -> fn(&'b ()) { x }

// Doesn't compile
pub fn contravariant2<'a, 'b: 'a>(x: Box<dyn FnOnce(&'a ())>) -> Box<dyn FnOnce(&'b ())> { x }
Run Code Online (Sandbox Code Playgroud)

有没有办法以FnOnce某种方式包装以使编译器相信正确的方差?

这是我可以使用代码得出的结果unsafe。请注意,我不保证这是否合理。我不知道有什么方法可以在没有unsafe代码的情况下做到这一点。

use std::marker::PhantomData;

trait Erased {}

impl<T> Erased for T {}

pub struct VariantBoxedFnOnce<Arg, Output> {
    boxed_real_fn: Box<dyn Erased + 'static>,
    _phantom_fn: PhantomData<fn(Arg) -> Output>,
}

impl<Arg, Output> VariantBoxedFnOnce<Arg, Output> {
    pub fn new(real_fn: Box<dyn FnOnce(Arg) -> Output>) -> Self {
        let boxed_real_fn: Box<dyn Erased + '_> = Box::new(real_fn);
        let boxed_real_fn: Box<dyn Erased + 'static> = unsafe {
            // Step through *const T because *mut T is invariant over T
            Box::from_raw(Box::into_raw(boxed_real_fn) as *const (dyn Erased + '_) as *mut (dyn Erased + 'static))
        };
        Self {
            boxed_real_fn,
            _phantom_fn: PhantomData,
        }
    }

    pub fn call_once(self, arg: Arg) -> Output {
        let boxed_real_fn: Box<Box<dyn FnOnce(Arg) -> Output>> = unsafe {
            // Based on Box<dyn Any>::downcast()
            Box::from_raw(Box::into_raw(self.boxed_real_fn) as *mut Box<dyn FnOnce(Arg) -> Output>)
        };
        boxed_real_fn(arg)
    }
}

pub fn contravariant<'a, 'b: 'a>(x: VariantBoxedFnOnce<&'a (), ()>) -> VariantBoxedFnOnce<&'b (), ()> { x }

#[cfg(test)]
mod tests {
    use super::*;

    fn foo(_x: &()) {}

    #[test]
    pub fn check_fn_does_not_require_static() {
        let f = VariantBoxedFnOnce::new(Box::new(foo));
        let x = ();
        f.call_once(&x);
    }

    #[test]
    pub fn check_fn_arg_is_contravariant() {
        let f = VariantBoxedFnOnce::new(Box::new(foo));
        let g = contravariant(f);
        let x = ();
        g.call_once(&x);
    }
}
Run Code Online (Sandbox Code Playgroud)

此处,VariantBoxedFnOnce仅限于采用一个参数的函数。

诀窍是存储消失的Box<dyn FnOnce(Arg) -> Output>的类型擦除版本Arg,因为我们不希望 的方差VariantBoxedFnOnce<Arg, Output>取决于Box<dyn FnOnce(Arg) -> Output>(它在 上是不变的Arg)。然而,还有一个PhantomData<fn(Arg) -> Output>字段可以提供适当的逆变Arg(和协方差Output)。

我们不能使用Any作为我们擦除的类型,因为只有'static类型实现Any,并且我们有一个步骤VariantBoxedFnOnce::new(),其中我们有一个Box<dyn Erased + '_>where'_不保证是'static。然后我们立即将其“转换”为'static,以避免在 上出现冗余的生命周期参数VariantBoxedFnOnce,但这'static是一个谎言(因此是unsafe代码)。call_once将擦除的类型“向下转换”为“原始” Box<dyn FnOnce(Arg) -> Output>,但ArgOutput可能由于差异而与原始类型不同。