Prelude への追加
概要
TryInto
,TryFrom
,FromIterator
トレイトがプレリュードに追加されました。- これにより、トレイトメソッドへの呼び出しに曖昧性が発生して、コンパイルに失敗するようになるコードがあるかもしれません。
詳細
標準ライブラリの prelude モジュールには、
すべてのモジュールにインポートされるものが余すことなく定義されています。
そこには、Option
, Vec
, drop
, Clone
などの、頻繁に使われるアイテムが含まれます。
Rust コンパイラは、手動でインポートされたアイテムをプレリュードからのものより優先します。
これにより、プレリュードに追加があっても既存のコードは壊れないようになっています。
たとえば、 example
という名前のクレートまたはモジュールに pub struct Option;
が含まれていたら、
use example::*;
とすることで Option
は曖昧性なく example
に含まれるものを指し示し、
標準ライブラリのものは指しません。
ところが、トレイトをプレリュードに追加すると、捉えがたい形でコードが壊れることがあります。
たとえば、MyTryInto
トレイトで定義されている x.try_into()
という呼び出しは、
std
の TryInto
もインポートされているときは、動かなくなる場合があります。
なぜなら、try_into
の呼び出しは今や曖昧で、どちらのトレイトから来ているかわからないからです。
だからこそ我々は、 TryInto
を未だにプレリュードに追加していませんでした。
追加してしまうと、多くのコードでそのような問題が起こりうるからです。
解決策として、Rust 2021 では新たなプレリュードが使用されます。 変更点は、以下の3つが追加されたということだけです。
追跡用の Issue はこちらです。
移行
Rust 2018 コードベースから Rust 2021 への自動移行の支援のため、2021 エディションには、移行用のリントrust_2021_prelude_collisions
が追加されています。
rustfix
でコードを Rust 2021 エディションに適合させるためには、次のように実行します。
cargo fix --edition
このリントは、新しくプレリュードに追加されたトレイトで定義されているメソッドと同名の関数やメソッドが呼び出されていることを検知します。 場合によっては、今までと同じ関数が呼び出されるように、あなたのコードを様々な方法で書き換えることもあります。
コードの移行を手作業で行いたい方や rustfix
が何を行うかをより詳しく理解したい方のために、どのような状況で移行が必要なのか、逆にどうであれば不要なのを以下に例示していきます。
移行が必要な場合
トレイトメソッドの衝突
あるスコープに、同じメソッド名を持つ2つのトレイトがある場合、どちらのメソッドが使用されるべきかは曖昧です。例えば:
trait MyTrait<A> { // This name is the same as the `from_iter` method on the `FromIterator` trait from `std`. // この関数名は、`std` の `FromIterator` トレイトの `from_iter` メソッドと同名。 fn from_iter(x: Option<A>); } impl<T> MyTrait<()> for Vec<T> { fn from_iter(_: Option<()>) {} } fn main() { // Vec<T> implements both `std::iter::FromIterator` and `MyTrait` // If both traits are in scope (as would be the case in Rust 2021), // then it becomes ambiguous which `from_iter` method to call // Vec<T> は `std::iter::FromIterator` と `MyTrait` の両方を実装する // もし両方のトレイトがスコープに含まれる場合 (Rust 2021 ではそうであるが)、 // どちらの `from_iter` メソッドを呼び出せばいいかが曖昧になる <Vec<i32>>::from_iter(None); }
完全修飾構文を使うと、これを修正できます:
fn main() {
// Now it is clear which trait method we're referring to
// こうすれば、どちらのトレイトメソッドを指し示しているかが明確になる
<Vec<i32> as MyTrait<()>>::from_iter(None);
}
dyn Trait
オブジェクトの固有メソッド
dyn Trait
の値に対してメソッドを呼び出すときに、メソッド名が新しくプレリュードに追加されたトレイトと重複していることがあります:
#![allow(unused)] fn main() { mod submodule { pub trait MyTrait { // This has the same name as `TryInto::try_into` // これは `TryInto::try_into` と同名 fn try_into(&self) -> Result<u32, ()>; } } // `MyTrait` isn't in scope here and can only be referred to through the path `submodule::MyTrait` // `MyTrait` はここではスコープ内になく、パス付きで `submodule::MyTrait` としか利用できない fn bar(f: Box<dyn submodule::MyTrait>) { // If `std::convert::TryInto` is in scope (as would be the case in Rust 2021), // then it becomes ambiguous which `try_into` method to call // `std::convert::TryInto` がスコープ内にあるときは (Rust 2021 ではそうなのだが)、 // どちらの `try_into` メソッドを呼び出せばいいかが曖昧になる f.try_into(); } }
静的ディスパッチのときと違って、トレイトオブジェクトに対してトレイトメソッドを呼び出すときは、そのトレイトがスコープ内にある必要はありません。
TryInto
トレイトがスコープ内にあるときは (Rust 2021 ではそうなのですが)、曖昧性が発生します。
MyTrait::try_into
と std::convert::TryInto::try_into
のどちらが呼び出されるべきなのでしょうか?
この場合、さらなる参照外しをするか、もしくはメソッドレシーバーの型を明示することで修正できます。
これにより、dyn Trait
のメソッドとプレリュードのトレイトのメソッドのどちらが選ばれているかが明確になります。
たとえば、上の f.try_into()
を (&*f).try_into()
にすると、try_into
が dyn Trait
に対して呼び出されることがはっきりします。
これに該当するのはMyTrait::try_into
メソッドのみです。
移行が不要な場合
固有メソッド
トレイトメソッドと同名の固有メソッドを定義しているような型もたくさんあります。
たとえば、以下では MyStruct
が from_iter
を実装していますが、
これは標準ライブラリの FromIterator
トレイトのメソッドと同名です。
#![allow(unused)] fn main() { use std::iter::IntoIterator; struct MyStruct { data: Vec<u32> } impl MyStruct { // This has the same name as `std::iter::FromIterator::from_iter` // これは `std::iter::FromIterator::from_iter` と同名 fn from_iter(iter: impl IntoIterator<Item = u32>) -> Self { Self { data: iter.into_iter().collect() } } } impl std::iter::FromIterator<u32> for MyStruct { fn from_iter<I: IntoIterator<Item = u32>>(iter: I) -> Self { Self { data: iter.into_iter().collect() } } } }
固有メソッドは常にトレイトメソッドより優先されるため、移行作業の必要はありません。
実装の参考事項
2021 エディションを導入することで名前解決に衝突が生じるかどうか(すなわち、エディションを変えることでコードが壊れるかどうか)を判断するために、このリントはいくつかの要素を考慮する必要があります。たとえば以下のような点です:
- 完全修飾呼び出しとドット呼び出しメソッド構文のどちらが使われているか?
- これは、メソッド呼び出し構文の自動参照付けと自動参照外しによる名前の解決方法に影響します。ドット呼び出しメソッド構文では、手動で参照外し/参照付けすることで優先順位を決められますが、完全修飾呼び出しではメソッドパス中に型とトレイト名が指定されていなければなりません (例:
<Type as Trait>::method
)
- これは、メソッド呼び出し構文の自動参照付けと自動参照外しによる名前の解決方法に影響します。ドット呼び出しメソッド構文では、手動で参照外し/参照付けすることで優先順位を決められますが、完全修飾呼び出しではメソッドパス中に型とトレイト名が指定されていなければなりません (例:
- 固有メソッドとトレイトメソッドのどちらが呼び出されているか?
- 固有メソッドはトレイトメソッドより優先されるので、
self
を取るトレイトメソッドは、TryInto::try_into
より優先されますが、&self
や&mut self
をとる固有メソッドは、自動参照付けが必要なので優先されません(もっとも、TryInto
はself
を取るので、それは当てはまりませんが)
- 固有メソッドはトレイトメソッドより優先されるので、
- そのメソッドは
core
かstd
から来たものか? (トレイトは自分自身とは衝突しないので) - その型は、名前が衝突するようなトレイトを実装しているか?
- メソッドが動的ディスパッチによって呼び出されているか? (つまり、
self
の型がdyn Trait
か?)- その場合、トレイトのインポートは名前解決に影響しないので、移行リントを出す必要はありません