非常有趣的问题!我想我理解了这里的问题。让我试着解释一下。
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 中,没有函数参数,但除此之外它与真正的Fn
trait完全相同。
让我们实现它!
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 功能,可以在类型系统中表达所有这些。但是,标准库需要以某种方式赶上才能使您的确切代码成为可能。