如何理解返回另一个函数的 Rust 函数?

6

阅读有关Rust的文章时,我遇到了一个例子函数,它接受一个数字并返回一个将该数字加到另一个数字的函数。

fn higher_order_fn_return<'a>(step_value: &'a i32) -> Box<Fn(i32) -> i32 + 'a> {
    Box::new(move |x: i32| x + step_value)
}

这里有很多Rust特定的机制,我无法理解。我确定其中一些与生命周期管理有关,但为什么必须以这种方式编写仍然不清楚。以下是几个问题:
  • 为什么将step_value作为引用传递?
  • 为什么返回函数需要使用boxed?
  • 如何解释非常规的函数类型写法(如Fn(i32) -> i32 + 'a)?
  • 'a为什么要写成泛型(<'a>),但在返回类型中又被“添加”(+ 'a)?
  • move的含义是什么,这里移动了什么?
2个回答

8

有一个禁止提问多个问题的规定,但由于所有问题都属于“这段代码的含义是什么”,所以我不会抱怨。此外,它确实将相当多的奇怪之处压缩成了一个相对较小、不太常见的片段。

为什么要将 step_value 作为引用传递?

不知道。它就是这样。如果按值传递,不会显著改变代码的语义。但它正在被引用传递,这就是所有其他与生存期相关的问题的原因。

为什么返回的函数被封装?

它没有返回函数。函数由 fn 定义。它返回一个闭包。问题在于每个闭包实际上都是一个匿名类型的实例(有时称为“伏地魔类型”),出于性能原因。匿名类型是个问题,因为你不能给它们命名,但你必须给你的返回类型命名。

绕过此问题的方法是返回一个特质对象。在这种情况下,它返回一个Fn。还有FnMutFnOnce。它返回了一个盒装对象,因为裸的特质对象不能按值传递,所以特质对象总是在某种指针的后面(可以是Box&Rc等)。

它们不能按值传递,因为编译器无法计算出它的大小,这使得它们的移动几乎不可能。之后,逻辑的训练直接进入了“编译器如何实现”的领域,这在此处有些超出范围。

如何解释非常规的函数类型写法(如Fn(i32) -> i32 + 'a)?

对于Rust来说,这并不不寻常。其他语言如何做与此无关。
让我们暂时忽略+ 'a,因为那实际上是另一回事。 Fn(i32) -> i32是重要的部分。 Rust中的每个“可调用”对象都实现了一个或多个FnFnMutFnOnce trait,这就是Rust表达能够调用某些东西的方式。括号内的内容是参数,->后面的内容是返回类型,就像函数一样。
您可以在问题"When does a closure implement Fn, FnMut and FnOnce?"中了解有关这些trait的更多信息。
由于生命周期是类型系统的一部分,因此首先它们会放在泛型参数列表中(即<...>内的内容)。
其次,编译器需要理解 Box 中的特质对象存在的有效期。如果你有一个 Box<SomeTrait>,编译器会允许该值存在多久?通常,这些信息都应包含在类型中,但如果使用了特质,则编译器不知道使用的是哪种类型。记住,你可以从任何一个实现了 SomeTraitBox<T> 创建一个 Box<SomeTrait>
在这种情况下,闭包将保持对 step_value 的借用,这意味着它不能超出该借用的生命周期(也就是 'a 生命周期)。但如果类型仅为 Box<Fn(i32) -> i32>,则编译器无法获得该信息。因此,有一种语法可指定某个类型隐藏在特质对象之后时,它不会超出给定的生命周期。
这就是+ 'a的含义:“这是一个实现了Fn(i32) -> i32 trait的封装值,它不能超出生命周期'a”。通常情况下,编译器会尝试猜测如何使闭包起作用,但并不总是正确的。在可能的情况下,它会尝试借用闭包捕获的内容。因此,当你在闭包内使用step_value时,编译器通常只会借用它。这本来不是问题,除了你要将闭包返回到函数外部。这种自动借用只能持续函数的生命周期,而这不够长。为了解决这个问题,你可以将step_value 移动到闭包中,而不是借用它。如果你在Box<Trait + 'a>中不写+ 'a,通常会发生什么?
实际上,编译器在这里有一个启发式的方法。默认情况下,每个特质对象都有一个附加的生命周期。它继承自包装它的指针。因此,&'a Trait 实际上是&'a (Trait + 'a)。Box没有自己的生命周期参数,因此它得到了static (即Box是Box),这意味着默认情况下,盒装特质对象不能包含任何非static借用。

1
step_value是按引用传递的,仅仅是为了展示一个具有非平凡生命周期的例子。 - Boiethios

2
为什么将step_value作为引用传递?
没有很好的理由。按值传递可以让一切变得更简单。但是,所讨论的示例可能之所以这样做,是因为你不能对每种类型都这样做,只能对那些是Copy的类型这样做。
为什么要返回函数的封装对象?
无法命名 lambda 的类型,因此无法从函数返回它。因此,必须返回一个特质对象(Fn 是一个特质),为此需要一个盒子。(使用 impl Trait 后,您将不再需要盒子。)
如何解释非常规的函数类型写法(如Fn(i32) -> i32 + 'a)? Fn 有一点语法糖,其中语法 Fn(arg1, arg2) -> ret 是简写(我认为)Fn<(arg1, arg2), Output=ret>。上面的+比错误优先级低,并不是Fn 约束的一部分;相反,它是一种约束组合,意味着Box中的类型既必须是Fn(i32) -> i32,又必须具有生命周期'a
为什么将'a写成泛型(<'a>),但在返回类型中“添加”(+ 'a)?
生命周期参数必须在函数(或类型)的通用参数部分中声明,因此需要<'a>。然后它出现在参数的引用类型中(& 'a i32),最后作为Box中的附加约束。 move的含义是什么,这里移动了什么?
它使闭包成为移动闭包,这意味着它捕获的内容被移动到闭包中,而不是通过引用捕获。但请注意,在此示例中,被移动的是一个引用,即step_value本身!

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接