序文
Rust エディションガイドへようこそ! 「エディション」とは、Rust に後方互換性が保てなくなるようなアップデートを行うための方法です。
このガイドでは、下記の項目について説明します:
- エディションとは何か
- 各エディションの変更内容
- コードをあるエディションから別のエディションへ移行する方法
エディションとは?
Rust 1.0 のリリースでは、Rust のコア機能として「よどみない安定性」が提供されるようになりました。 Rust は、1.0 のリリース以来、いちど安定版にリリースされた機能は、将来の全リリースに渡ってサポートし続ける、というルールの下で開発されてきました。
一方で、後方互換でないような小さい変更を言語に加えることも、ときには便利です。
最もわかりやすいのは新しいキーワードの導入で、これは同名の変数を使えなくします。
例えば、Rust の最初のバージョンには async
や await
といったキーワードはありませんでした。
後のバージョンになってこれらを突然キーワードに変えてしまうと、例えば let async = 1;
のようなコードが壊れてしまいます。
このような問題を解決するために、エディションという仕組みが使われています。
後方互換性を失わせるような機能をリリースしたいとき、我々はこれを新しいエディションの一部として提供します。
エディションはオプトイン、すなわち導入したい人だけが導入できるので、既存のクレートは明示的に新しいエディションに移行しない限りは変化を受けません。
すなわち、2018 以降のエディションを選択しない限り、たとえ最新バージョンの Rust であっても async
はキーワードとして扱われません。
導入の可否はクレートごとに決めることができ、Cargo.toml
への記載内容により決定されます。
cargo new
コマンドで作成される新しいクレートは、常に最新の安定版のエディションでセットアップされます。
エディションはエコシステムを分断しない
エディションで最も重要な規則は、あるエディションのクレートと別のエディションでコンパイルされたクレートがシームレスに相互運用できるようになっているということです。 これにより、新しいエディションへ移行するかどうかは、他のクレートに影響を与えない「自分だけの問題」だと言えるのです。
クレートの相互運用性を守るために、我々がエディションに加えられる変更にはある種の制限がかかります。 一般に、エディションに加えられる変更は「表面上の」ものになりがちです。 エディションに関わらず、すべての Rust のコードは最終的にはコンパイラの中で同じ内部表現に変換されるのです。
エディションの移行は簡単で、ほとんど自動化されている
我々は、クレートを新しいエディションにアップグレードするのが簡単になるよう目指しています。
新しいエディションがリリースされるとき、我々は移行を自動化するツールも提供します。
このツールは、新しいエディションに適合させるために必要な小さな変更をコードに施します。
例えば、Rust 2018 への移行の際は、async
と名のつく全てのものを、等価な生識別子構文である r#async
へと書き換える、といった具合です。
この自動移行は必ずしも完璧とは限らず、手作業での変更が必要なコーナーケースもないとは言えません。 このツールは、コードの正しさやパフォーマンスに影響を与えうるような、プログラムの意味に関わる変更は避けるために全力を尽くします。
我々はこのツールの他に、エディションを構成する変更を取り扱っている、本エディション移行ガイドも管理しています。 このガイドでは、それぞれの変更内容と、もっと詳しく知りたい人向けのリンク、さらには知っておくべき詳細や重箱の隅まで網羅しています。 このガイドはエディションの概要を示すと同時に、自動化ツールに何らかの問題が生じたときのトラブルシューティング用の文献にもなります。
新しいプロジェクトを作成する
Cargoは新たなプロジェクトを作成する際に自動で最新のエディションをコンフィギュレーションに追加します。
> cargo +nightly new foo
Created binary (application) `foo` project
> cat foo/Cargo.toml
[package]
name = "foo"
version = "0.1.0"
authors = ["your name <you@example.com>"]
edition = "2021"
[dependencies]
この edition = "2021"
によってあなたのパッケージが Rust 2021 を利用するように設定されます。
これ以外は必要ありません。
もし、他の古いエディションを使いたい場合は、その設定の値を変更できます。例えば、
[package]
name = "foo"
version = "0.1.0"
authors = ["your name <you@example.com>"]
edition = "2015"
[dependencies]
とすると、あなたのパッケージは Rust 2015 でビルドされます。
既存のプロジェクトのエディションを移行する
Rust には、プロジェクトのエディションを進めるための自動移行ツールが付属しています。 このツールは、あなたのソースコードを書き換えて次のエディションに適合させます。 簡単にいうと、新しいエディションに進むためには次のようにすればよいです。
cargo fix --edition
を実行するCargo.toml
のedition
フィールドを新しいエディションに設定する。たとえば、edition = "2021"
とするcargo build
やcargo test
を実行して、修正がうまくいったことを検証する。
以下のセクションで、これらの手順の詳細と、その途中で起こりうる問題点について詳しく説明します。
我々は、新しいエディションへの移行をできるだけスムーズに行えるようにしたいと考えています。 もし、最新のエディションにアップグレードするのが大変な場合は、我々はそれをバグとみなします。 もし移行時に問題があった場合にはバグ登録してください。 よろしくお願いします!
訳注:Rustの日本語コミュニティもあります。 Slackを使用しておりこちらから登録できます。
移行の開始
例えば、2015エディションから2018エディションに移行する場合を見てみましょう。 ここで説明する手順は、例えば2021エディションのように、別のエディションに移行する場合も実質的に同様です。
src/lib.rs
に以下のコードがあるクレートがあるとします。
#![allow(unused)] fn main() { trait Foo { fn foo(&self, i32); } }
このコードは i32
という無名パラメータを使用しています。
これは Rust 2018ではサポートされておらず、コンパイルに失敗します。
このコードを更新してみましょう。
あなたのコードを新しいエディションでコンパイルできるようにする
あなたのコードは新しいエディションに互換性のない機能を使っているかもしれないし、使っていないかもしれません。
Cargo には cargo fix
というサブコマンドがあり、これがあなたのコードを自動的に更新して次のエディションへの移行を補助してくれます。
まず初めに、これを実行してみましょう。
cargo fix --edition
これはあなたのコードをチェックして、自動的に移行の問題を修正してくれます。
もう一度 src/lib.rs
を見てみましょう。
#![allow(unused)] fn main() { trait Foo { fn foo(&self, _: i32); } }
i32
値をとるパラメータに名前が追加された形でコードが書き換えられています。
この場合は、パラメータ名がなかったので、使用されていないパラメータの慣習に従って _
を付加しています。
cargo fix
は常に自動的にコードを修正してくれるわけではありません。
もし、cargo fix
がコードを修正できない時にはコンソールに修正できなかったという警告を表示します。
その場合は手動でコードを修正してください。
「発展的な移行戦略」の章では、移行に関するより多くの情報があります。また、このガイドの他の章では、どのような変更が必要かについても説明しますので、併せてご参照ください。
問題が発生したときは、ユーザーフォーラム で助けを求めてください。
新機能を使うために新たなエディションを有効化する
新しいエディションの新機能を使うには明示的にオプトインする必要があります。
準備がよければ、Cargo.toml
に新しい edition
のキーバリューペアを追加してください。
例えば以下のような形になります。
[package]
name = "foo"
version = "0.1.0"
edition = "2018"
もし edition
キーがなければCargoはデフォルトで Rust 2015をエディションとして使います。
しかし、上記の例では 2018
を明示的に指定しているので、コードは Rust 2018 でコンパイルされます!
次に、新しいエディション上であなたのプロジェクトをテストしましょう。
cargo test
を実行するなどして、プロジェクトのテストを走らせ、すべてが元のまま動くことを確認してください。
新たに警告が出た場合、(--edition
なしの) cargo fix
をもう一度実行することで、コンパイラからの提案を受け入れてみるのも良いかもしれません。
わーい。今やあなたのコードは Rust 2015 と Rust 2018 の両方で有効です!
発展的な移行戦略
移行ツールの仕組み
cargo fix --edition
コマンドは、cargo check
コマンドと同様のコマンドを、次のエディションでコンパイルされなくなるコードを検知する特別なリントが有効になった状態で実行することで機能します。
このリントには、コードを変更したら現在と次のエディションの双方に適合させるための指示も含まれています。
cargo fix
コマンドはソースコードをそれに従って変更し、再び cargo check
を実行して修正がうまく行ったか確認します。
うまく行かなかった場合、変更を巻き戻して警告を表示します。
現在と次のエディションの両方に同時に適合したコードに書き換えると、コードを段階的に移行することが楽になります。
自動移行が完全には成功しなかったか、手作業で変えてやる必要がある場合は、元ののエディション留まったまま同じことを繰り返してから、Cargo.toml
を編集して次のエディションに進めてもよいです。
cargo fix --edition
が適用するリントは、リントグループの一部です。
例えば、2018 から 2021 に移行する場合、Cargo は rust-2021-compatibility
というリントグループをコードの修正に使用します。
それぞれのリントを移行に役立てるコツについては、後の「部分的な移行」の章をご覧ください。
cargo fix
は、cargo check
を複数回実行する可能性があります。
たとえば、一通りの修正を終えても、修正によって新たな警告が出て、さらなる修正が必要になるかもしれません。
Cargo は、新しく警告が出なくなるまでこれを繰り返し続けます。
複数の設定を移行する
cargo fix
は一度に1つのコンフィギュレーションでしか動きません。
Cargo のフィーチャや条件付きコンパイルを使用している場合、cargo fix
を異なるフラグで複数回実行する必要があるかもしれません。
例えば、あなたのコードが #[cfg]
を使ってプラットフォームによって違うコードを含むようになっていた場合、cargo fix
を --target
オプションつきで実行して、ターゲットごとに修正をする必要があるかもしれません。
クロスコンパイルができない場合、コードを別のマシンに移して作業せざるを得ないかもしれません。
同様に、フィーチャによる条件分岐、例えば #[cfg(feature = "my-optional-thing")]
のようなものがある場合、--all-features
フラグを使って、フィーチャの壁を超えて cargo fix
がすべてのコードを変更できるようにするとよいでしょう。
フィーチャごとに別々にコードを移行したい場合は、--features
フラグを使って一つずつ移行作業をすることもできます。
巨大なプロジェクトやワークスペースの移行
大きなプロジェクトで問題が発生した場合、作業を単純にするために、段階的に移行していくこともできます。
Cargo のワークスペースでは、エディションはパッケージごとに定義されているため、自然とパッケージごとに1つずつ移行をすることになります。
Cargo のパッケージにおいては、全パッケージを同時に移行することも、Cargo ターゲットごとに1つずつ移行することもできます。
例えば、パッケージに複数のバイナリ、テスト、サンプルコード(example
ターゲット)がある場合、cargo fix --edition
コマンドに特定のターゲット選択用のフラグを組み合わせることで、一つのターゲットだけを移行することもできます。
デフォルトでは、cargo fix
は --all-targets
が暗黙に指定されていると扱います。
より発展的には、Cargo.toml
中に各ターゲットで使用するエディションを指定することもできます:
[[bin]]
name = "my-binary"
edition = "2018"
おそらく普通はこれは必要ありませんが、ターゲットがたくさんあって全部いっぺんに移行作業できないような場合には一つの選択肢になるでしょう。
壊れたコードを元に部分的に移行する
ときどき、コンパイラに提案された修正ではうまくいかないことがあります。 すると、Cargo は何が起こったかとどんなエラーが出たかを示す警告を報告しますが、デフォルトでは Cargo は変更を巻き戻します。 しかし、Cargo にはコードを壊れたままにしておいてもらい、手作業で問題を解決する、というのも有効な手段です。 ほとんどの修正は正しいかもしれませんし、壊れた修正もだいたい正しくて、少しいじれば問題ないかもしれないのです。
そんなときは、cargo fix
コマンドを実行するときに --broken-code
オプションをつけて、Cargo が変更を巻き戻さないようにできます。
そうすれば、エラーを実際に見てみることも、修正点を確認することもできます。
プロジェクトを段階的に移行するもう一つの方法は、それぞれの修正を一つずつ適用することです。
このためには、各リントを警告として追加して、(--edition
なしの)cargo fix
を実行するか、お手持ちのエディタや IDE が「クイックフィックス」をサポートしていればそれを使って提案を適用すればよいです。
例えば、2018 エディションには keyword-idents
という、キーワードとの衝突をすべて修正するためのリントがあります。
各クレートのトップ(例えば src/lib.rs
や src/main.rs
の先頭)に #![warn(keyword_idents)]
を追加して、cargo fix
を実行すれば、そのリントによる提案だけを受け入れることができます。
各エディションで有効化されるリントの一覧は、リントグループのページを見るか、 rustc -Whelp
コマンドを実行すれば確認できます。
マクロの移行
マクロの中には、エディションを進めるにあたって手作業が必要なものがあります。
例えば、cargo fix --edition
は、次のエディションで動作しない文法を生成するマクロを自動修正することは難しいかもしれません。
これは、手続き型マクロと macro_rules
を使ったマクロの双方で問題になります。
macro_rules
を使ったマクロは、マクロが同じクレートに属していたら自動でアップデートできる場合もありますが、いくつかの状況ではできません。
手続き型マクロは原則、全く修正できないと言っていいでしょう。
例えば、この(わざとらしい)マクロ foo
を含むクレートを 2015 から 2018 に移行しようとしても、foo
は自動修復されません。
#![allow(unused)] fn main() { #[macro_export] macro_rules! foo { () => { let dyn = 1; println!("it is {}", dyn); // "これは {} です" }; } }
マクロが 2015 のクレートで定義されている場合、マクロの衛生性(後述)のおかげでこれは他のエディションのクレートからも使用することができます。
2015 では、dyn
は通常の識別子で、制限なく使用できます。
一方で、2018 では、dyn
はもはや正当な識別子ではありません。
cargo fix --edition
で 2018 へ移行するとき、Cargo は警告やエラーを一切表示しません。
しかし、foo
はどのクレートで呼び出されても動作しません。
あなたのコードにマクロがある場合、そのマクロの構文を十分に網羅するテストがあることが推奨されます。 また、そのマクロを複数のエディションのクレートの中でインポートして使用し、どこでも動くということを確認するのも良いでしょう。 問題に突き当たった場合、このガイドの本章に目を通して、全エディションで動作するようにするためにどうすればいいか理解する必要があります。
マクロの衛生性
マクロには、「エディション衛生性」と呼ばれる仕組みが使われています。このシステムでは、マクロ内のトークンがどのエディションから来たかが記録されています。 これにより、呼び出す側のエディションがどれであるかを気にせずに、外部のマクロを呼び出すことができます。
上で例に出した、macro_rules
で定義されたマクロを詳しく見てみましょう。
このマクロは dyn
を識別子に使用しています。
このマクロが定義されているのが 2015 エディションのクレート内なのならば、これは問題なく動きます。
さらに、2018 エディションでは dyn
はキーワードで普通は識別子に使えませんが、2018 エディションのクレートからこのマクロを呼び出したとしても大丈夫です。
パーサーはトークンのエディションに着目して、どう解釈したらよいかを判断します。
問題が起こるのは、マクロが定義されている側のクレートのエディションを 2018 に変更したときです。
今や、これらのトークンは 2018 エディションのものとしてタグ付けされているために、パースに失敗します。
しかしながら、このマクロは自身のクレートから呼び出されてもいないために、cargo fix --edition
はこれを検査することも修正することもできません。
ドキュメンテーションテスト
現在のところ、cargo fix
はドキュメンテーションテストを更新することはできません。
Cargo.toml
でエディションを更新したら、cargo test
を実行して全てのテストに通過することを確認すべきです。
新しいエディションでサポートされない構文がドキュメンテーションテストに使われていた場合は、手作業で修正する必要があります。
まれなケースですが、エディションをテストごとに設定することもできます。
例えば、バッククォート3つの後に edition2018
アノテーションをつければ、rustdoc
が使うエディションを指定できます。
生成されるコード
自動移行が使えない場所がもう一つあります。それは、ビルドスクリプトが Rust コードをコンパイル時に生成する場合です(具体例は「コード生成」をご参照ください)。 このとき、出てくるコードが次のエディションで機能しない場合は、生成されるコードが新しいエディションに適合するように、ビルドスクリプトを自分で書き換える必要があります。
Cargo でないプロジェクトの移行
プロジェクトのビルドシステムが Cargo 以外の場合でも、次のエディションへの移行に自動リントが利用できるかもしれません。
適切なリントグループを使って、前述の移行リントを有効化できます。
例えば、#![warn(rust_2021_compatibility)]
というアトリビュートを使ったり、-Wrust-2021-compatibility
や --force-warns=rust-2021-compatibility
などの CLI フラグを使用するとよいです。
次に、これらのリントをコードに適用します。 これにはいくつかの手があります:
- 警告を読んで、コンパイラに提案された変更を自分で加える。
- エディタや IDE の機能で、コンパイラからの提案を適用する。 例えば、Visual Studio Code で Rust Analyzer 拡張 を使えば、自動的に提案を受け入れるための「クイックフィックス」が使えます。 他にも多くのエディタで同様の機能が使えます。
rustfix
ライブラリを用いて、移行ツールを自作する。 このライブラリは Cargo 内部でも使われており、コンパイラからの JSON メッセージを元にソースコードを編集します。 ライブラリの使用例は、examples
ディレクトリをご覧ください。
新しいエディションで慣用的な書き方をする
エディションは、新機能の追加や古い機能の削除のためだけのものではありません。 どんなプログラミング言語でも、その言語らしい書き方は時代によって変化します。Rust も同じです。 古いプログラムはコンパイルには通るかもしれませんが、今は別の書き方があるかもしれません。
例えば、Rust 2015 では、外部クレートは以下のように extern crate
で明示的に宣言される必要がありました:
// src/lib.rs
extern crate rand;
Rust 2018 では、外部クレートを使うのにextern crate
は必要ありません。
cargo fix
には --edition-idioms
オプションがあり、古い書き方の一部を新しい書き方に書き換えることができます。
警告: 現行の「イディオムリント」にはいくつか問題があることが知られています。 これらのリントはときどき、受け入れるとコンパイルできなくなるような誤った提案をすることがあります。 現在、以下のリントがあります。
- Edition 2018:
- Edition 2021 にイディオムリントはありません。
以下の手順は、コンパイラや Cargo の多少のバグを厭わない恐れ知らずだけにしかお勧めできません! 不都合が発生する場合は、前述の
--broken-code
オプションを使ってツールにやれるだけのことをさせ、残った問題を自分の手で解決してもよいでしょう。
ともあれ、先程の短いコードを Cargo に直してもらうにはこうすればよいです:
cargo fix --edition-idioms
すると、src/lib.rs
に書かれた extern crate rand;
が削除されます。
これで、自分でコードに手を下すことなく、コードを現代風にできました!
Rust 2015
Rust 2015は「安定性」というテーマを掲げています。 このエディションはRust 1.0のリリースから始まり、デフォルトのエディションとなっています。 エディションの仕組み自体は2017年末に考案されましたが、Rust 1.0は2015年5月にリリースされていて、「2015」が特定のエディションを指定しなかった時のデフォルトになります。
「安定性」がRust 2015エディションのテーマです。 なぜなら、Rust 1.0はRust開発に著しい変化をもたらしたからです。 Rust 1.0以前は、Rustは毎日のように変わっていました。 そのような言語は大規模なソフトウエア開発には使えないですし、学ぶことも難しいでしょう。 Rust 1.0とRust 2015エディションの登場とともに、我々は後方互換性にコミットし、Rust上で開発を行う人々のための強固な基盤を提供しています。
Rust 2015はデフォルトのエディションなのであなたのコードをRust 2015へポーティングするということはありません。 どんなRustのコードもRust 2015です。 あなたは Rust 2015から離れることはあっても、近づいていくということはありません。 ということで、これ以上あまり言うことはないでしょう!
Rust 2018
情報 | |
---|---|
RFC | #2052 (このRFCはエディションシステムそのものも提案している) |
リリースバージョン | 1.31.0 |
Rust 2018 のリリースのために、エディションシステムが作られました。 Rust 2018 のリリースは、生産性をテーマに掲げた数々の新機能とともにもたらされました。 ほとんどの新機能には後方互換性があり、すべてのエディションで使用可能となりました。 一方、一部の変更にはエディション機構が必要となりました(代表例はモジュールシステムの変更です)。
パスとモジュールシステムへの変更
概要
use
宣言中のパスが、他のパスと同じように扱われるようになりました。::
から始まるパスの直後には、常に外部クレートが続くようになりました。pub(in path)
のような可視性修飾子1において、パスはcrate
,self
,super
のいずれかで始まらなくてはならなくなりました。
1
pub(in path)
構文は、アイテムを path
に指定したモジュール内だけに公開にするための可視性修飾子です。
例えば、pub(in crate::outer_mod)
のように書くと crate::outer_mod
モジュール内だけからアクセスできるようになります。("public in crate::outer_mod
" という意味です。)
詳細は The Rust Reference (英語) の説明もご参照ください。
動機
Rust を習って間もない人にとって、モジュールシステムは最も難しいものであることが多いです。 もちろん、習得に時間がかかるものは誰にでもあります。 しかし、モジュールシステムが多くの人を混乱させる根本的原因は、モジュールシステムの定義そのものは単純で首尾一貫しているにも関わらず、その帰結が一貫性のない、非直感的な、不思議なものに感じられてしまうことにあります。
ですので、Rust の 2018 エディションにおけるモジュールシステムの新機能は、モジュールシステムを単純化し、仕組みをよりわかりやくするために導入されました。
簡単にまとめると、
extern crate
は九割九分の場面で必要なくなりました。crate
キーワードは、自身のクレートを指します。- サブモジュール内であっても、パスをクレート名から始めることができます。
::
から始まるパスは、常に外部クレートを指します。foo.rs
とfoo/
サブディレクトリは共存できます。サブディレクトリにサブモジュールを置く場合でも、mod.rs
は必要なくなりました。use
宣言におけるパスも、他の場所のパスと同じように書けます。
こうして並べられると、一見これらの新しい規則はてんでバラバラですが、これにより、総じて今までよりはるかに「こうすればこう動くだろう」という直感が通じるようになりました。 詳しく説明していきましょう!
詳しく説明
新機能を一つずつ取り上げていきます。
さようなら、extern crate
これは非常に単純な話です。
プロジェクトにクレートをインポートするために extern crate
を書く必要はもはやありません。
今まで:
// Rust 2015
extern crate futures;
mod submodule {
use futures::Future;
}
これから:
// Rust 2018
mod submodule {
use futures::Future;
}
今や、プロジェクトに新しくクレートを追加したかったら、Cargo.toml
に追記して、それで終わりです。
Cargo を使用していない場合は、rustc
に外部クレートの場所を --extern
フラグを渡しているでしょうが、これを変える必要はありません。
一つ注意ですが、
cargo fix
は今の所この変更を自動では行いません。 将来、自動で変更がなされるようになるかもしれません。
例外
このルールには一つだけ例外があります。それは、"sysroot" のクレートです。 これらのクレートは、Rust に同梱されています。
通常は、これらが必要なのは非常に特殊な状況です。
1.41 以降、rustc
は --extern=CRATE_NAME
フラグを受け付けるようになりました。
このフラグを指定すると、extern crate
で指定するのと同じように、与えられたクレート名が自動的に追加されます。
ビルドツールは、このフラグを用いてクレートのプレリュードに sysroot のクレートを注入できます。
Cargo にはこれを表現する汎用的な方法はありませんが、proc_macro
のクレートではこれが使用されます。
例えば、以下のような場合には明示的に sysroot のクレートをインポートする必要があります:
std
: 通常は不要です。クレートに#![no_std]
が指定されていない限り、std
は自動的にインポートされるからです。core
: 通常は不要です。クレートに#![no_core]
が指定されていない限り、core
は自動的にインポートされるからです。 例えば、標準ライブラリに使用されている一部の内部クレートではこれが必要です。proc_macro
: 1.42 以降、proc-macro クレートではこれは自動的にインポートされます。 それより古いリリースをサポートしたい場合か、Cargo 以外のビルドツールを使っていてそれがrustc
に適切な--extern
フラグを渡さない場合は、extern crate proc_macro;
と書く必要があります。alloc
:alloc
クレート内のアイテムは、通常はstd
を通して公開されたものが使用されます。 アロケーションをサポートするno_std
なクレートにおいては、明示的にalloc
をインポートすることが必要になります。test
: これは nightly チャンネルでのみ使用可能で、まだ安定化されていないベンチマークのために主に使われます。
マクロ
extern crate
のもう一つの使い道は、マクロのインポートでしたが、これも必要なくなりました。
マクロは、他のアイテムと同様、use
でインポートできます。
たとえば、以下の extern crate
は:
#[macro_use]
extern crate bar;
fn main() {
baz!();
}
こんな感じに変えることができます:
use bar::baz;
fn main() {
baz!();
}
クレートの名前変更
今までは as
を使ってクレートを違う名前でインポートしていたとします:
extern crate futures as f;
use f::Future;
その場合、 extern crate
の行を消すだけではうまくいきません。こう書く必要があります:
use futures as f;
use self::f::Future;
f
を使っているすべてのモジュールに対して、同様の変更が必要です。
crate
キーワードは自身のクレートを指す
use
宣言や他のコードでは、crate::
プレフィクスを使って自身のクレートのルートを指すことができます。
たとえば、crate::foo::bar
と書けば、それがどこに書かれていようと、foo
モジュール内の bar
という名前を指します。
::
というプレフィクスは、かつてはクレートのルートまたは外部クレートのいずれかを指していましたが、今は常に外部クレートを指します。
例えば、::foo::bar
と書くと、これは常に foo
というクレートの bar
という名前を指します。
外部クレートのパス
かつては、use
によるインポートなしで外部クレートを使用するには、::
から始まるパスを書かなくてはなりませんでした。
// Rust 2015
extern crate chrono;
fn foo() {
// this works in the crate root
// クレートのルートでは、このように書ける
let x = chrono::Utc::now();
}
mod submodule {
fn function() {
// but in a submodule it requires a leading :: if not imported with `use`
// 一方、サブモジュールでは `use` でインポートしない限り :: から始めないといけない
let x = ::chrono::Utc::now();
}
}
今は、外部クレートの名前はサブモジュールを含むクレート全体でスコープに含まれます。
// Rust 2018
fn foo() {
// this works in the crate root
// クレートのルートでは、このように書ける
let x = chrono::Utc::now();
}
mod submodule {
fn function() {
// crates may be referenced directly, even in submodules
// サブモジュール内でも、クレートを直接参照できる
let x = chrono::Utc::now();
}
}
さようなら、mod.rs
Rust 2015 では、サブモジュールは:
// This `mod` declaration looks for the `foo` module in
// `foo.rs` or `foo/mod.rs`.
// この `mod` 宣言は、`foo` モジュールを `foo.rs` または `foo/mod.rs` のいずれかに探す
mod foo;
foo.rs
か foo/mod.rs
のどちらにも書けました。
もしサブモジュールがある場合、必ず foo/mod.rs
に書かなくてはなりませんでした。
したがって、foo
のサブモジュール bar
は、foo/bar.rs
に書かれることになりました。
Rust 2018 では、サブモジュールのあるモジュールは mod.rs
に書かなくてはならないという制限はなくなりました。
foo.rs
は foo.rs
のままで、サブモジュールは foo/bar.rs
のままでよくなりました。
これにより、特殊な名前がなくなり、エディタ上でたくさんのファイルを開いても、mod.rs
だらけのタブでなく、ちゃんとそれぞれの名前を確認することができるでしょう。
Rust 2015 | Rust 2018 |
---|---|
. ├── lib.rs └── foo/ ├── mod.rs └── bar.rs |
. ├── lib.rs ├── foo.rs └── foo/ └── bar.rs |
use
におけるパス
Rust 2018 では、Rust 2015 に比べてパスの扱いが単純化・統一されています。
Rust 2015 では、use
宣言におけるパスは他の場所と異なった挙動を示しました。
特に、use
宣言におけるパスは常にクレートのルートを基準にしたのに対し、プログラム中の他の場所でのパスは暗黙に現在のスコープが基準になっていました。
トップレベルモジュールではこの2つに違いはなかったので、プロジェクトがサブモジュールを導入するほど大きくない限りはすべては単純に見えました。
Rust 2018 では、トップレベルモジュールかサブモジュールかに関わらず、use
宣言でのパスと他のプログラム中のパスは同じように使用できます。
現在のスコープからの相対パスも、外部クレート名から始まるパスも、crate
, super
, self
から始まるパスも使用できます。
今まではこう書いていたコードは:
// Rust 2015
extern crate futures;
use futures::Future;
mod foo {
pub struct Bar;
}
use foo::Bar;
fn my_poll() -> futures::Poll { ... }
enum SomeEnum {
V1(usize),
V2(String),
}
fn func() {
let five = std::sync::Arc::new(5);
use SomeEnum::*;
match ... {
V1(i) => { ... }
V2(s) => { ... }
}
}
Rust 2018 でも全く同じように書けます。ただし、extern crate
の行は消すことができます。
// Rust 2018
use futures::Future;
mod foo {
pub struct Bar;
}
use foo::Bar;
fn my_poll() -> futures::Poll { ... }
enum SomeEnum {
V1(usize),
V2(String),
}
fn func() {
let five = std::sync::Arc::new(5);
use SomeEnum::*;
match ... {
V1(i) => { ... }
V2(s) => { ... }
}
}
サブモジュール内でも、コードを全く変えずに、同じように書けます。
// Rust 2018
mod submodule {
use futures::Future;
mod foo {
pub struct Bar;
}
use foo::Bar;
fn my_poll() -> futures::Poll { ... }
enum SomeEnum {
V1(usize),
V2(String),
}
fn func() {
let five = std::sync::Arc::new(5);
use SomeEnum::*;
match ... {
V1(i) => { ... }
V2(s) => { ... }
}
}
}
これにより、コードを他の場所に移動することが簡単になり、マルチモジュールなプロジェクトがより複雑になるのを防止できます。
もし、例えば外部モジュールとローカルのモジュールが同名であるなど、パスが曖昧な場合は、エラーになります。
その場合、他と衝突している名前のうち一方を変更するか、明示的にパスの曖昧性をなくす必要があります。
パスの曖昧性をなくすには、::name
と書いて外部クレート名であることを明示するか、self::name
と書いてローカルのモジュールやアイテムであることを明示すればよいです。
トレイト関数の匿名パラメータの非推奨化
概要
- 関数に本体があるとき、トレイト関数のパラメータは、任意の論駁不可能なパターンを使えます。
詳細
RFC #1685 に基づいて、トレイト関数のパラメータを匿名にすることはできなくなりました。
例えば、2015 エディションでは、以下のように書けました:
#![allow(unused)] fn main() { trait Foo { fn foo(&self, u8); } }
2018 エディションでは、すべての引数に(ただの _
であってもいいので、何らかの)名前がついていなければなりません:
#![allow(unused)] fn main() { trait Foo { fn foo(&self, baz: u8); } }
新しいキーワード
概要
動機
トレイトオブジェクトを表す dyn Trait
dyn Trait
機能は、トレイトオブジェクトを使うための新しい構文です。簡単に言うと:
Box<Trait>
はBox<dyn Trait>
になり、&Trait
と&mut Trait
は&dyn Trait
と&mut dyn Trait
になる
といった具合です。プログラム内では:
#![allow(unused)] fn main() { trait Trait {} impl Trait for i32 {} // old // いままで fn function1() -> Box<Trait> { unimplemented!() } // new // これから fn function2() -> Box<dyn Trait> { unimplemented!() } }
これだけです!
なぜ?
トレイトオブジェクトにトレイト名をそのまま使うのは悪手だったと、後になって分かりました。 今までの構文は、経験者にとってさえ往々にして曖昧にして難解で、代替機能を使うべきで本来お呼びでないような場面1で頻繁に使われ、時には遅く、代替機能にはできることができないのです。
その上、impl Trait
が入ったことで、「impl Trait
か dyn Trait
か」の関係はより対称的になり、「impl Trait
か Trait
か」よりちょっといい感じです。
impl Trait
の説明はこちらです。
したがって、新しいエディションでは、トレイトオブジェクトが必要なときは、ただの Trait
でなく dyn Trait
を使うべきです。
1 訳注: 原文ではこの文は、本ページで説明する新構文を提案する RFC から抜粋された文章になっています。 特に脚注で示した箇所は、原文では "favors a feature that is not more frequently used than its alternatives" とあり、その文意は同 RFC に解説されています。以下では、それを要約します。
特定のトレイトを実装した異なる型を共通して扱いたいとき、大抵はトレイトオブジェクトを使う必要はありません。 単一のコンテナに複数の型の構造体を入れたい場合、enum
を使えばよいです。 関数の返り値が特定のトレイトを実装していると示すには、impl Trait
構文を使えばよいです。 特定のトレイトを実装する任意の型を関数の引数や構造体のフィールドにした場合、ジェネリクスを使えばよいです。 大抵の場合は、このようにセマンティクス面からもパフォーマンス面からもより適切な代替案があり、トレイトオブジェクトの出る幕はありません。
トレイトオブジェクトが真に必要なのは、これより複雑なことをしたい場合だけです。 しかし、Rust 2015 では、&Trait
のように書くだけで、「気軽に」トレイトオブジェクトが作れてしまうという罠がありました。 そこで、Rust 2018 では、どうしてもトレイトオブジェクトを作りたい場合は&dyn Trait
構文を使用することが必要になりました。
async
と await
これらのキーワードは Rust に非同期の機能を実装するために予約されました。非同期の機能は最終的に 1.39.0 でリリースされました。
キーワード try
キーワード try
は try
ブロックで使うために予約されましたが、(これを書いている時点で)まだ安定化されていません(追跡イシュー)
推論変数への生ポインタに対するメソッドのディスパッチ
概要
- リント
tyvar_behind_raw_pointer
はハードエラー1になりました。
(訳注) ハードエラーとは、#[allow(...)]
などを使って、ユーザーが無効化することができないエラーのことです。
例えば、ライフタイムに不整合があったとします。ユーザーが実際はそれが安全であると信じていても、そのようなコードをコンパイルすることは許されていません。 #[allow(...)]
による上書きも不可能となっています。これがハードエラーです。
一方、到達不能コード(たとえば関数の途中で return
をしており、それ以降のコードは決して実行されないような状況)は、コンパイル自体は可能で、時には役に立つこともあるので、ユーザーが #[allow(dead_code)]
と書くことで警告を抑制できます。これはハードエラーではありません。
詳細は rustc book の説明(英語) もご参照ください。
詳細
詳細は Rust のイシュー #46906 を参照してください。
訳注: 詳しく解説します。以下のプログラムをご覧ください2。
let s = libc::getenv(k.as_ptr()) as *const _; s.is_null()
1行目の
libc::getenv
は、*mut c_char
型の生ポインタを返す関数です。 このポインタはas
を使って*const
ポインタに変換できますが、その際これが「何の型のポインタであるか」を_
にて省略しています。2行目では、
*const _
であるs
に対して.is_null()
を呼び出しています。 任意の型T
について、プリミティブ型*const T
はis_null
という固有メソッドを持つので、ここで呼び出されるのはこの固有メソッドです。問題はこの後です。 現在はメソッド3を呼べる型は一部の型に限られていますが、 将来それを任意の型に拡張しようという提案が出ています。 この新機能は "arbitrary self types" (self の型の任意化)と呼ばれます。 しかし、これが導入されると困ったことが起きます。
次のような構造体があったとしましょう2:
#![feature(arbitrary_self_types)] struct MyType; impl MyType { // この関数が問題 fn is_null(self: *mut Self) -> bool { println!("?"); true } }
すると、最初のプログラムの2行目
s.is_null
はどうなるでしょうか? 変数s
はキャストによって*const _
、つまり「何かの型への生定数ポインタ」を意味していました。 そして今や、is_null
として呼び出せる関数は2つあります。 1つは先程の*const T
に対して実装されたis_null
、 もう一つは今*const MyType
に対して実装されたis_null
です。 つまり、メソッドの呼び出しに曖昧性が生じています。この問題の解決策は簡単です。キャスト後の型がどの定数ポインタになるのか明示すればよいです2:
let s = libc::getenv(k.as_ptr()) as *const libc::c_char; s.is_null()
こうすることで、
is_null
の候補は*const T
だけになります。libc::c_char
は他のクレートで定義された型ですので、 この型に対して新しくメソッドが実装されることはなく、恒久的に曖昧性がなくなります。こうした理由から、
*const _
や*mut _
など、「未知の型への生ポインタ」に対してメソッドを呼び出すと、コンパイラがそれを検知するようになりました。 最初は警告リントとして導入されましたが、Rust 2018 エディションでハードエラーに格上げされました。これが、本ページで説明されている変更点です。
これらのソースコードは mikeyhew 氏による rust-lang/rust#46906 へのコメントより引用されたものです。ただし、コメントが著者によって追加されています。
関連関数のうち、第一引数が self
であるものは、メソッド呼び出し演算子(.
)を用いて呼び出すことができます。
このような関連関数をメソッドと呼びます。
s.is_null()
と書くと、これは s
に対してメソッド is_null(...)
を呼び出していることになります。
(参考)
訳注: タイトルにある「推論変数」とは、英語では inference variable または existencial variable と呼ばれます。 Rust には型をある程度明示しなくても自動的に決定する機能(型推論)があります。 型推論とは、非常に単純に説明すると、未知の型を変数、プログラム中から得られる手がかりを条件とみなした「型の連立方程式」を解く事に当たります。 推論変数とは、この方程式における変数のことです。 今回は
*const _
、つまり「未知の型_
に対する定数生ポインタ」が出てきています。この_
が「推論変数」にあたります。 詳しくは、rustc book の型推論の説明 (英語)もご参照ください。
Cargo への変更
概要
Cargo.toml
にターゲットの指定がある場合であっても、他のターゲットの自動探索がされなくなるということはなくなりました1。path
フィールドが指定されていないターゲットに対して、src/{target_name}.rs
の形のターゲットパスは自動推定されなくなりました2。- 現在のディレクトリに対して
cargo install
できなくなりました。現在のパッケージをインストールしたい場合はcargo install --path .
と指定する必要があります3。
1 訳注:
Cargo は、Cargo.toml
内に明示的に指定されていなくても、フォルダ構成の慣習に従っているファイルに関しては、自動でターゲットに追加します。
例えば、src/bin/my_application.rs
というファイルがプロジェクト内に存在したら、Cargo.toml
に [[bin]]
セクションで my_application
の存在が宣言されていなくても、自動的にこのファイルがバイナリターゲット(つまり、cargo run --bin my_application
として実行できるもの)としてビルドされるようになっています。
これは、ターゲットの自動探索と呼ばれています。
Rust 2015 と Rust 2018 の違いは、Cargo.toml
に明示的にターゲットが1つ以上宣言されている場合(つまり、[lib]
, [[bin]]
, [[test]]
, [[bench]]
, [[example]]
のどれかが1つ以上ある場合)に生じます。
Rust 2015 では、これらのうちどれか1つが指定されていた場合、ターゲットの自動探索は無効化されました。
Rust 2018 では、その場合であっても、ターゲットの自動探索は無効化されません。
詳細は、Cargo Book の "Target auto-discovery"(英語)もご覧ください。
2 訳注:
Cargo は、ターゲットを宣言するセクション(つまり、例えば [lib]
セクションや [[bin]]
セクション)に path
フィールドがなかった場合、
name
フィールドとフォルダ構成の慣習に従ってパスを推論し、そのパスが存在すればそれを使用します。
たとえば、Cargo.toml
に
[[bin]]
name = "my_binary"
というセクションがあった場合、path
フィールドが src/bin/my_binary.rs
であると自動的に判断します。
そして、src/bin/my_binary.rs
が存在すれば、これをターゲットのソースとして採用することになります。
Rust 2015 では、これに加えて path
が src/my_binary.rs
である可能性も候補になりました。
すなわち、上の状況において src/bin/my_binary.rs
が存在しないが src/my_binary.rs
が存在する場合は、src/my_binary.rs
をターゲットのソースとして採用しました。
しかし、この挙動は時にわかりにくいエラーを生むため、この形のパスが候補とされる挙動を利用することは非推奨となり、Rust 2018 では挙動そのものが廃止されました。
3 訳注:
cargo install
コマンドは、指定したクレートのバイナリターゲットをインストールする、つまりビルドして実行ファイルを所定の場所に配置するためのコマンドです。
Rust 2015 では、カレントディレクトリが Cargo プロジェクト下であったときに cargo install
とだけ実行すると、そのプロジェクトに含まれるクレートを対象としてインストールが実行されました。
Rust 2018 ではこの挙動は廃止され、cargo install --path .
と明示的にカレントディレクトリのクレートをインストールの対象にすると宣言しなければならなくなりました。
Rust 2021
情報 | |
---|---|
RFC | #3085 |
リリースバージョン | 1.56.0 |
Rust 2021 エディションでは、新機能を追加し、言語をより一貫したものにして、 さらに将来の拡張性の余地を広げるための、いくつかの変更がなされています。 以下の章ではこれらの変更の詳細を見ていくと同時に、 既存のコードを移行するためのガイドも示していきます。
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
か?)- その場合、トレイトのインポートは名前解決に影響しないので、移行リントを出す必要はありません
デフォルトの Cargo のフィーチャリゾルバ
概要
edition = "2021"
ではCargo.toml
でresolver = "2"
が設定されているとみなされます。
詳細
Rust 1.51.0 から、Cargo には新しいフィーチャリゾルバがオプトインできるようになっています。
これは、Cargo.toml
で resolver = "2"
と書くことで有効化できます。
Rust 2021 から、これがデフォルトになりました。
つまり、 Cargo.toml
に edition = "2021"
と書けば、暗黙に resolver = "2"
も設定されているとみなされます。
このリゾルバはワークスペース全体に設定され、依存先では無視されます。
また、この設定はワークスペースの最上位のパッケージでしか効きません。
仮想ワークスペースにおいて新しいリゾルバをオプトインしたい場合は、
以前と同様に resolver
フィールドを明示的に設定する必要があります。
新しいフィーチャリゾルバは、クレートへの依存に異なるフィーチャが設定されていてもそれらをマージしないようになりました。 詳細は the announcement of Rust 1.51 に記載されています。
移行
新しいリゾルバに適合させるための自動化された移行ツールはありません。 ほとんどのプロジェクトでは、更新後に必要な変更はあっても微々たるものでしょう。
cargo fix --edition
でのアップデート時に、Cargo は新しいリゾルバで依存先のフィーチャに変更があるかどうかを表示します。
たとえば、このように表示されます:
note: Switching to Edition 2021 will enable the use of the version 2 feature resolver in Cargo. This may cause some dependencies to be built with fewer features enabled than previously. More information about the resolver changes may be found at https://doc.rust-lang.org/nightly/edition-guide/rust-2021/default-cargo-resolver.html
When building the following dependencies, the given features will no longer be used:(訳) 2021 エディションに切り替えると、Cargoのフィーチャリゾルバがバージョン 2 に切り替わります。 切り替え後、いくつかの依存先では有効化されるフィーチャが減少することがあります。 リゾルバの変更点については、 https://doc.rust-lang.org/nightly/edition-guide/rust-2021/default-cargo-resolver.html もご覧ください。
以下の依存先をビルドするときに、以下のフィーチャが使われなくなります:bstr v0.2.16: default, lazy_static, regex-automata, unicode libz-sys v1.1.3 (as host dependency): libc
これにより、記載されたフィーチャがその依存先で使われずにビルドされるようになることがわかります。
ビルドの失敗
状況によっては、変更後にプロジェクトが正しくビルドされなくなることもあります。 あるパッケージの依存関係が、別のパッケージにおいて特定のフィーチャが有効されることを前提にしている場合、そのフィーチャが使われなくなることでコンパイルに失敗するかもしれません。
たとえば、我々のパッケージにこんな依存関係があったとしましょう:
# Cargo.toml
[dependencies]
bstr = { version = "0.2.16", default-features = false }
# ...
そして依存関係の中にはこんなパッケージもあるとしましょう:
# Another package's Cargo.toml
# 別のパッケージの Cargo.toml
[build-dependencies]
bstr = "0.2.16"
我々のパッケージでは、今までは bstr
の words_with_breaks
関数を使用していたとします。この関数は(本来) bstr
の "unicode" フィーチャを有効化しないと使えないものです。
歴史的事情から、今まではこれでもうまくいきました。というのも、Cargo は2つのパッケージで使われている bstr
のフィーチャを共通化していたからです。
しかしながら、Rust 2021 へのアップデート後、 bstr
は1回目(ビルド依存関係として)はデフォルトのフィーチャで、2回目(我々のパッケージの通常の依存先として)はフィーチャなしで、合計2回ビルドされます。
今や bstr
は "unicode" フィーチャなしでビルドされるので、 words_with_breaks
メソッドは存在せず、メソッドがないというエラーが発生してビルドは失敗します。
ここでの解決策は、依存関係の宣言に我々が実際に使っているフィーチャを書くようにすることです。
[dependencies]
bstr = { version = "0.2.16", default-features = false, features = ["unicode"] }
ときには、あなたが直接いじることのできないサードパーティな依存先で問題が発生することもあります。
その場合は、問題が起こっている依存関係について、正しくフィーチャを指定するように、そのプロジェクトにパッチを送るのもよいでしょう。
あるいは、自身の Cargo.toml
に記載する依存関係にフィーチャを追加することもできます。
新しいリゾルバには以下のような併合ルールがあり、その下でフィーチャは併合されます。すなわち、
- 現在ビルドされていないターゲットに対するプラットフォーム特有の依存関係で有効化されているフィーチャは、無視されます。
- build-dependencies と proc-macro では、通常の依存関係とは独立したフィーチャが使用されます。
- dev-dependencies では、(tests や examples などの) ターゲットをビルドするときに必要でない限り、フィーチャは有効化されません。
実際の例としては、diesel
と diesel_migrations
を使用する場合が挙げられます。
これらのパッケージはデータベースへのサポートを提供しますが、データベースはフィーチャを用いて選択されます。たとえば、こんな感じです:
[dependencies]
diesel = { version = "1.4.7", features = ["postgres"] }
diesel_migrations = "1.4.0"
ここで問題なのは、 diesel_migrations
は内部に diesel
に依存する手続き的マクロをもちます。
この手続き的マクロは、自身が使用する diesel
で有効化されているフィーチャが、依存関係木の他の場所で有効化されているものと同じであると仮定します。
ところが、新しいリゾルバが使用されると、2つの diesel
が使用され、そのうち手続き的マクロ用のものは "postgres" フィーチャなしでビルドされるために、ビルドに失敗します。
ここでの解決策は、diesel
をビルド時の依存として追加し、そこに必要なフィーチャを指定することです。例えば以下のようになります。
[build-dependencies]
diesel = { version = "1.4.7", features = ["postgres"] }
これにより、 Cargo はホスト依存関係(proc-macro と build-dependencies)のフィーチャとして "postgres" を追加します。
訳注:ホスト依存関係とは、コンパイラホスト(コンパイラを実行しているプラットフォーム)向けにビルド・実行される依存を指し、proc-macro クレートや build-dependencies 配下の依存クレートが該当します。 一方、通常の依存関係はコンパイルターゲットのプラットフォーム向けにビルドされます。
これで、 diesel_migrations
の手続き的マクロは "postgres" フィーチャが有効化された状態で走り、正しくビルドされます。
(現在開発中の) diesel
のリリース 2.0 では、このような仮定なしに動くよう再設計されているため、このような問題は発生しません。
フィーチャを探索する
cargo tree
コマンドには、新しいリゾルバへの移行を補助する、素晴らしい新機能が含まれています。
cargo tree
を使えば、依存関係木を探索して、どのフィーチャが有効化されているか、そしてなによりなぜそれが有効化されているのかが分かります。
例えば、--duplicates
(短縮形: -d
) フラグを使用すると、同じパッケージが複数回ビルドされている場所がわかります。
さきほどの bstr
を例に取れば、このような表示になるでしょう:
> cargo tree -d
bstr v0.2.16
└── foo v0.1.0 (/MyProjects/foo)
bstr v0.2.16
[build-dependencies]
└── bar v0.1.0
└── foo v0.1.0 (/MyProjects/foo)
この出力から、bstr
が複数回ビルドされていることと、どの依存関係をたどると双方が現れるかが分かります。
-f
フラグを使えば、それぞれのパッケージがどのフィーチャを使用しているかがわかります。こんな感じです:
cargo tree -f '{p} {f}'
こうすると、Cargo は出力の「フォーマット」を変更して、パッケージと有効化されているフィーチャの双方を表示するようになります。
さらに、-e
フラグを使用してどの「辺」を表示してほしいか指定することもできます。
例えば、cargo tree -e features
とすれば、各依存関係の間に、各依存関係がどのフィーチャを追加しているのかが表示されます。
-i
フラグを使って木を「反転」させると、このオプションはより便利になります。
例えば、依存関係木があまりにも大きくて、何が bstr
に依存してるのかよくわからなくても、次のコマンドを実行すればいいです:
> cargo tree -e features -i bstr
bstr v0.2.16
├── bstr feature "default"
│ [build-dependencies]
│ └── bar v0.1.0
│ └── bar feature "default"
│ └── foo v0.1.0 (/MyProjects/foo)
├── bstr feature "lazy_static"
│ └── bstr feature "unicode"
│ └── bstr feature "default" (*)
├── bstr feature "regex-automata"
│ └── bstr feature "unicode" (*)
├── bstr feature "std"
│ └── bstr feature "default" (*)
└── bstr feature "unicode" (*)
この出力例からは、foo
が bar
に "default" フィーチャ付きで依存していることがわかり、
bar
はビルド時の依存として bstr
に "default" フィーチャ付きで依存していることもわかります。
さらに、bstr
の "default" フィーチャによって "unicode" フィーチャ(と、他のフィーチャも)が有効になっていることもわかります。
配列に対する IntoIterator
概要
- すべてのエディションで、配列が
IntoIterator
を実装するようになります。 - Rust 2015 と Rust 2018 では、メソッド呼び出し構文が使われても(つまり
array.into_iter()
と書いても)、IntoIterator::into_iter
は隠されています。 これにより、array.into_iter()
は従来どおり(&array).into_iter()
に解決されます。 - Rust 2021 から、
array.into_iter()
がIntoIterator::into_iter
を意味するように変更されます。
詳細
Rust 1.53 より前は、配列の参照だけが IntoIterator
を実装していました。
すなわち、&[1, 2, 3]
と &mut [1, 2, 3]
に対しては列挙できる一方で、[1, 2, 3]
に対して列挙することはできませんでした。
for &e in &[1, 2, 3] {} // Ok :)
// OK :)
for e in [1, 2, 3] {} // Error :(
// エラー :(
これは古くからある Issueですが、見た目ほど解決は簡単ではありません。
トレイト実装を追加するだけでは、既存のコードが壊れてしまいます。
メソッド呼び出し構文の仕組み上、array.into_iter()
は現状でも (&array).into_iter()
とみなされてコンパイルが通ります。
トレイト実装を追加すると、その意味が変わってしまうのです。
多くのケースで、この手の互換性破壊(トレイト実装の追加)は「軽微」で許容可能とみなされてきました。 しかし、このケースではあまりにも多くのコードが壊れてしまうのです。
何度も提案されてきたのは、「Rust 2021 でのみ配列に IntoIterator
を実装する」ことでした。
しかし、これは単に不可能なのです。
エディションは併用されうるので、あるエディションではトレイト実装が存在して、別のエディションでは存在しない、というわけにはいかないからです。
代わりに、(Rust 1.53.0 から)トレイト実装はすべてのエディションで追加されましたが、
Rust 2021 より前のコードが破壊されないようにちょっとしたハックが行われました。
Rust 2015 と 2018 のコードでは、コンパイラは従来どおり array.into_iter()
を (&array).into_iter()
に解決し、あたかもトレイト実装が存在しないかのように振る舞います。
これは .into_iter()
というメソッド呼び出し構文だけに適用されます。
一方、このルールは for e in [1, 2, 3]
, iter.zip([1, 2, 3])
, IntoIterator::into_iter([1, 2, 3])
といった他の構文には適用されず、
そのような書き方は全てのエディションで使えるようになります。
互換性破壊を防ぐためにちょっとしたハックが必要になったのは残念ですが、 これによりエディション間の違いが最小限になったのです。
移行
into_iter()
への呼び出しのうち、Rust 2021 で意味が変わるようなものに対しては、
array_into_iter
というリントが発生します。
1.41 のリリース以降、array_into_iter
リントはすでにデフォルトで警告として発出されています(1.55 ではさらにいくつかの機能追加が行われました)。
警告が今現在出ていないコードは、今すぐにでも Rust 2021 に進むことができます!
コードを自動的に Rust 2021 エディションに適合するよう自動移行するか、既に適合するものであることを確認するためには、以下のように実行すればよいです:
cargo fix --edition
エディション間の違いが少ないので、Rust 2021 への移行も非常に簡単です。
配列に対する into_iter
のメソッド呼び出しに関しては、(イテレータの)要素が参照でなく所有権を持った値となります。
例えば:
fn main() { let array = [1u8, 2, 3]; for x in array.into_iter() { // x is a `&u8` in Rust 2015 and Rust 2018 // x is a `u8` in Rust 2021 // Rust 2015 と Rust 2018 では、x は `&u8` // Rust 2021 では、x は `u8` } }
移行のための最も簡単な方法は、前のエディションと完全に同じ挙動をするように、
所有権を持った配列上を参照でイテレートするもう一つのメソッド iter()
を呼び出すことです:
fn main() { let array = [1u8, 2, 3]; for x in array.iter() { // <- This line changed // この行を書き換えた // x is a `&u8` in all editions // x はすべてのエディションで `&u8` } }
必須でない移行
前のエディションで完全修飾メソッド構文を使っていた場合(例: IntoIterator::into_iter(array)
)、
これはメソッド呼び出し構文に書き換え可能です(例: array.into_iter()
)。
クロージャはフィールドごとにキャプチャする
概要
|| a.x + 1
がa
でなくa.x
だけをキャプチャするようになりました。- これにより、ドロップのタイミングが変わったり、クロージャが
Send
やClone
を実装するかどうかが変わったりします。cargo fix
は、このような違いが起こりうると検出した場合、let _ = &a
のような文を挿入して、クロージャが変数全体をキャプチャするように強制します。
詳細
クロージャは、本体の中で使用しているすべてのものを自動的にキャプチャします。
例えば、|| a + 1
と書くと、周囲のコンテキスト中の a
への参照が自動的にキャプチャされます。
Rust 2018 以前では、クロージャに使われているのが1つのフィールドだけであっても、クロージャは変数全体をキャプチャします。
例えば、 || a.x + 1
は a.x
への参照だけでなく、a
への参照をキャプチャします。
a
全体がキャプチャされると、a
の他のフィールドの値を書き換えたりムーブしたりできなくなります。従って以下のようなコードはコンパイルに失敗します:
let a = SomeStruct::new();
drop(a.x); // Move out of one field of the struct
// 構造体のフィールドの1つをムーブする
println!("{}", a.y); // Ok: Still use another field of the struct
// OK: 構造体の他のフィールドは、まだ使える
let c = || println!("{}", a.y); // Error: Tries to capture all of `a`
// エラー: `a` 全体をキャプチャしようとする
c();
Rust 2021 からは、クロージャのキャプチャはより精密になります。 特に、使用されるフィールドだけがキャプチャされるようになります (場合によっては、使用する変数以外にもキャプチャすることもあり得ます。詳細に関しては Rust リファレンスを参照してください)。 したがって、上記のコードは Rust 2021 では問題ありません。
フィールドごとのキャプチャは RFC 2229 の一部として提案されました。この RFC にはより詳しい動機が記載されています。
移行
Rust 2018 のコードベースから Rust 2021 への自動移行の支援のため、2021 エディションには、移行用のリントrust_2021_incompatible_closure_captures
が追加されています。
rustfix
でコードを Rust 2021 エディションに適合させるためには、次のように実行します。
cargo fix --edition
以下では、クロージャによるキャプチャが出現するコードについて、自動移行が失敗した場合に手動で Rust 2021 に適合するように移行するにはどうすればいいかを考察します。 移行がどのようになされるか知りたい人も以下をお読みください。
クロージャによってキャプチャされる変数が変わると、プログラムの挙動が変わったりコンパイルできなくなったりすることがありますが、その原因は以下の2つです:
以下のような状況を検知すると、cargo fix
は「ダミーの let」をクロージャの中に挿入して、強制的に全ての変数がキャプチャされるようにします:
#![allow(unused)] fn main() { let x = (vec![22], vec![23]); let c = move || { // "Dummy let" that forces `x` to be captured in its entirety // `x` 全体が強制的にキャプチャされるための「ダミーの let」 let _ = &x; // Otherwise, only `x.0` would be captured here // それがないと、`x.0` だけがここでキャプチャされる println!("{:?}", x.0); }; }
この解析は保守的です。ほとんどの場合、ダミーの let は問題なく消すことができ、消してもプログラムはきちんと動きます。
ワイルドカードパターン
クロージャは本当に読む必要のあるデータだけをキャプチャするようになったので、次のコードは x
をキャプチャしません:
#![allow(unused)] fn main() { let x = 10; let c = || { let _ = x; // no-op // 何もしない }; let c = || match x { _ => println!("Hello World!") }; }
この let _ = x
は何もしません。
なぜなら、_
パターンは右辺を無視し、さらに、x
はメモリ上のある場所(この場合は変数)への参照だからです。
この変更(いくつかの値がキャプチャされなくなること)そのものによってコード変更の提案がなされることはありませんが、後で説明する「ドロップ順序」の変更と組み合わせると、提案がなされる場合もあります。
ちなみに: 似たような式の中には、同じく自動挿入される "ダミーの let" であっても、let _ = &x
のように「何もしない」わけではない文もあります。なぜかというと、右辺(&x
)はメモリ上のある場所を指し示すのではなく、値が評価されるべき式となるからです(その評価結果は捨てられますが)。
ドロップの順序
クロージャが変数 t
の値の所有権を取るとき、その値がドロップされるのは t
がスコープ外に出たときではなく、そのクロージャがドロップされたときになります:
#![allow(unused)] fn main() { fn move_value<T>(_: T){} { let t = (vec![0], vec![0]); { let c = || move_value(t); // t is moved here } // c is dropped, which drops the tuple `t` as well // c がドロップされ、そのときにタプル `t` もまたドロップされる } // t goes out of scope here // t はここでスコープを抜ける }
上記のコードの挙動は Rust 2018 と Rust 2021 で同じです。ところが、クロージャが変数の一部の所有権を取るとき、違いが発生します:
#![allow(unused)] fn main() { fn move_value<T>(_: T){} { let t = (vec![0], vec![0]); { let c = || { // In Rust 2018, captures all of `t`. // In Rust 2021, captures only `t.0` // Rust 2018 では、`t` 全体がキャプチャされる。 // Rust 2018 では、`t.0` だけがキャプチャされる move_value(t.0); }; // In Rust 2018, `c` (and `t`) are both dropped when we // exit this block. // Rust 2018 では、 `c` (と `t`) の両方が // このブロックを抜けるときにドロップされる。 // // In Rust 2021, `c` and `t.0` are both dropped when we // exit this block. // Rust 2021 では、 `c` と `t.0` の両方が // このブロックを抜けるときにドロップされる。 } // In Rust 2018, the value from `t` has been moved and is // not dropped. // Rust 2018 では、`t` はすでにムーブされており、ここではドロップされない // // In Rust 2021, the value from `t.0` has been moved, but `t.1` // remains, so it will be dropped here. // Rust 2021 では、`t.0` はムーブされているが、 // `t.1` は残っており、ここでドロップされる } }
ほとんどの場合、ドロップのタイミングが変わってもメモリが解放されるタイミングが変わるだけで、さほど問題にはなりません。
しかし、Drop
の実装に副作用のある(いわゆるデストラクタである)場合、ドロップの順序が変わるとプログラムの意味が変わってしまうかもしれません。
その場合は、コンパイラはダミーの let
を挿入して変数全体がキャプチャされるように提案します。
トレイト実装
何がキャプチャされているかによって、クロージャには自動的に以下のトレイトが実装されます:
Clone
: キャプチャされた値がすべてClone
を実装していた場合。Send
,Sync
,UnwindSafe
などの自動トレイト: キャプチャされた値がすべてそのトレイトを実装していた場合。
Rust 2021 では、キャプチャされる値が変わることによって、クロージャが実装するトレイトも変わることがあります。 先ほどの移行リントは、それぞれのクロージャについて、これまで実装されていた自動トレイトが何であるか、そして移行後もそれらが残るかどうかを調べます。 もし今まで実装されていたトレイトが実装されなくなる場合、「ダミーの let」が挿入されます。
例えば、スレッド間で生ポインタを受け渡しする一般的な方法に、ポインタを構造体でラップし、そのラッパー構造体に自動トレイト Send
/Sync
を実装するというものがあります。
thread::spawn
に渡されるクロージャが使うのは、ラッパー構造体のうち特定の変数だけですが、キャプチャされるのはラッパー構造体全体です。
ラッパー構造体は Send
/Sync
なので、コードは安全であるとみなされ、コンパイルは成功します。
フィールドごとのキャプチャが導入されると、キャプチャ内で使用されているフィールドだけがキャプチャされますが、フィールドの中身はもともと Send
/Sync
でなかったのですから、せっかくラッパーを作っても元の木阿弥です。
#![allow(unused)] fn main() { use std::thread; struct Ptr(*mut i32); unsafe impl Send for Ptr {} let mut x = 5; let px = Ptr(&mut x as *mut i32); let c = thread::spawn(move || { unsafe { *(px.0) += 10; } }); // Closure captured px.0 which is not Send // クロージャは px.0 をキャプチャしたが、これは Send ではない }
panic マクロの一貫性
概要
panic!(..)
では常にformat_args!(..)
が使われるようになりました。つまり、println!()
と同じ書き方をすることになります。panic!("{")
と書くことはできなくなりました。{
を{{
とエスケープしなくてはなりません。x
が文字列リテラルでないときに、panic!(x)
と書くことはできなくなりました。- 文字列でないペイロード付きでパニックしたい場合、
std::panic::panic_any(x)
を使うようにしてください。 - もしくは、
x
のDisplay
実装を用いて、panic!("{}", x)
と書いてください。
- 文字列でないペイロード付きでパニックしたい場合、
assert!(expr, ..)
に関しても同様です。
詳細
panic!()
は、Rust で最もよく知られたマクロの一つです。
しかし、このマクロにはいくぶん非直感的な挙動がありましたが、
今までは後方互換性の問題から修正できませんでした。
// Rust 2018
panic!("{}", 1); // Ok, panics with the message "1"
// OK。 "1" というメッセージと共にパニックする
panic!("{}"); // Ok, panics with the message "{}"
// OK。 "{}" というメッセージと共にパニックする
panic!()
マクロは、2つ以上の引数が渡されたときだけ、フォーマット文字列を使用します。
引数が1つのときは、引数の中身に見向きもしません。
// Rust 2018
let a = "{";
println!(a); // Error: First argument must be a format string literal
// エラー: 第一引数は文字列リテラルでなくてはならない
panic!(a); // Ok: The panic macro doesn't care
// OK: panicマクロは気にしない
その上、このマクロは panic!(123)
のように文字列でない引数を渡すこともできました。
このような使い方は稀で、ほとんど役に立ちません。
というのも、この呼び出しで出力されるメッセージはあきれるほど役に立たないからです: panicked at 'Box<Any>'
(訳: 'Box<Any>
でパニックしました)。
これで特に困るのは、暗黙のフォーマット引数が安定化されたときです。
この機能により、println!("hello {}", name)
の代わりに println!("hello {name}")
と略記できるようになります。
しかし、panic!("hello {name}")
は期待される挙動を示しません。
なぜなら、panic!()
は引数が1つだけ与えられたときにそれをフォーマット文字列として扱わないからです。
この紛らわしい状況を解決するために、Rust 2021 の panic!()
マクロはより一貫したものになりました。
新しい panic!()
マクロは、単一引数として任意の式を受け付けることがなくなりました。
代わりに、println!()
と同様に、常に最初の引数をフォーマット文字列として処理するようになりました。
panic!()
マクロが任意のペイロードを受け付けるわけではなくなったので、
フォーマット文字列以外のペイロードと共にパニックさせる唯一の方法は、
panic_any()
を使うことだけになりました。
// Rust 2021
panic!("{}", 1); // Ok, panics with the message "1"
// Ok。"1" というメッセージと共にパニックする
panic!("{}"); // Error, missing argument
// エラー。引数が足りない
panic!(a); // Error, must be a string literal
// エラー。文字列リテラルでないといけない
さらに、core::panic!()
と std::panic!()
は Rust 2021 で同一のものになります。
現在は、これらの間には歴史的な違いがあり、
#![no_std]
のオンオフを切り替えることで見て取ることができます。
移行
panic
への呼び出しのうち、非推奨の挙動を使用していて Rust 2021 ではエラーになる場所に対して、non_fmt_panics
というリントが発生します。
Rust 1.50 以降、non_fmt_panics
リントはすでにデフォルトで警告として発出されています(後のバージョンではさらにいくつかの機能追加が行われました)。
警告が今現在出ていないコードは、今すぐにでも Rust 2021 に進むことができます!
コードを自動的に Rust 2021 エディションに適合するよう自動移行するか、既に適合するものであることを確認するためには、以下のように実行すればよいです:
cargo fix --edition
手動で移行することを選んだり、そうする必要がある場合には、各 panic の呼び出しについて、 println
と同様のフォーマットに書き換えるか、std::panic::panic_any
を用いて非文字列型のデータと共にパニックさせるかを選ぶ必要があります。
例えば、panic!(MyStruct)
の場合、std::panic::panic_any
(これはマクロでなく関数であることに注意)を使うよう書き換えて、std::panic::panic_any(MyStruct)
とすればよいです。
パニックのメッセージに波括弧が含まれているのに、引数の個数が一致しない場合は(例: panic!("Some curlies: {}")
)、
println!
と同じ構文を使うか(つまり panic!("{}", "Some curlies: {}")
とするか)、波括弧をエスケープすれば(つまり panic!("Some curlies: {{}}")
とすれば)、
その文字列リテラルを用いてパニックすることができます。
構文の予約
概要
shikibetsushi#
,shikibetsushi"..."
,shikibetsushi'...'
の3つの構文が新たに予約され、トークン分割されなくなりました。- 主に影響を受けるのはマクロです。例えば、
quote!{ #a#b }
と書くことはできなくなりました。 - キーワードが特別扱いされることもないので、例えば
match"..." {}
と書くこともできなくなりました。 - 識別子と後続の
#
,"
,'
の間に空白文字を挿入することで、エラーを回避できます。 - エディション移行ツールは、必要な場所に空白を挿入してくれます。
詳細
私達は、将来新しい構文を導入する余地を残すため、接頭辞付きの識別子とリテラルの構文を予約することにしました。
予約されたのは、任意の識別子 prefix
を用いて prefix#identifier
, prefix"string"
, prefix'c'
, prefix#123
のいずれかの形で書かれるものです。
(ただし、b'...'
(バイト文字列)やr"..."
(生文字列)のように、すでに意味が割り当てられているものを除きます。)
これにより、将来エディションをまたくごとなく構文を拡張できるようになります。 これを、次のエディションまでの一時的な構文のために使ったり、もし適切なら、恒久的な構文のために使ったりするでしょう。
エディションの区切りがないと、これは破壊的変更に当たります。
なぜなら、現在のマクロは、例えば hello"world"
という構文を、 hello
と "world"
の2つのトークンとして受け入れるからです。
もっとも、(自動)修正はシンプルで、hello "world"
のように空白を入れるだけです。
同様に、prefix#ident
は prefix #ident
とする必要があります。
エディション移行ツールは、そのように修正してくれます。
これが提案された RFC は、このような書き方をトークン分割エラーにすると決めているだけで、 特定の接頭辞に意味を持たせることはまだしていません。 接頭辞に何らかの役割を割り当てるのは、将来の提案にその余地が残されています。 接頭辞が予約されたおかげで、今後は破壊的変更なく新しい構文を導入できます。
例えば、以下のような接頭辞が使えるようになるかもしれません(ただし、いずれもまだ提案が固まったわけではありません):
k#keyword
で、現在のエディションにまだ導入されていないキーワードを書けるようにする。 たとえば、2015 エディションではasync
はキーワードではありませんが、 このような接頭辞が使えたのならば、2018 エディションでasync
が予約語になるのを待たずに、2015 エディションでもk#async
が使えたということになります。
f""
で、フォーマット文字列の略記とする。 例えば、f"hello {name}"
と書いたら、それと等価なformat!()
の呼び出しと見なす。
s""
でString
リテラルを表す。
c""
かz""
で、ヌル終端のC言語の文字列を表す。
移行
Rust 2018 のコードベースから Rust 2021 への自動移行の支援のため、2021 エディションには、移行用のリントrust_2021_prefixes_incompatible_syntax
が追加されています。
rustfix
でコードを Rust 2021 エディションに適合させるためには、次のように実行します。
cargo fix --edition
コード移行を手で行いたいか、行う必要があっても、移行は非常に簡単です。
例えば、次のように定義されたマクロがあったとしましょう:
#![allow(unused)] fn main() { macro_rules! my_macro { ($a:tt $b:tt) => {}; } }
Rust 2015 と 2018 では、以下のように、1つ目と2つ目のトークンの間に空白を入れることなくマクロを呼び出しても問題ありませんでした:
my_macro!(z"hey");
Rust 2021 では z
という接頭辞は許されないので、このマクロを呼び出すためには、以下のように接頭辞の後にスペースを入れる必要があります:
my_macro!(z "hey");
警告からエラーへの格上げ
概要
bare_trait_objects
リントかellipsis_inclusive_range_patterns
リントが出るコードは、Rust 2021 ではエラーになります。
詳細
現存する2つのリントが Rust 2021 ではエラーになります。古いエディションではこれらは警告のままです。
bare_trait_objects
:
Rust 2021 では、トレイトオブジェクトを表すために、dyn
キーワードを使用することが必須になりました。
例えば、以下のコードでは &MyTrait
に dyn
キーワードが含まれていないため、Rust 2021 ではただのリントではなくエラーが発生します:
#![allow(unused)] fn main() { pub trait MyTrait {} pub fn my_function(_trait_object: &MyTrait) { // should be `&dyn MyTrait` // `&dyn MyTrait` と書かなくてはならない unimplemented!() } }
ellipsis_inclusive_range_patterns
:
閉区間パターン(つまり、終端の値を含む範囲)を表す、非推奨の ...
構文は、Rust 2021 では使えなくなります。
式との統一性のため、..=
を使うことが推奨されていました。
例えば、次のコードはパターンとして ...
を使っているため、Rust 2021 ではただのリントではなくエラーが発生します:
#![allow(unused)] fn main() { pub fn less_or_eq_to_100(n: u8) -> bool { matches!(n, 0...100) // should be `0..=100` // `0..=100` と書かなくてはならない } }
移行
あなたの Rust 2015 か 2018 のコードで、bare_trait_objects
や ellipsis_inclusive_range_patterns
といったエラーが出ず、#![allow()]
などを使ってこれらのリントを許可する設定をしていなければ、移行の際にやることはありません。
...
をパターンに使用していたり、トレイトオブジェクトに dyn
を使っていないクレートがある場合は、
cargo fix --edition
を実行すればよいです。
マクロ規則における OR パターン
概要
macro_rules
におけるパターンの挙動がほんの少し変更されました:macro_rules
において、$_:pat
で|
を使ったパターンにもマッチするようになりました。例えば、A | B
にマッチします。- 新しく導入された
$_:pat_param
は、かつての$_:pat
と同じ挙動を再現します。すなわち、こちらは(トップレベルの)|
にはマッチしません。 $_:pat_param
は全てのエディションで使用可能です。
詳細
Rust 1.53.0 から、パターン中のどこでも、|
をネストして使えるようになりました。
これにより、Some(1) | Some(2)
でなく Some(1 | 2)
と書くことができるようになりました。
今まではこうは書けなかったので、これは破壊的変更ではありません。
ところが、この変更は macro_rules
で定義されたマクロ にも影響します。
macro_rules
では、:pat
というフラグメント指定子で、パターンを受け付けることができます。
現在のところ、:pat
はトップレベルの |
にマッチしません。
なぜなら Rust 1.53 以前は、全てのパターンが(どのネストレベルにでも)|
を含むことができるわけではなかったからです。
matches!()
のように、
A | B
のようなパターンを受け付けるマクロを書くには、
$($_:pat)|+
のような書き方をしなくてはなりませんでした。
既存のマクロを壊す可能性があるため、Rust 1.53.0 では :pat
が |
を含むことができるようには変更されませんでした。
代わりに、Rust 2021 で変更がなされました。
新しいエディションでは、:pat
フラグメント指定子は A | B
にマッチします。
Rust 2021 では、$_:pat
フラグメントに |
そのものを続けることはできません。
パターンフラグメントに |
が続いてるものにマッチさせたいような場合は、新しく追加された :pat_param
が過去と同じ挙動を示すようになっています。
ただし、エディションはクレートごとに設定されることに注意してください。 つまり、マクロが定義されているクレートのエディションだけが関係します。 マクロを使用する方のクレートのエディションは、マクロの挙動に影響しません。
移行
$_:pat
が使われている場所のうち、Rust 2021 で意味が変わるようなものに対しては、rust_2021_incompatible_or_patterns
というリントが発生します。
コードを自動的に Rust 2021 エディションに適合するよう自動移行するか、既に適合するものであることを確認するためには、以下のように実行すればよいです:
cargo fix --edition
あなたのマクロが、$_:pat
がトップレベルの |
にマッチしないという挙動に依存している場合は、
$_:pat
を $_:pat_param
に書き換える必要があります。
例えば以下のようになります。
#![allow(unused)] fn main() { macro_rules! my_macro { ($x:pat | $y:pat) => { // TODO: implementation // TODO: 実装 } } // This macro works in Rust 2018 since `$x:pat` does not match against `|`: // Rust 2018 では、`$x:pat` が `|` にマッチしないので、以下のマクロは正常に動きます: my_macro!(1 | 2); // In Rust 2021 however, the `$_:pat` fragment matches `|` and is not allowed // to be followed by a `|`. To make sure this macro still works in Rust 2021 // change the macro to the following: // 一方 Rust 2021 では、`$_:pat` フラグメントは `|` にもマッチし、 // `|` が続くのは許されなくなりました。 // Rust 2021 でもマクロが動作するためには、マクロを以下のように変更しなくてはなりません: macro_rules! my_macro { ($x:pat_param | $y:pat) => { // <- this line is different // この行を変えた // TODO: implementation // TODO: 実装 } } }