21

考虑以下代码:

fn foo<'a, T: 'a>(t: T) -> Box<Fn() -> &'a T + 'a> {
    Box::new(move || &t)
}

我的期望:

  • 类型 T 有生命周期'a
  • 价值t只要T.
  • t移动到闭包,所以闭包只要t
  • 闭包返回一个t被移动到闭包的引用。因此,只要闭包存在,引用就有效。
  • 没有生命周期问题,代码编译。

实际发生的情况:

  • 代码无法编译:
error[E0495]: cannot infer an appropriate lifetime for borrow expression due to conflicting requirements
 --> src/lib.rs:2:22
  |
2 |     Box::new(move || &t)
  |                      ^^
  |
note: first, the lifetime cannot outlive the lifetime  as defined on the body at 2:14...
 --> src/lib.rs:2:14
  |
2 |     Box::new(move || &t)
  |              ^^^^^^^^^^
note: ...so that closure can access `t`
 --> src/lib.rs:2:22
  |
2 |     Box::new(move || &t)
  |                      ^^
note: but, the lifetime must be valid for the lifetime 'a as defined on the function body at 1:8...
 --> src/lib.rs:1:8
  |
1 | fn foo<'a, T: 'a>(t: T) -> Box<Fn() -> &'a T + 'a> {
  |        ^^
  = note: ...so that the expression is assignable:
          expected std::boxed::Box<(dyn std::ops::Fn() -> &'a T + 'a)>
             found std::boxed::Box<dyn std::ops::Fn() -> &T>

我不明白冲突。我该如何解决?

4

4 回答 4

20

非常有趣的问题!我我理解了这里的问题。让我试着解释一下。

tl;dr:闭包不能返回对移动捕获的值的引用,因为那将是对self. 这样的引用不能被返回,因为Fn*特征不允许我们表达它。这与流迭代器问题基本相同,可以通过 GAT(通用关联类型)修复。


手动实现它

您可能知道,当您编写闭包时,编译器将为impl适当的Fn特征生成一个结构和块,因此闭包基本上是语法糖。让我们尽量避免所有这些糖并手动构建您的类型。

您想要的是拥有另一种类型并且可以返回对该拥有类型的引用的类型。并且您希望拥有一个返回所述类型的盒装实例的函数。

struct Baz<T>(T);

impl<T> Baz<T> {
    fn call(&self) -> &T {
        &self.0
    }
}

fn make_baz<T>(t: T) -> Box<Baz<T>> {
    Box::new(Baz(t))
}

这与您的盒装封盖相当。让我们尝试使用它:

let outside = {
    let s = "hi".to_string();
    let baz = make_baz(s);
    println!("{}", baz.call()); // works

    baz
};

println!("{}", outside.call()); // works too

这工作得很好。字符串s被移动到Baz类型中,并且该Baz实例被移动到Box. s现在归 拥有baz,然后归 拥有outside

当我们添加一个字符时会变得更有趣:

let outside = {
    let s = "hi".to_string();
    let baz = make_baz(&s);  // <-- NOW BORROWED!
    println!("{}", baz.call()); // works

    baz
};

println!("{}", outside.call()); // doesn't work!

现在我们不能使 的生命周期baz大于 的生命周期s,因为baz包含一个引用,s该引用将是 的悬空引用s会比 更早地超出范围baz

我想用这段代码说明一点:我们不需要在类型上注释任何生命周期Baz来确保它的安全;Rust 自己解决了这个问题,并强制它的baz寿命不超过s. 这在下面很重要。

为它写一个特征

到目前为止,我们只介绍了基础知识。让我们试着写一个Fn更接近你原来问题的特征:

trait MyFn {
    type Output;
    fn call(&self) -> Self::Output;
}

在我们的 trait 中,没有函数参数,但除此之外它与真正的Fntrait完全相同。

让我们实现它!

impl<T> MyFn for Baz<T> {
    type Output = ???;
    fn call(&self) -> Self::Output {
        &self.0
    }
}

现在我们有一个问题:我们写什么而不是????一个人会天真地写&T……但我们需要一个生命周期参数来引用。我们在哪里得到一个?返回值的生命周期是多少?

让我们检查一下我们之前实现的功能:

impl<T> Baz<T> {
    fn call(&self) -> &T {
        &self.0
    }
}

所以这里我们也使用&T没有生命周期参数。但这仅适用于终身省略。基本上,编译器填充空白,因此fn call(&self) -> &T相当于:

fn call<'s>(&'s self) -> &'s T

啊哈,所以返回的引用的生命周期是绑定到self生命周期的!(更有经验的 Rust 用户可能已经感觉到这是怎么回事......)。

(作为旁注:为什么返回的引用不依赖于其T自身的生命周期?如果T引用非的东西,'static那么这必须被考虑,对吧?是的,但它已经被考虑了!记住,没有实例Baz<T>can ever寿命比事物T可能引用的时间长。所以self寿命已经比任何寿命T都短。因此我们只需要专注于self寿命)

但是我们如何在 trait impl 中表达这一点?事实证明:我们(还)不能。这个问题经常在流迭代器的上下文中被提及——也就是说,迭代器返回一个生命周期绑定到self生命周期的项目。在今天的 Rust 中,很难实现这一点;类型系统不够强大。

未来呢?

幸运的是,前段时间合并了一个RFC“通用关联类型” 。该 RFC 扩展了 Rust 类型系统以允许关联的特征类型是通用的(在其他类型和生命周期上)。

让我们看看我们如何使您的示例(有点)与 GAT 一起工作(根据 RFC;这些东西还不能工作☹)。首先,我们必须更改特征定义:

trait MyFn {
    type Output<'a>;   // <-- we added <'a> to make it generic
    fn call(&self) -> Self::Output;
}

代码中的函数签名没有改变,但请注意生命周期省略!以上fn call(&self) -> Self::Output等价于:

fn call<'s>(&'s self) -> Self::Output<'s>

所以关联类型的生命周期是绑定到self生命周期的。正如我们所愿!impl看起来像这样:

impl<T> MyFn for Baz<T> {
    type Output<'a> = &'a T;
    fn call(&self) -> Self::Output {
        &self.0
    }
}

要返回一个装箱MyFn,我们需要这样写(根据RFC 的这一部分

fn make_baz<T>(t: T) -> Box<for<'a> MyFn<Output<'a> = &'a T>> {
    Box::new(Baz(t))
}

如果我们想使用真正的 Fn特征怎么办?据我了解,即使使用 GAT,我们也不能。Fn我认为改变现有特征以向后兼容的方式使用 GAT是不可能的。所以标准库很可能会保持不那么强大的特性。(旁注:如何以向后不兼容的方式发展标准库以使用新的语言特性是我已经想知道几次了;到目前为止,我还没有听说这方面的任何真正计划;我希望 Rust 团队来做点什么……)


概括

您想要的在技术上并非不可能或不安全(我们将它实现为一个简单的结构并且它可以工作)。Fn然而,不幸的是,现在不可能在 Rust 的类型系统中以闭包/特征的形式表达你想要的东西。这与流迭代器正在处理的问题相同。

使用计划中的 GAT 功能,可以在类型系统中表达所有这些。但是,标准库需要以某种方式赶上才能使您的确切代码成为可能。

于 2018-04-13T09:05:46.793 回答
9

我的期望:

  • 该类型T具有生命周期'a
  • 价值t只要T.

这是没有意义的。一个值不能像一个类型“活得一样长”,因为一个类型是不活的。“T有一生'a”是一个很不精确的说法,容易引起误解。真正的意思是“T: 'a的实例T必须至少与生命周期一样保持有效'a。例如,T 不能是生命周期短于的引用'a,或者包含此类引用的结构。请注意,这与形成引用无关 T,即&T

那么value 的t存在时间与它的词法范围(它是一个函数参数)所说的一样长,这与它完全无关'a

  • t移动到闭包,所以闭包只要t

这也是不正确的。只要闭包在词法上存在,闭包就会存在。它在结果表达式中是临时的,因此一直存在到结果表达式的末尾。t的生命周期根本不关心闭包,因为它T内部有自己的变量,即t. 由于捕获是 的副本/移动t,因此它不会以任何方式受到t的生命周期的影响。

然后将临时闭包移动到盒子的存储中,但这是一个具有自己生命周期的新对象。该闭包的生命周期与盒子的生命周期绑定,它是函数的返回值,然后(如果将盒子存储在函数之外)将盒子存储在其中的任何变量的生命周期。

所有这一切都意味着返回对其自身捕获状态的引用的闭包必须将该引用的生命周期绑定到它自己的引用。不幸的是,这是不可能的

原因如下:

Fn特质暗示特质,FnMut特质又暗示FnOnce特质。也就是说, Rust 中的每个函数对象都可以使用按值self参数调用。这意味着每个函数对象必须仍然有效,并使用按值self参数调用并一如既往地返回相同的内容。

换句话说,尝试编写一个返回对其自身捕获的引用的闭包大致扩展为以下代码:

struct Closure<T> {
    captured: T,
}
impl<T> FnOnce<()> for Closure<T> {
    type Output = &'??? T; // what do I put as lifetime here?
    fn call_once(self, _: ()) -> Self::Output {
        &self.captured // returning reference to local variable
                       // no matter what, the reference would be invalid once we return
    }
}

这就是为什么你试图做的事情从根本上是不可能的。退后一步,想想你实际上想用这个闭包来完成什么,然后找到其他方法来完成它。

于 2018-04-13T08:45:29.670 回答
1

其他答案是一流的,但我想说明您的原始代码无法工作的另一个原因。一个大问题在于签名:

fn foo<'a, T: 'a>(t: T) -> Box<Fn() -> &'a T + 'a>

这表示调用者可以在调用时指定任何生命周期,foo并且代码将是有效且内存安全的。这段代码不可能是这样的。'a用set to 调用它是没有意义的'static,但是这个签名没有任何东西可以阻止它。

于 2018-04-13T18:32:31.953 回答
1

您希望该类型T具有生命周期'a,但t不是对类型值的引用T。该函数t通过参数传递获取变量的所有权:

// t is moved here, t lifetime is the scope of the function
fn foo<'a, T: 'a>(t: T)

你应该做:

fn foo<'a, T: 'a>(t: &'a T) -> Box<Fn() -> &'a T + 'a> {
    Box::new(move || t)
}
于 2018-04-13T07:40:51.040 回答