为什么Rust编译器要求Option<&impl Trait>需要类型注解?

6

鉴于这个最小可复现示例:

fn main() {
    println!("{}", foo(None));
}

trait Trait {}
struct Struct {}
impl Trait for Struct {}

fn foo(maybe_trait: Option<&impl Trait>) -> String {
    return "hello".to_string();
}

Rust编译器不高兴:

error[E0282]: type annotations needed
 --> src\main.rs:2:20
  |
2 |     println!("{}", foo(None));
  |                    ^^^ cannot infer type for `impl Trait`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0282`.

使用类型注释可以使这个代码编译通过:
fn main() {
    let nothing: Option<&Struct> = None;
    println!("{}", foo(nothing));
}

trait Trait {}
struct Struct {}
impl Trait for Struct {}

fn foo(maybe_trait: Option<&impl Trait>) -> String {
    return "hello".to_string();
}

如果我们在类型注释中使用Trait而不是Struct,会向我们提供更多的信息:
warning: trait objects without an explicit `dyn` are deprecated
 --> src\main.rs:2:26
  |
2 |     let nothing: Option<&Trait> = None;
  |                          ^^^^^ help: use `dyn`: `dyn Trait`
  |
  = note: #[warn(bare_trait_objects)] on by default

error[E0277]: the size for values of type `dyn Trait` cannot be known at compilation time
 --> src\main.rs:3:20
  |
3 |     println!("{}", foo(nothing));
  |                    ^^^ doesn't have a size known at compile-time
  |
  = help: the trait `std::marker::Sized` is not implemented for `dyn Trait`
  = note: to learn more, visit <https://doc.rust-lang.org/book/ch19-04-advanced-types.html#dynamically-sized-types-and-the-sized-trait>
note: required by `foo`
 --> src\main.rs:10:1
  |
10| fn foo(maybe_trait: Option<&impl Trait>) -> String {
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

error: aborting due to previous error

For more information about this error, try `rustc --explain E0277`.

我理解为“您不应在此处使用Trait,因为我不知道需要为此参数分配多少内存”。

但是,当我传递None时,这与相关吗?
当然,传递实现了Trait(即Struct)的类型的任何具体实例对编译器都是可以的。


附注:
我已经阅读了此答案,介绍了&dyn Trait&impl Trait之间的区别。我不确定何时使用哪种方法,但由于我的程序使用类型注释(如上所示)和&impl Trait进行编译,因此它似乎是安全的选择。

如果我们将函数参数的类型设置为Option<&dyn Trait>,则我的程序在main()中没有类型注释的情况下编译:

fn main() {
    println!("{}", foo(None));
}

trait Trait {}
struct Struct {}
impl Trait for Struct {}

fn foo(maybe_trait: Option<&dyn Trait>) -> String {
    return "hello".to_string();
}

$ cargo --version
cargo 1.37.0 (9edd08916 2019-08-02)  

$ cat Cargo.toml
[package]
name = "rdbug"
version = "0.1.0"
authors = ["redacted"]
edition = "2018"

2
这是因为编译器必须"推断"类型,即使只有一个可猜测的类型(除了某些特定情况之外),它也不能“猜测”它们。'None'可能是'Option<Struct>::None' 或 'Option::<AnotherTypeImplementingTrait>::None'。参数位置的'impl trait'类似于泛型,所以编译器必须知道要实例化的'foo'版本,并且'None'可能是任何东西。 - trent
1
请参考以下示例:有没有一种方法可以提示编译器在使用Option::None时使用某种默认泛型类型?(所有答案)和将可选函数参数指定为特征而不是具体类型。这些中的一个是否回答了您的问题,或者您仍然不确定? - trent
2
无论如何,选项的大小取决于实现特质的类型。 - Stargateur
3
我认为你可能忽略了 foo::HugeThing 即使传入 None,其行为可能与 foo::OtherThing 不同,编译器必须知道使用正确的函数。 - trent
1
不是所有引用的大小都相同,因为对于无大小类型的引用,它们是胖指针,但即使它们是,编译器仍然不会为您选择类型。 - trent
显示剩余4条评论
1个回答

5

这个:

fn foo(maybe_trait: Option<&impl Trait>) -> String {

仅仅是以下代码的语法糖:

fn foo<T: Trait>(maybe_trait: Option<&T>) -> String {

这意味着编译器将为每个你要与其使用的实现 Trait 的类型 T 生成许多 foo 函数。因此,即使您使用 None 调用它,编译器也需要知道在该情况下是哪个 T,以便选择/生成正确的函数。 Option<T> 类型在内存中的表示方式取决于 T 类型的表示方式。函数 foo 的编译汇编取决于此。对于不同的 T,生成的汇编代码可能看起来不同。(例如定义是 Some 还是 None 的枚举标记可能在不同的字节偏移处。它可以使用不同的寄存器,它可能决定是否展开循环,内联函数,矢量化...) 这是静态调度的优势,即使您编写具有大量抽象的代码,您也会获得完全针对实际使用的具体类型进行优化的代码。
使用即将到来的 特殊功能,您实际上可以为不同的 T 子集手动编写不同的 foo 实现,因此编译器知道您正在调用哪个 foo 非常重要。每个都可以对 None 做出不同的操作。
另一方面,这个:
fn foo(maybe_trait: Option<&dyn Trait>) -> String {

意味着有仅有一个函数foo,它接受包含指向某种实现Trait的类型的fat指针的Option。如果你在函数内调用maybe_trait上的一些方法,则会经过动态调度。
由于只有一个函数foo,当使用None时,您无需对类型进行任何说明,因为只有一个类型。
但是动态分派是有代价的 - 这个函数没有针对任何特定的T进行优化,它以动态方式处理每个T

谢谢!加上@trenctl的评论,这很完美。我会稍等一下再接受,以便他也有机会写答案,因为他也解释了一些好的观点。 - lucidbrot
1
@lucidbrot 谢谢你,但是除非你认为我在这个答案中没有解释清楚我的评论中有重要的东西,否则我会让 michalsrb 有勾选标记 :) 很高兴能帮到你! - trent
这终于让我恍然大悟,非常好的答案。 - detly

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