非常有趣的问题!我认为我理解这里的问题。让我试着解释一下。
tl;dr:闭包不能返回对通过移动捕获的值的引用,因为那将是对self 的引用。无法返回此类引用,因为 Fn* 特征不允许我们表达。这与 streaming iterator problem 基本相同,可以通过 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()); // 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。
我想用这个 sn-p 说明的一点:我们不需要在类型 Baz 上注释任何生命周期来确保安全; Rust 自己解决了这个问题,并强制 baz 的寿命不超过 s。这在下面很重要。
为其编写特征
到目前为止,我们只介绍了基础知识。让我们试着写一个像Fn 这样的特征来更接近你原来的问题:
trait MyFn {
type Output;
fn call(&self) -> Self::Output;
}
在我们的 trait 中,没有函数参数,但除此之外它与 the real 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> 的任何实例都不能比 T 可能引用的东西寿命更长。因此,self 的生命周期已经比 T 的任何生命周期短。因此我们只需要集中注意力在self 生命周期内)
但是我们如何在 trait impl 中表达这一点?事实证明:我们不能(还)。在流式迭代器的上下文中经常提到这个问题——也就是说,迭代器返回一个生命周期绑定到self生命周期的项目。在今天的 Rust 中,很难实现这一点;类型系统不够强大。
未来呢?
幸运的是,有一个 RFC "Generic Associated Types" 不久前被合并了。该 RFC 扩展了 Rust 类型系统,以允许关联的特征类型是通用的(在其他类型和生命周期上)。
让我们看看我们如何使您的示例(有点)与 GAT 一起工作(根据 RFC;这些东西还不能工作 ☹)。首先我们必须改变 trait 定义:
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,我们需要这样写(根据this section of the RFC:
fn make_baz<T>(t: T) -> Box<for<'a> MyFn<Output<'a> = &'a T>> {
Box::new(Baz(t))
}
如果我们想使用 real Fn 特征怎么办?据我了解,即使使用 GAT,我们也不能。我认为不可能改变现有的 Fn 特征以向后兼容的方式使用 GAT。所以标准库很可能会保持不那么强大的特性。 (旁注:如何以向后不兼容的方式发展标准库以使用新的语言特性已经I wondered about 几次了;到目前为止,我还没有听说这方面的任何真正计划;我希望 Rust 团队来有什么...)
总结
您想要的在技术上并非不可能或不安全(我们将它实现为一个简单的结构并且它可以工作)。然而,不幸的是,现在不可能在 Rust 的类型系统中以闭包/Fn 特征的形式表达你想要的东西。这与 流式迭代器 正在处理的问题相同。
使用计划中的 GAT 功能,可以在类型系统中表达所有这些。但是,标准库需要以某种方式迎头赶上才能使您的确切代码成为可能。