非常有趣的问题!我认为我理解了这里涉及到的问题。让我试着解释一下。
tl;dr: 闭包不能返回对通过移动捕获的值的引用,因为那将是对self
的引用。这样的引用无法返回,因为Fn*
特征不允许我们表达这种情况。 这基本上与流迭代器问题相同,可以通过GAT(通用关联类型)来修复。
手动实现
您可能已经知道,当您编写一个闭包时,编译器会生成一个结构体和实现适当的Fn
特征的impl
块,因此闭包基本上是语法糖。让我们尝试避免所有这些语法糖,并手动构建您的类型。
您想要拥有另一种类型并能够返回该拥有的类型的引用。而且,您希望有一个函数,该函数返回所述类型的装箱实例。
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());
baz
};
println!("{}", outside.call());
这个很好用。字符串
s
被移动到类型
Baz
中,然后将该
Baz
实例移动到
Box
中。
s
现在归
baz
所有,然后是
outside
。
当我们添加一个字符时,情况变得更加有趣:
let outside = {
let s = "hi".to_string();
let baz = make_baz(&s);
println!("{}", baz.call());
baz
};
println!("{}", outside.call());
现在我们不能使
baz
的生命周期比
s
的生命周期更长,因为
baz
包含对
s
的引用,如果
s
的作用域先于
baz
结束,那么
s
就会成为一个悬垂引用。
我想通过这个片段表达的意思是:我们不需要在类型
Baz
上注明任何生命周期就可以使其安全;Rust自己找出来并强制执行
baz
的生命周期不会超过
s
的生命周期。这在下面将很重要。
编写一个trait
到目前为止,我们只涵盖了基础知识。让我们尝试编写一个类似于
Fn
的trait,以更接近您的原始问题:
trait MyFn {
type Output;
fn call(&self) -> Self::Output;
}
在我们的特征中,没有函数参数,但除此之外,它与真正的Fn
特征基本相同。
让我们来实现它!
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>
的实例都不能比
T
可能引用的东西存活得更长。因此,
self
生命周期已经短于
T
可能具有的任何生命周期。因此,我们只需要集中精力处理
self
生命周期)
但是,我们如何在特质实现中表达这一点呢?事实证明:
我们无法(至少目前还无法)。在
流式迭代器的上下文中经常提到这个问题--也就是说,迭代器返回的项目的生命周期受限于
self
生命周期。在当今的Rust中,很遗憾,无法实现这一点;类型系统还不够强大。
未来会怎样呢?
幸运的是,有一个
RFC“通用关联类型”已经在一段时间前合并了。这个RFC扩展了Rust类型系统,允许特征的相关类型是泛型的(超过其他类型和生命周期)。
让我们看看如何使用GATs使您的示例(有点)能够工作(根据RFC;这些东西还不能正常工作☹)。首先,我们必须更改特征定义:
trait MyFn {
type Output<'a>;
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
特质呢?据我所知,即使使用GATs,我们也无法做到这一点。我认为不可能以向后兼容的方式更改现有的Fn
特质以使用GATs。因此,标准库很可能会保留较弱的特质。 (副注:如何以向后不兼容的方式演变标准库以使用新语言特性是我已经多次思考的问题;到目前为止,我还没有听说过任何实际计划;我希望Rust团队能够想出一些办法...)
概述
你想要的并不是技术上不可能或不安全的(我们将其实现为一个简单的结构体,它可以工作)。然而,不幸的是,目前在Rust的类型系统中无法以闭包/Fn
特质的形式表达你想要的内容。这与流迭代器正在处理的问题相同。
有了计划中的GAT功能,就可以在类型系统中表达所有这些。但是,标准库需要做出某些改进才能使你的精确代码成为可能。
self
参数进行调用”——非常好的观察!我完全没有注意到。 - Lukas Kalbertodt