为什么在Iterator和Read traits中使用by_ref().take()的方式不同?

17

这里有两个函数:

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);
}

尽管第一个代码可以成功编译,但第二个会导致编译错误:

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

std::iter::Iteratorstd::io::Read特征中的by_reftake的签名几乎相同,因此我认为如果第一个编译通过,第二个也会编译通过。 我错在哪里了?

2个回答

14

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

Read特性也有相应的实现,但与Iterator不同的是,Read特性不在预导入模块中,所以您需要use std ::io::Read来使用此实现:

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);
}

Playground


7
我们确实有Read等价物。但是,与Iterator不同,Read特质没有预导模块,所以您需要使用use std::io::Read来使用此实现。 - kennytm
请解释为什么在“Read”在作用域之前,OP会得到特定的错误。 - Shepmaster
2
@Shepmaster,我在另一个答案中添加了一个相当详细的解释。我相信你知道原因,但无论如何,你都问了。 :) - Sven Marnach

14
这确实是一个令人困惑的错误信息,你之所以会得到这个错误是因为原因相当微妙。ozkriff的答案正确地解释了这是因为Read特质不在作用域内。我想添加一些更多的上下文和解释,为什么你会看到特定的错误,而不是方法未找到的错误。 ReadIterator上的take()方法通过值获取self,换句话说,它消耗了它的接收器。这意味着只有当你拥有接收器时才能调用它。你问题中的函数通过可变引用接受iter,因此它们不拥有底层的I对象,所以你不能为底层对象调用<Iterator>::take()<Read>::take()
然而,正如ozkriff指出的那样,标准库为实现相应特质的类型提供了对可变引用的“转发”实现。当你在第一个函数中调用iter.take(2)时,实际上会调用<&mut Iterator<Item = T>>::take(iter, 2),它只消耗了你对迭代器的可变引用,而不是迭代器本身。这是完全有效的;虽然函数不能消耗迭代器本身,因为它没有拥有它,但函数确实拥有引用。然而,在第二个函数中,你最终调用了<Read>::take(*iter, 2),它试图消耗底层读取器。由于你没有拥有那个读取器,你会得到一个错误消息,说明你无法将其移动出借用上下文。
那么为什么第二个方法调用解析为不同的方法呢?ozkriff的答案已经解释了这是因为Iterator特质在标准预导模块中,而Read特质默认情况下不在作用域内。让我们更详细地看一下方法查找。这在Rust语言参考手册的“方法调用表达式”部分有记录:

第一步是构建接收方类型的候选列表。通过重复解引用接收方表达式的类型,将遇到的每种类型添加到列表中,然后最后尝试进行非大小限制的强制转换,如果成功,则添加结果类型。然后,对于每个候选项T,立即在T之后添加&T&mut T

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

然后,对于每个候选类型T,在以下位置搜索具有该类型接收器的可见方法:
1. T的内在方法(直接在T上实现的方法)。 2. 由T实现的任何可见特征提供的方法。如果T是类型参数,则首先查找T上的特征边界提供的方法。然后查找所有剩余的作用域中的方法。
对于的情况,这个过程从在&mut I上查找take()方法开始。由于I是一个泛型类型,所以&mut I上没有内在方法,因此我们可以跳过步骤1。在步骤2中,我们首先查找&mut I的特征边界上的方法,但只有I的特征边界,所以我们继续查找作用域中所有剩余的方法来查找take()。由于Iterator在作用域中,我们确实找到了标准库中的转发实现,并且可以停止处理我们的候选类型列表。
对于第二种情况,,我们也从&mut I开始,但由于Read不在作用域中,我们将看不到转发实现。然而,一旦我们到达候选类型列表中的I,特征边界提供的方法子句就会生效:无论特征是否在作用域中,它们都会首先被查找。I具有Read的特征边界,因此找到了::take()。正如我们上面所看到的,调用此方法会导致错误消息。
总之,要使用其方法,必须将特征包含在作用域中,但是即使特征不在作用域中,也可以使用特征边界上的方法。

1
@rsalmei self 的类型始终取决于您调用方法的类型。如果该类型是可变引用,并且在该可变引用类型上有 Iterator 的实现,则通过值获取 self 意味着获取一个可变引用。 - Sven Marnach
2
换句话说,如果特质中的接收器参数写为 self,那么它将始终具有类型 Self,但是只要我们在可变引用类型上显式实现该特质,就允许 Self 成为可变引用类型。而 &mut Iterator 的转发实现正是这样做的——它为任何实现 Iterator 特质的类型的可变引用实现了 Iterator 特质。 - Sven Marnach
谢谢Sven!我想我明白了,这就解释了为什么在核心和mut I实现中他们必须两次解引用它"(**self)"!非常好。 - rsalmei
2
@rsalmei 没错 - 只解引用一次会导致无限递归。 - Sven Marnach
很好的解释。我能问一下为什么许多迭代器方法(比如take和count)默认需要self并消耗迭代器,而&mut self选项也是可用的吗? - raj
显示剩余2条评论

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