跳转到内容

Rust 错误 E0283:type annotations needed(sum、collect、into 需要类型注解)

E0283 的意思是:Rust 知道你的值必须能做什么,但能做到这一点的类型有很多,而 Rust 不会替你挑一个。你通常会在 .sum()、.collect() 或 .into() 之后马上碰到它。这是最好修的错误之一:写明你想要的类型,就搞定了。这个系列的其他错误,请看 Rust 编译错误详解。

error[E0283]: type annotations needed
--> src/main.rs:3:9
|
3 | let total = prices.iter().sum();
| ^^^^^ --- type must be known at this point
|
= note: the type must implement `Sum<&i32>`
= help: the following types implement trait `Sum<A>`:
`Duration` implements `Sum<&'a Duration>`
`Duration` implements `Sum`
`Option<T>` implements `Sum<Option<U>>`
`Result<T, E>` implements `Sum<Result<U, E>>`
`Saturating<u128>` implements `Sum<&'a Saturating<u128>>`
`Saturating<u128>` implements `Sum`
`Saturating<u16>` implements `Sum<&'a Saturating<u16>>`
`Saturating<u16>` implements `Sum`
and 88 others
note: required by a bound in `std::iter::Iterator::sum`
help: consider giving `total` an explicit type
|
3 | let total: /* Type */ = prices.iter().sum();
| ++++++++++++

第一行的意思是“需要类型注解”。最后的 help: 建议你“给 total 写一个明确的类型”。

产生这个错误的程序是这样的:

fn main() {
let prices = vec![250, 120, 99]; // 价格,单位是分
let total = prices.iter().sum();
println!("总计:{total} 分");
}

.collect() 和 .into() 会报同样的错误,只是 note 不一样。对于 let words = text.split(' ').collect();,note 是 the type must implement `FromIterator<&str>`。对于 let name = "Ferris".into();,note 是 the type must implement `From<&str>`。

从上往下读这条信息:

  1. total 下面的 ^^^^^: 这就是 Rust 没法确定类型的那个变量。
  2. sum 下面的 ---: “type must be known at this point”(到这里必须知道类型)。sum 可以返回很多种不同的类型,所以 Rust 需要在这里得到答案。
  3. note: the type must implement `Sum<&i32>`(这个类型必须实现 Sum<&i32>)。这是 Rust 找到的规则:“total 必须是一个能把一堆 &i32 加起来得到的类型”。
  4. help 列表: 那些知道怎样成为“某种东西之和”的类型。它们有很多:每一种数字类型、Duration,甚至还有 Option 和 Result。Rust 列出了 8 个,然后说“and 88 others”(还有 88 个)。人们说的 multiple impls satisfy this bound(有多个实现满足这个约束)就是这个意思:不止一个类型有符合这条规则的 impl。

想象你在咖啡馆点了“随便来点喝的”。这是一条真实的规则(必须能喝),但茶、果汁和水都符合。你不选,服务员就什么也端不上来。E0283 就是 Rust 端着托盘,在等你的答复。

sum、collect 和 into 在返回类型上是泛型的。你要的类型,决定了这个函数做什么。trait(Sum、FromIterator、From)只是规则。所以当没有任何东西告诉 Rust 类型时,它只有规则,没有唯一的答案。

而答案很重要。把同样的单词收集进 Vec<&str> 会保留重复项,收集进 HashSet<&str> 则会去掉重复项。求和得到 u8 时,超过 255 就会溢出,而 u64 不会。如果 Rust 替你挑了一个类型,别处一个小小的改动,就可能悄悄改变程序的行为。Rust 宁愿问你一次。

Rust 怎样自己推断类型(类型推断),在 E0282 页面里讲过了,这里不再重复。这些方法背后的 trait,参见 trait 和泛型。

E0282、E0283、E0284:同一个问题,三个代码

Section titled “E0282、E0283、E0284:同一个问题,三个代码”

三者说的都是 “type annotations needed”(需要类型注解),对你来说意思也一样:写出类型。代码只是告诉你,Rust 卡住的时候正在做什么:

  • E0282: Rust 对类型完全没有线索,或者它在拿到线索之前就需要知道类型(比如要查找 .len() 这样的方法)。
  • E0283(本页): Rust 有一条以 trait 规则形式出现的线索(must implement `Sum<&i32>`),但有很多类型都符合这条规则。
  • E0284: 规则涉及的是属于那个未知类型的另一个类型,比如 parse 的错误类型(cannot satisfy `<_ as FromStr>::Err == _`)。在知道主类型之前,Rust 没法检查它。

代码甚至会在你打字的时候变来变去。let words = text.split(' ').collect(); 后面跟着 println!("{:?}", words),报的是 E0283。把下一行改成 words.len(),就变成了 E0282,因为现在 Rust 需要知道类型才能找到方法。下面的修法对这三个错误都管用。

大多数时候都选这个方法。在变量名后面加上 : Type。写起来很短,而且能让下一个读代码的人一眼看出你在构建什么:

use std::collections::HashSet;
fn main() {
let prices = vec![250, 120, 99];
let total: u32 = prices.iter().sum();
let tags = "rust,web,rust,cli";
let list: Vec<&str> = tags.split(',').collect();
let unique: HashSet<&str> = tags.split(',').collect();
println!("总计:{total} 分");
println!("{:?}", list);
println!("不重复的标签:{} 个", unique.len());
}

输出:

总计:469 分
["rust", "web", "rust", "cli"]
不重复的标签:3 个
▶ 动手试一试

同样是 collect(),却得到两种不同的结果:你写下的类型就是给它的指令。里面的部分可以用 _ 交给 Rust 推断,比如 let list: Vec<_> = ...。

修复 2:在 sum 和 collect 上用 turbofish

Section titled “修复 2:在 sum 和 collect 上用 turbofish”

当没有变量可以加注解时,选这个方法:比如在 println! 里面、迭代器链的中间,或者作为函数参数。用 ::<Type>(也就是“turbofish”)把类型写在调用上:

fn main() {
let prices = vec![250, 120, 99];
println!("总计:{} 分", prices.iter().sum::<u32>());
let line = "tea,cake,soup";
let items = line.split(',').collect::<Vec<_>>();
let initials = line.split(',').map(|s| &s[..1]).collect::<String>();
println!("{:?}", items);
println!("首字母:{initials}");
}

输出:

总计:469 分
["tea", "cake", "soup"]
首字母:tcs
▶ 动手试一试

这里的单词保留了英文,因为 &s[..1] 按字节截取第一个字符,而一个汉字占 3 个字节。.product::<u64>() 的用法也一样。修复 1 和修复 2 同样正确,选读起来更顺的那个就好。

修复 3:对于 .into(),写明目标类型(turbofish 不管用)

Section titled “修复 3:对于 .into(),写明目标类型(turbofish 不管用)”

用 .into() 和 .try_into() 时,选这个方法。你可能很想写 "Ferris".into::<String>(),但那会得到另一个错误,因为类型参数属于 Into trait,而不属于这个方法:

error[E0107]: method takes 0 generic arguments but 1 generic argument was supplied
--> src/main.rs:2:25
|
2 | let name = "Ferris".into::<String>();
| ^^^^ expected 0 generic arguments

这条错误的意思是“这个方法不接受泛型参数,你却给了一个”。正确的做法是给变量加注解,或者直接在目标类型上调用 From,这样读起来也很自然:

fn main() {
let name: String = "Ferris".into();
let city = String::from("Lisbon");
let small: u8 = 200;
let big: u64 = small.into();
let also_big = u64::from(small);
let level: i64 = 42;
let byte: u8 = level.try_into().unwrap();
let too_big = u8::try_from(300_i64);
println!("{name} 住在 {city}");
println!("{big} {also_big} {byte}");
println!("300 能放进 u8 吗?{}", too_big.is_ok());
}

输出:

Ferris 住在 Lisbon
200 200 42
300 能放进 u8 吗?false
▶ 动手试一试

String::from(...) 和 u64::from(...) 在一行的开头就说明了目标类型,所以在没有其他线索时,很多人更喜欢用它们,而不是 .into()。参见类型转换。

修复 4:在类型固定的地方使用这个值

Section titled “修复 4:在类型固定的地方使用这个值”

当这个值本来就要传到某个类型已知的地方时(比如函数参数或结构体字段),选这个方法。Rust 会通读整个函数,所以后面的这次使用就足够了:

fn print_receipt(total: u32, items: Vec<&str>) {
println!("{} 件商品,{} 分", items.len(), total);
}
fn main() {
let prices = vec![250, 120, 99];
let total = prices.iter().sum(); // 是 u32,多亏了 print_receipt
let items = "tea,cake,soup".split(',').collect(); // 是 Vec<&str>,原因相同
print_receipt(total, items);
}

输出:

3 件商品,469 分
▶ 动手试一试

不是每种使用都算数。if prices.iter().sum() > 400 仍然是 E0283,因为 > 可以比较很多种类型的组合。把值传给像 impl Display 这样的泛型参数也没用,因为很多类型都是 Display 的。这次使用必须指明唯一一个确切的类型。

把 .collect() 直接放进 println!。 println!("{:?}", line.split(',').collect()); 会报 E0283,而 rustc 自己就会给出修法:.collect::<Vec<_>>()。

.try_into().unwrap()。 let small = big.try_into().unwrap(); 会报 E0283,并附上 the type must implement `TryFrom<i64>`。修复:let small: u8 = big.try_into().unwrap();,或者 u8::try_from(big).unwrap()。

把 Default::default() 传给泛型函数。 对于 fn show(value: impl Debug),调用 show(Default::default()) 会报 E0283:Rust 不知道你要的是谁的默认值。修复:show(Settings::default()) 或 show(u32::default())。如果单独写一行 let s = Default::default();,得到的则是 E0790;参见 Default。

你自己写的泛型函数。 如果类型参数只出现在返回类型里,比如 fn make<T: Default>() -> T,那么 let v = make(); 就是 E0283。修复:make::<Settings>(),或者 let v: Settings = make();。

  • E0282:需要类型注解,发生在 Rust 对类型完全没有线索(或者调用方法时需要知道类型)的时候。
  • E0284:需要类型注解,并附有 cannot satisfy `<_ as FromStr>::Err == _`,几乎总是来自 .parse()。
  • E0790:调用 Default::default()(或其他 trait 函数)时没有说明是哪个类型。
  • E0277:你写明了类型,但它没法用这种方式构建,比如 let n: Vec<i32> = "1 2".split(' ').collect();。
  • E0107:泛型参数的个数不对,.into::<String>() 给你的就是这个错误。
  • 所有编译错误:错误页面的完整列表。

小测验

1. E0283 的 note the type must implement Sum<&i32> 告诉了你什么?

2. 哪一行能修好 let name = "Ferris".into();?

3. 为什么把同样的单词收集进 Vec<&str> 和 HashSet<&str> 会得到不同的结果?

得分: 0 / 3