📚
[Rust] なぜ Iterator<char> の .collect() で String に変換できるのか?
疑問
- なぜ
Iterator
の.collect()
利用時に型を明記する必要があるのか? - なぜ
Iterator<char>
の.collect()
でString
に変換できるのか?
// 1. なぜ Iterator の .collect() 利用時に型を明記する必要があるのか?
fn main() {
let v = vec![1, 2, 3];
// これはコンパイルエラー
// let s = v.into_iter().collect();
let s: Vec<i32> = v.into_iter().collect();
println!("{:?}", s);
}
// 2. なぜ Iterator<char> の .collect() で String に変換できるのか?
fn main() {
let v = vec!['a', 'b', 'c'];
// .collect() で Iterator<char> を String に変換できる
let s: String = v.into_iter().collect();
println!("{:?}", s);
}
まとめ
-
.collect()
は、FromIterator
を実装する型を返すが、FromIterator
を実装するものが複数あるので、型推論できません。そのため、型を明示的に指定する必要があります。 -
String
はFromIterator<char>
を実装しています。そのため、.collect()
でString
変換できます。
型推論できないのがコンパイルエラーの原因
コンパイルエラーの内容を確認します。Rust はこの手のエラーメッセージが非常に親切なので助かります。 error[E0282]: type annotations needed
というエラーでした。
error[E0282]: type annotations needed
--> src/main.rs:5:9
|
5 | let s = v.into_iter().collect();
| ^
|
help: consider giving `s` an explicit type
|
5 | let s: Vec<_> = v.into_iter().collect();
| ++++++++
For more information about this error, try `rustc --explain E0282`.
error: could not compile `playground` (bin "playground") due to previous error
rustc --explain E0282
を実行するとより詳細な説明も得られます。
The compiler could not infer a type and asked for a type annotation.
...
通常、Rust は型を省略して書いても型推論により型を自動的に推論して決めてくれます。しかし、型推論時に一つに型を特定できない場合やそもそも特定できない場合、このエラーが出ます。
型推論の(複数の)候補は一体何なのか?
.collect()
で返される型が複数ありうるため型推論が失敗しているとすると、一体それは何があるのだろうか。.collect()
はどんな型を返すのかを確認してみることでわかるようになります。
.collect()
の定義は、以下のようになっている。ジェネリクス関数であって、FromIterator<Self::Item>
を実装している型 B
を返すことになっています。
fn collect<B: FromIterator<Self::Item>>(self) -> B
where
Self: Sized,
{
FromIterator::from_iter(self)
}
つまり、FromIterator
を実装している型が複数あるということになります。実際、標準ライブラリのコードを見てみると、以下のようになっています。
Vec<T>
の例
impl<T> FromIterator<T> for Vec<T> {
#[inline]
fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Vec<T> {
<Self as SpecFromIter<T, I::IntoIter>>::from_iter(iter.into_iter())
}
}
HashSet<T>
の例
impl<T, S> FromIterator<T> for HashSet<T, S>
where
T: Eq + Hash,
S: BuildHasher + Default,
{
#[inline]
fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> HashSet<T, S> {
let mut set = HashSet::with_hasher(Default::default());
set.extend(iter);
set
}
}
これ以外にもたくさんのコレクションの型が FromIterator
を実装していました。これでは型推論で型は一つに定まることはまずあり得ないです。二つめの疑問も String
の実装を見ることで納得できました。
String
の例
impl FromIterator<char> for String {
fn from_iter<I: IntoIterator<Item = char>>(iter: I) -> String {
let mut buf = String::new();
buf.extend(iter);
buf
}
}
つまり、Iterator<char>
を使って .collect()
する際には Vec<char>
をはじめとする各種コレクションの型もありえるし、String
にもなり得るということです。
あとがき
まとめはこちらです。以下は感想です。
- この
FromIterator
を適切に実装するカスタムクラスを作ることもできそうです。 - 思い返せば Java だと
Stream.collect
は引数で.collect(HashSet::new)
のような形でHashSet
に変換していましたが、Rust では trait を実装して型推論させることで、型の方を指定すれば良いようになっています。これは単に作りの問題なのか型推論の精度の差などに起因するのか興味が出てきました。この点はまた調べてみたいです。
Discussion