为什么 find 闭包的参数需要两个 & 符号?

25

我一直在使用 Rust 技术,将我基于 OCaml 实现的函数式 Score4 AI 引擎进行了移植。我特别想看看 Rust 在处理函数式代码方面的表现。

最终结果是:它可以正常工作,并且非常快 - 远比 OCaml 快。它几乎接近于命令式的 C/C++ 速度 - 这真是太棒了。

然而,有一件事情让我困惑-为什么在这段代码的最后一行需要两个与符号(&&)?

let moves_and_scores: Vec<_> = moves_and_boards
    .iter()
    .map(|&(column,board)| (column, score_board(&board)))
    .collect();
let target_score = if maximize_or_minimize { 
    ORANGE_WINS 
} else { 
    YELLOW_WINS 
};
if let Some(killer_move) = moves_and_scores.iter()
    .find(|& &(_,score)| score==target_score) {
         ...
我添加它们的原因是编译器的错误“引导”了我;但我正在试图理解为什么......我使用了 Stack Overflow 其他地方提到的技巧,向编译器“询问”某个东西的类型。

我之所以添加它们,是因为编译器的错误信息“指引”我这样做;但是我正试图理解为什么……我使用了 Stack Overflow 上别处提到的技巧,“询问”编译器告诉我某些东西的类型。

let moves_and_scores: Vec<_> = moves_and_boards
    .iter()
    .map(|&(column,board)| (column, score_board(&board)))
    .collect();
let () = moves_and_scores;

导致此错误的原因是:

src/main.rs:108:9: 108:11 error: mismatched types:
 expected `collections::vec::Vec<(u32, i32)>`,
    found `()`
(expected struct `collections::vec::Vec`,
    found ()) [E0308]
src/main.rs:108     let () = moves_and_scores;

正如我所预料的那样,moves_and_scores 是一个元组的向量:Vec<(u32, i32)>。但是在紧接着的下一行中,iter()find() 强制我在闭包参数中使用丑陋的双重引用符号:

if let Some(killer_move) = moves_and_scores.iter()
    .find(|& &(_,score)| score==target_score) {
find闭包为什么需要两个“&”?我可以理解为什么需要一个(通过引用传递元组来节省时间/空间),但是为什么需要两个呢?这是因为iter吗?也就是说,iter创建了引用,然后find期望在每个输入上都有一个引用,因此需要一个引用的引用?
如果是这样的话,这是否是Rust中相当丑陋的设计缺陷?
实际上,我希望findmap和所有其他函数式基元都是集合本身的一部分。强制我使用iter()来执行任何类型的函数式工作似乎很繁琐,如果它迫使我在每个可能的函数式链中都使用这种“双“&””,那么更是如此。
我希望我错过了一些显而易见的东西-任何帮助/澄清都非常欢迎。

1
恭喜你成功管理了端口,打败OCaml在函数式编程方面的表现,这说明你做对了一些事情! - Matthieu M.
@MatthieuM。谢谢!我希望有更简洁的方法来处理函数链(即.iter().map(...)。iter().filter() ... .iter().find(...))而不需要在每个步骤中引入额外的引用级别 - 但似乎我无法避免它。 - ttsiodras
1个回答

26

这里

moves_and_scores.iter()

这段内容是关于编程的。它讲解了如何使用一个借用的向量元素迭代器,以及如何使用“find”方法来查找满足特定条件的元素。在你的情况下,“Item”类型为“(u32, i32)”,并且需要使用一个参数为“&&(u32, i32)”类型的谓词函数作为“find”方法的输入。请注意保留原文中的HTML标签。
pub trait Iterator {
    ...
    fn find<P>(&mut self, predicate: P) -> Option<Self::Item>
    where P: FnMut(&Self::Item) -> bool {...}
    ...            ^

它可能被定义为this,因为它只需要检查该项并返回一个布尔值。这不需要通过值传递该项。

如果你想要一个(u32, i32)的迭代器,你可以写

moves_and_scores.iter().cloned()

cloned()方法将拥有类型为&TItem项的迭代器转换为拥有类型为TItem项的迭代器,如果T是可克隆的。另一种方法是使用into_iter()而不是iter()

moves_and_scores.into_iter()

第一种方法克隆了借用的元素,而第二种方法使用向量并将元素移出。
通过像这样编写lambda函数
|&&(_, score)| score == target_score

你可以解构“双重引用”,并创建一个的本地副本。由于是简单类型,具有复制(Copy)特性,所以这是允许的。
除了解构谓词的参数外,您还可以编写以下代码:
|move_and_score| move_and_score.1 == target_score

因为点运算符会自动解除引用所需的次数。

感谢您的反馈!iter().cloned() 有性能影响吗?我的意思是,它是否实际分配“克隆”?此外,我相信 into_iter() 肯定会有性能影响,因为它的移动语义将会改变源向量。我认为最好的方法是您第三个建议——使用“自动解引用尽可能多次”——我需要进一步了解一下这个。 - ttsiodras
1
顺便问一下,你们有没有想过为什么 Rust 没有将函数式操作符(map、filter 等)作为集合的一部分,并需要首先使用 .iter()(这在每个步骤中引入了额外的“引用间接层级”)...? - ttsiodras
1
@ttsiodras 关于性能:这些都不应该真正有所影响。克隆类型为(u32,i32)的元组非常便宜。不要让“克隆”这个词吓到你。 :-) 对于简单的“普通数据类型”,克隆和复制基本上是相同的。但是某些类型无法复制,因为它们更加复杂。只有在这些情况下,我才会担心克隆。 - sellibitze
4
有许多不同的方法可以迭代集合(iteriter_mutinto_iterdrain),每种方法都有其用途,没有一种“正确”的方式。因此,您需要具体情况具体分析。 - sellibitze
1
Rust 是软件工程中令人惊叹的体验。 - geckos

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