为什么在迭代器和读取特征之间使用by_ref().take()会有所不同?

Ole*_*Cat 9 rust borrow-checker

这有两个功能:

fn foo<I>(iter: &mut I)
where
    I: std::iter::Iterator<Item = u8>,
{
    let x = iter.by_ref();
    let y = x.take(2);
}

fn bar<I>(iter: &mut I)
where
    I: std::io::Read,
{
    let x = iter.by_ref();
    let y = x.take(2);
}
Run Code Online (Sandbox Code Playgroud)

虽然第一个编译正常,第二个编译错误:

error[E0507]: cannot move out of borrowed content
  --> src/lib.rs:14:13
   |
14 |     let y = x.take(2);
   |             ^ cannot move out of borrowed content
Run Code Online (Sandbox Code Playgroud)

签名by_reftake几乎完全相同std::iter::Iteratorstd::io::Read特征,所以我认为如果第一个编译,第二个也将编译.我哪里弄错了?

Sve*_*ach 10

这确实是一个令人困惑的错误消息,您得到它的原因相当微妙。ozkriff回答正确地解释了这是因为Read特征不在范围内。我想添加更多上下文,并解释为什么您会收到您看到的特定错误,而不是未找到该方法的错误。

take()上方法ReadIterator采用self由值,或者换句话说,它消耗它的接收机。这意味着只有当您拥有接收器的所有权时才能调用它。您问题中的函数iter通过可变引用接受,因此它们不拥有底层I对象,因此您不能调用<Iterator>::take()<Read>::take()底层对象。

然而,正如ozkriff指出,标准库提供的“转发”的实现Iterator,并Read为以实现各自的特点类型的可变引用。当你调用iter.take(2)你的第一个函数时,你实际上最终调用了<&mut Iterator<Item = T>>::take(iter, 2),它只消耗你对迭代器的可变引用,而不是迭代器本身。这是完全有效的;虽然函数不能使用迭代器本身,因为它不拥有它,但函数确实拥有引用。但是,在第二个函数中,您最终会调用<Read>::take(*iter, 2),它会尝试使用底层读取器。由于您不拥有该阅读器,您会收到一条错误消息,说明您无法将其移出借用的上下文。

那么为什么第二个方法调用解析为不同的方法呢?ozkriff 的回答已经解释了发生这种情况是因为Iteratortrait 在标准前奏中,而Readtrait 默认不在范围内。让我们更详细地看一下方法查找。它记录在Rust 语言参考的“方法调用表达式”部分:

第一步是构建候选接收器类型列表。通过反复取消引用接收者表达式的类型,将遇到的每个类型添加到列表中,然后最后尝试进行无大小强制转换,如果成功,则添加结果类型来获得这些。然后,对于每个候选者T,在 之后立即将&T和添加&mut T到列表中T

根据这个规则,我们的候选类型列表是

&mut I, &&mut I, &mut &mut I, I, &I, &mut I
Run Code Online (Sandbox Code Playgroud)

然后,对于每个候选类型T,在以下位置搜索具有该类型接收器的可见方法:

  1. T的固有方法(直接在 上实现的方法T)。

  2. 由 实现的可见特征提供的任何方法T。如果T是类型参数,T则首先查找trait bounds on 提供的方法。然后查找范围内的所有剩余方法。

对于这种情况I: Iterator,此过程从查找take()方法开始&mut I。上有没有内在的方法&mut I,因为I是一个通用型的,所以我们可以跳过步骤1在步骤2中,我们先来看看了对性状界的方法&mut I,但也有仅特质界I,所以我们移动到查找take()上范围内的所有剩余方法。由于Iterator在范围内,我们确实从标准库中找到了转发实现,并且可以停止处理我们的候选类型列表。

对于第二种情况,I: Read我们也从 开始&mut I,但由于Read不在范围内,我们不会看到转发实现。但是,一旦我们进入I候选类型列表,关于 trait bounds 提供的方法的条款就开始了:首先查找它们,无论 trait 是否在范围内。 I有一个特征界限Read,所以<Read>::take()找到了。正如我们在上面看到的,调用此方法会导致错误消息。

总之,特征必须在范围内才能使用它们的方法,但即使特征不在范围内,也可以使用特征边界上的方法。

  • 换句话说,如果特征中的接收器参数写为“self”,那么它将始终具有“Self”类型,但“Self”允许是可变引用类型,前提是我们在该可变引用上显式实现该特征参考类型。而“&amp;mut Iterator”的转发实现正是这样做的——它为对实现“Iterator”特征的类型的任何可变引用实现“Iterator”特征。 (2认同)
  • @rsalmei 完全正确 - 仅取消引用一次就会导致无限递归。 (2认同)

ozk*_*iff 8

impl<'a, I: Iterator + ?Sized> Iterator for &'a mut I是第一个函数编译的原因.它实现Iterator了对迭代器的所有可变引用.

Read特征具有相当的,但不像IteratorRead特点是没有前奏,所以你需要use std::io::Read使用这个IMPL:

use std::io::Read; // remove this to get "cannot move out of borrowed content" err

fn foo<I, T>(iter: &mut I)
where
    I: std::iter::Iterator<Item = T>,
{
    let _y = iter.take(2);
}

fn bar<I>(iter: &mut I)
where
    I: std::io::Read,
{
    let _y = iter.take(2);
}
Run Code Online (Sandbox Code Playgroud)

操场

  • 对于`Read`,我们确实[等效](https://doc.rust-lang.org/1.29.1/src/std/io/impls.rs.html#20-45).但是,与`Iterator`不同,`Read`特性不是前奏,所以你需要`使用std :: io :: Read`来使用这个impl. (6认同)
  • @Shepmaster我在一个单独的答案中添加了一个有点冗长的解释.我相信你知道原因,但无论如何,你问过.:) (2认同)