Rust类型不匹配,但仅在不使用类型注释时才会出现。

3

动机

我想要读取多个磁盘文件中的值流。这些文件可能是CSV文件,也可能是制表符分隔,或者会采用某些专有二进制格式。因此我希望处理多个文件的函数能够将 Path -> Iterator<Data> 函数作为参数传递进来。如果我理解正确的话,在Rust中我需要将这个迭代器和函数封装起来,因为它们是不确定大小的。因此我的读取函数应该是这样的(这里只是使用 i32 作为数据的简单代理):

fn foo(read_from_file: Box<dyn Fn(&Path) -> Box<dyn Iterator<Item=i32>>>) {
    panic!("Not implemented");
}

为了测试,我不想从磁盘上读取实际文件。我希望我的测试数据就在测试模块中。以下是我想要的大致内容,但出于简单起见,我将其放入了bin项目的主体中:

use std::path::Path;

fn foo(read_from_file: Box<dyn Fn(&Path) -> Box<dyn Iterator<Item=i32>>>) {
    panic!("Not implemented");
}

fn main() {

    let read_from_file = Box::new(|path: &Path| Box::new(match path.as_os_str().to_str().unwrap() {
        "/my_files/data.csv" => vec![1, 2, 3],
        "/my_files/data_2.csv" => vec![4, 5, 6],
        _ => panic!("Invalid filename"),
    }.into_iter()));

    foo(read_from_file);
}

错误

这会导致编译错误:

   Compiling iter v0.1.0 (/home/harry/coding/rust_sandbox/iter)
error[E0271]: type mismatch resolving `for<'r> <[closure@src/main.rs:9:35: 13:19] as FnOnce<(&'r Path,)>>::Output == Box<(dyn Iterator<Item = i32> + 'static)>`
  --> src/main.rs:15:9
   |
15 |     foo(read_from_file);
   |         ^^^^^^^^^^^^^^ expected trait object `dyn Iterator`, found struct `std::vec::IntoIter`
   |
   = note: expected struct `Box<(dyn Iterator<Item = i32> + 'static)>`
              found struct `Box<std::vec::IntoIter<{integer}>>`
   = note: required for the cast to the object type `dyn for<'r> Fn(&'r Path) -> Box<(dyn Iterator<Item = i32> + 'static)>`

For more information about this error, try `rustc --explain E0271`.
error: could not compile `iter` due to previous error

我其实不太理解这个。 std::vec::IntoIter 实现了 Iterator,那么为什么会出现类型错误呢?

解决方法,也是我不太理解的

如果我添加一个显式类型注释 Box<dyn Fn(&Path) -> Box<dyn Iterator<Item=i32>>>,那么就可以编译通过:

use std::path::Path;

fn foo(read_from_file: Box<dyn Fn(&Path) -> Box<dyn Iterator<Item=i32>>>) {
    panic!("Not implemented");
}

fn main() {

    let read_from_file : Box<dyn Fn(&Path) -> Box<dyn Iterator<Item=i32>>>
        = Box::new(|path: &Path| Box::new(match path.as_os_str().to_str().unwrap() {
        "/my_files/data.csv" => vec![1, 2, 3],
        "/my_files/data_2.csv" => vec![4, 5, 6],
        _ => panic!("Invalid filename"),
    }.into_iter()));

    foo(read_from_file);

我非常困惑这个为什么会工作。我对 Rust 的理解是,在 let 定义中,显式类型是可选的——除非编译器无法推断它,在这种情况下,编译器应该发出 error[E0283]: type annotations required 错误提示信息。

1
另一个选择是在内部闭包上添加返回类型注释:Box :: new(| path:&Path | - > Box <dyn Iterator <Item = _ >> {Box :: new(...)}) - PitaJ
3
我确定有一个无法找到的重复内容,但在Rust中,Box<T>Box<dyn Trait>是根本不同的东西,并且具有不同的内存布局。如果你只是将某个Iterator实例装箱,则它不会成为Box<dyn Iterator>。你必须将其转换为一个。 - Aplet123
2个回答

3
指向动态大小类型 (DST) 的指针,比如 Box<dyn Iterator<Item=i32>>,是"fat"的。而 Box<std::vec::IntoIter<i32>> 不是 DST 的指针(因为已知 IntoIter 的大小),因此可以是一个“thin”指针,仅指向堆上的 IntoIter 实例。
相比于 thin 指针,创建和使用 fat 指针更加昂贵。这就是为什么,正如 @Aplet123 所提到的,你需要明确地告诉编译器 某种方式(通过类型注释或一个 as 转换),你想将由你的闭包生成的 thin Box<std::vec::IntoIter<i32>> 指针转换为 fat Box<dyn Iterator<Item=i32>> 指针的原因。
请注意,如果您删除let绑定并在foo函数调用的参数列表中创建闭包,则编译器会使闭包必须返回一个fat指针,因为foo期望的参数类型。

这也非常有帮助。虽然我仍然不太明白为什么编译器能够在参数列表中创建闭包时正确地进行类型推断,但如果它是在上面创建的话就不行。 - Harry Braviner
除了整数字面量之外,编译器必须知道 let 绑定在声明时的类型。如果闭包没有注释声明,编译器会假设返回类型为 Box<IntoIter>,原因在答案中已经说明 -- 它不能使用未来的用法来确定 let 绑定的类型,而是未来的用法必须与绑定在声明时的类型相符。但是,如果在参数列表中创建闭包,编译器知道函数需要返回一个 Box<dyn Iterator> 的闭包,并能够自动执行适当的转换。 - EvilTak
1
稍作澄清:编译器可以根据后面的信息推断类型(例如,let mut x = vec![]; x.push("hello"); 可以工作),但它不能为 闭包参数 的类型做到这一点,除了数值类型(浮点数也可以)。 - cameron1024

1
对我来说,这似乎是类型推断失败了,因为闭包无法推断出它需要返回指向 v-table 的指针(来自 dyn Iterator)。
然而,我建议这里可能不需要 Box<dyn Foo>。虽然 Iterator 是一个 trait,你不能在编译时知道它的大小,但从某种意义上说,你可以。
Rust "单态化"泛型代码,这意味着它为每个具体类型生成泛型函数/结构等的副本。例如,如果你有:
struct Foo<T> {
  value: T
}

fn main() {
  let _ = Foo { value: "hello" };
  let _ = Foo { value: 123 };
}

它将生成一个Foo_str_'static和一个Foo_i32(粗略地说),并在需要时进行替换。

您可以利用此功能,在使用特性的同时使用通用静态调度。 您的函数可以重写为:

fn foo<F, I>(read_from_file: F)
where
  F: Fn(&Path) -> I,
  I: Iterator<Item = i32>,
{
  unimplemented!()
}

fn main() {
  // note the lack of boxing
  let read_from_file = |path: &Path| {
    // ...
  };

  foo(read_from_file);
}

这段代码可能(但我没有进行基准测试)更快、更符合习惯用法,并且可以消除编译器错误。


这很有道理。但现在我的函数有两种类型(FI)是通用的。实际上,foo将成为结构体的new方法,如果我按照这种方法进行,看起来我的结构体最终必须通用于FI。如果我想编写结构体的类型,则需要更多的样板文件。有没有什么办法可以避免这种情况? - Harry Braviner
通常情况下,您可以依靠类型推断来解决大部分问题。在我给出的示例中,尽管 foo 具有类型参数,但我们实际上不必指定它,编译器可以从上下文中推断出它的类型。根据我的经验,这是“默认”的情况,只有在特殊情况下才需要显式指定类型参数。 - cameron1024
好的,我在我的实际代码中进行了这个更改,它确实有效。通过这样做,我是否严格地失去了一些灵活性,因为类型 F 必须在编译时已知?但是,作为回报,如果 F 被频繁调用,我可能会获得一些性能提升,因为不再有胖指针。 - Harry Braviner
请注意,使用泛型类型参数 I 作为返回类型将意味着闭包必须始终返回相同的 Iterator 类型 -- 有关更多信息,请参见 Rust 书中的 此章节。您是正确的,泛型使您失去了灵活性,但获得了运行时性能,因为 FI 都必须在编译时知道。 - EvilTak
1
在运行时,FT都必须事先知道。EvilTak的观点是正确的,你会失去在运行时选择2种不同类型的迭代器的能力(除非你创建一些类似于WrappingIterator<T>的东西,它包装了一个Box<dyn Iterator<Item = T>>或类似的东西)。但这最终取决于你的需求。如果你发现经常遇到这个问题,并且你不需要性能,也许向box-y解决方案转移更好。我的经验是,“惯用”的rust代码往往比Box<dyn Foo>更依赖于泛型,但这只是我的经验。 - cameron1024
值得注意的是,Iterator通常是静态分派和动态分派差异更为显著的地方,因为它们经常在相对紧密的循环中被调用。但是,与任何性能问题一样,我建议首先编写惯用代码,如果基准测试显示速度太慢,则进行重构。 - cameron1024

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