Prelude への追加

概要

  • TryInto, TryFrom, FromIterator トレイトがプレリュードに追加されました。
  • これにより、トレイトメソッドへの呼び出しに曖昧性が発生して、コンパイルに失敗するようになるコードがあるかもしれません。

詳細

標準ライブラリの prelude モジュールには、 すべてのモジュールにインポートされるものが余すことなく定義されています。 そこには、Option, Vec, drop, Clone などの、頻繁に使われるアイテムが含まれます。

Rust コンパイラは、手動でインポートされたアイテムをプレリュードからのものより優先します。 これにより、プレリュードに追加があっても既存のコードは壊れないようになっています。 たとえば、 example という名前のクレートまたはモジュールに pub struct Option; が含まれていたら、 use example::*; とすることで Option は曖昧性なく example に含まれるものを指し示し、 標準ライブラリのものは指しません。

ところが、トレイトをプレリュードに追加すると、捉えがたい形でコードが壊れることがあります。 たとえば、MyTryInto トレイトで定義されている x.try_into() という呼び出しは、 stdTryInto もインポートされているときは、動かなくなる場合があります。 なぜなら、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_intostd::convert::TryInto::try_into のどちらが呼び出されるべきなのでしょうか?

この場合、さらなる参照外しをするか、もしくはメソッドレシーバーの型を明示することで修正できます。 これにより、dyn Trait のメソッドとプレリュードのトレイトのメソッドのどちらが選ばれているかが明確になります。 たとえば、上の f.try_into()(&*f).try_into() にすると、try_intodyn Trait に対して呼び出されることがはっきりします。 これに該当するのはMyTrait::try_intoメソッドのみです。

移行が不要な場合

固有メソッド

トレイトメソッドと同名の固有メソッドを定義しているような型もたくさんあります。 たとえば、以下では MyStructfrom_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 をとる固有メソッドは、自動参照付けが必要なので優先されません(もっとも、TryIntoself を取るので、それは当てはまりませんが)
  • そのメソッドは corestd から来たものか? (トレイトは自分自身とは衝突しないので)
  • その型は、名前が衝突するようなトレイトを実装しているか?
  • メソッドが動的ディスパッチによって呼び出されているか? (つまり、 self の型が dyn Trait か?)
    • その場合、トレイトのインポートは名前解決に影響しないので、移行リントを出す必要はありません
関連キーワード:  メソッド, iter, 追加, try, into, from, MyTrait, エディション, TryInto, 移行