システムプログラマーのためのRust
経験豊富なCおよびC++プログラマー向けのRustチュートリアル。
目次へ移動。 コントリビューションへ移動。
このチュートリアルは、ポインターと参照がどのように機能するかをすでに理解しており、 整数幅やメモリ管理といったシステムプログラミングの概念に慣れているプログラマーを 対象としています。主にRustとC++の違いを取り上げることで、 おそらくすでに知っているであろう余分な説明をあまり挟まずに、 Rustプログラムをすばやく書けるようになることを目指しています。
願わくは、RustがC++プログラマーにとってかなり直感的な言語であるとよいと思います。 構文のほとんどはかなり似ています。(私の経験では)大きな違いは、 優れたシステムプログラミングに関する、時に曖昧な概念がコンパイラーによって 厳格に強制されることです。これは最初は腹立たしいことがあります。やりたいことが あるのに、コンパイラーがそれを許してくれない(少なくとも安全なコードでは) からです。そして時には、それらのことが実際には安全であるにもかかわらず、 それをコンパイラーに納得させられないことがあります。しかし、何が許されるかについての 良い直感はすぐに身に付くでしょう。メモリ安全性に関する自分自身の考えを コンパイラーに伝えるには、新しく、時には複雑な型注釈がいくつか必要です。 しかし、オブジェクトのライフタイムについてしっかりした考えを持ち、 ジェネリックプログラミングの経験があるなら、それらを学ぶのはそれほど 難しくないはずです。
このチュートリアルは、一連のブログ記事として始まりました。 一つには、私(@nrc)がRustを学ぶための助けとしてです(何かを学んだことを 確認するには、それを誰かに説明しようとする以上に良い方法はありません)。 もう一つには、既存のRust学習リソースに満足できなかったからです。それらは、 私がすでに知っている基礎に時間をかけすぎており、私にとってはより低レベルの 直感を使ったほうがうまく説明できる概念を、高レベルの直感で説明していました。 その後、Rustのドキュメントははるかに良くなりましたが、それでも既存の C++プログラマーはRustの自然な対象読者である一方で、特に十分に対応されている わけではないと思っています。
目次
- はじめに - Hello world!
- 制御フロー
- プリミティブ型と演算子
- 一意ポインター
- 借用ポインター
- Rcと生ポインター
- データ型
- デストラクチャリング pt 1
- デストラクチャリング pt 2
- 配列とvec
- グラフとアリーナ割り当て
- クロージャーと第一級関数
その他のリソース
- The Rust book/guide - Rust全般を学ぶための 最良の場所であり、おそらくここに書かれている内容について別の見解を得たり、 ここで扱われていない内容を調べたりするために行くべき最良の場所です。
- Rust APIドキュメント - Rustライブラリの 詳細なドキュメントです。
- The Rust reference manual - 一部は少し 古くなっていますが、網羅的です。詳細を調べるのに適しています。
- Discussフォーラム - Rustの使用や学習に関する議論や 質問のための一般的なフォーラムです。
- StackOverflowのRust質問 - Rustに関する 多くの初心者向けおよび上級者向けの質問への回答があります。ただし注意してください。 Rustは長年にわたって大きく変化しており、一部の回答は非常に古くなっている可能性があります。
- A Firehose of Rust - C++プログラマーに、 Rustでライフタイム、可変エイリアシング、ムーブセマンティクスがどのように機能するかを 紹介する録画された講演です。
コントリビューション
ぜひお願いします!
誤字や間違いを見つけた場合は、遠慮せずPRを送ってください!より大きな変更や、 見てみたい新しい章については、issueを 自由に作成してください。チュートリアルがそのような方法で改善できると思うなら、 既存の作業の再編成や例の拡充も歓迎します。
段落、セクション、または章をコントリビュートしたい場合は、ぜひお願いします! 扱う内容のアイデアが欲しい場合は、issueの一覧を参照してください。 特にnew materialタグが付いたものです。 何か確信が持てないことがある場合は、ここで私(@nrc)をpingするか、 irc(#rustまたは#rust-internals上のnrc)で連絡してください。
スタイル
当然ながら、想定読者はC++プログラマーです。このチュートリアルは、一般読者向けではなく、 経験豊富なC++プログラマーにとって新しいことに集中すべきです(ただし、読者が C++の最新バージョンに詳しいとは想定していません)。基礎的な内容が多すぎることは避けたいですし、 他のリソース、特にRust guide/bookとの重複が多すぎることは絶対に避けたいです。
エッジケースのユースケース(たとえば、Cargoとは異なるビルドシステムを使用する、 構文拡張を書く、不安定なAPIを使用するなど)に関する作業はもちろん歓迎します。 また、すでに高レベルで扱われているトピックについて掘り下げる作業も歓迎します。
C++コードをRustコードに変換するためのレシピ形式の例は避けたいですが、 この種の小さな例は問題ありません。
異なる形式(たとえば、質疑応答/FAQ、またはより大きな実例)の使用は歓迎します。
演習やミニプロジェクトの提案を追加する予定はありませんが、それに興味がある場合は 知らせてください。
かなり学術的なトーンを目指していますが、あまり堅苦しくはしません。すべての文章は 英語(アメリカ英語ではなくイギリス英語。ただし、アメリカ英語を含む任意の言語への ローカライズ/翻訳は大歓迎です)で書かれ、有効なGitHub markdownであるべきです。 文体、文法、句読点などに関する助言については、Oxford Style Manual またはThe Economist Style Guideを参照してください。 幅は80カラムに制限してください。私はOxford commaが好きです。
提出する作業が完璧でなければならないとは思わないでください。私は喜んで編集しますし、 将来的には他の人たちもきっとそうしてくれるでしょう。
はじめに - hello world!
C や C++ を使用しているとしたら、おそらくそれは、そうしなければならないからです。システムへの低レベルアクセスが必要であるか、最後の一滴まで性能を引き出す必要があるか、あるいはその両方が必要なのでしょう。Rust は、メモリに関する同じレベルの抽象化、同じ性能を提供しつつ、より安全で、より生産的にすることを目指しています。
具体的には、C++ よりも使いたいと思うかもしれない言語は世の中に数多くあります。Java、Scala、Haskell、Python などです。しかし、それらを使えないのは、抽象化レベルが高すぎる(メモリへ直接アクセスできない、ガベージコレクションの使用を強制される、など)か、性能上の問題がある(性能が予測不能であるか、単に十分に高速ではない)ためです。Rust はガベージコレクションの使用を強制せず、C++ と同様に、メモリへの生ポインターを扱うことができます。Rust は C++ の「使った分だけ支払う」という哲学を採用しています。ある機能を使わなければ、その機能が存在することによる性能上のオーバーヘッドを支払うことはありません。さらに、Rust のすべての言語機能には予測可能な(そして通常は小さな)コストがあります。
こうした制約により、Rust は C++ の(まれな)実行可能な代替手段になっていますが、Rust には利点もあります。Rust はメモリ安全です。Rust の型システムにより、C++ でよくある種類のメモリエラー、つまり未初期化メモリへのアクセスやダングリングポインターは、Rust ではすべて不可能になります。さらに、他の制約が許す限り、Rust は他の安全性の問題も防ごうとします。たとえば、すべての配列インデックスアクセスは境界チェックされます(もちろん、そのコストを避けたい場合は、安全性を犠牲にして避けることができます。Rust では、他の多くの unsafe なことと同様に、unsafe ブロック内でこれを行えます。重要なのは、Rust が unsafe ブロック内の unsafe 性を unsafe ブロック内に留め、それがプログラムの残りの部分に影響できないようにすることです)。最後に、Rust は現代的なプログラミング言語から多くの概念を取り入れ、それらをシステム言語の領域に導入しています。願わくは、それによって Rust でのプログラミングがより生産的で、効率的で、楽しいものになりますように。
このセクションの残りでは、Rust をダウンロードしてインストールし、最小限の Cargo プロジェクトを作成し、Hello World を実装します。
Rust の入手
Rust は http://www.rust-lang.org/tools/install から入手できます。 そこからのダウンロードには、Rust コンパイラー、標準ライブラリ、そして Rust のパッケージマネージャー兼ビルドツールである Cargo が含まれています。
Rust には stable、beta、nightly の 3 つのチャネルがあります。Rust は 6 週間ごとに新しいリリースを行う迅速なリリーススケジュールで運用されています。リリース日には、nightly が beta になり、beta が stable になります。
Nightly は毎晩更新され、最先端の機能を試したいユーザーや、自分のライブラリが将来の Rust でも動作することを確認したいユーザーに最適です。
Stable はほとんどのユーザーにとって正しい選択です。Rust の安定性保証は stable チャネルにのみ適用されます。
Beta は主に、コードが期待どおりに動作し続けることを確認するために、ユーザーの CI で使用されることを想定して設計されています。
したがって、おそらく stable チャネルを使うのがよいでしょう。Linux または OS X を使っている場合、それを入手する最も簡単な方法は次を実行することです。
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Windows では、同様に簡単な方法として次を実行できます。
choco install rust
その他のインストール方法については、http://www.rust-lang.org/tools/install を参照してください。
ソースは github.com/rust-lang/rust で見つけることができます。
コンパイラーをビルドするには、./configure && make rustc を実行します。より詳細な手順については、building-from-source を参照してください。
Hello World!
Rust プログラムをビルドする最も簡単で一般的な方法は、Cargo を使うことです。Cargo を使って hello というプロジェクトを開始するには、cargo new --bin hello を実行します。これにより、hello という新しいディレクトリが作成され、その中に Cargo.toml ファイルと、main.rs というファイルを含む src ディレクトリが作成されます。
Cargo.toml は、私たちのプロジェクトに関する依存関係やその他のメタデータを定義します。これについては後で詳しく戻ってきます。
すべてのソースコードは src ディレクトリに入ります。main.rs にはすでに Hello World プログラムが含まれています。次のようになっています。
fn main() { println!("Hello, world!"); }
プログラムをビルドするには、cargo build を実行します。ビルドして実行するには、cargo run を実行します。後者を行うと、コンソールに歓迎のメッセージが表示されるはずです。成功です!
Cargo は target ディレクトリを作成し、そこに実行可能ファイルを配置しています。
コンパイラーを直接使いたい場合は、rustc src/main.rs を実行できます。これにより、main という実行可能ファイルが作成されます。多くのオプションについては rustc --help を参照してください。
では、コードに戻りましょう。いくつか興味深い点があります。関数やメソッドを定義するには fn を使います。main() は私たちのプログラムのデフォルトのエントリーポイントです(プログラム引数については後に回します)。C++ のような別個の宣言やヘッダーファイルはありません。println! は Rust における printf 相当のものです。! は、それがマクロであることを意味します。標準ライブラリの一部は、明示的にインポート/インクルードしなくても利用できます(prelude)。println! マクロはそのサブセットの一部として含まれています。
例を少し変更してみましょう。
fn main() { let world = "world"; println!("Hello {}!", world); }
let は変数を導入するために使われます。world は変数名であり、これは文字列です(技術的には型は &'static str ですが、それについては後で詳しく説明します)。型を指定する必要はなく、推論されます。
println! 文で {} を使うのは、printf で %s を使うようなものです。実際には、それより少し一般的です。なぜなら、Rust は変数がすでに文字列でない場合でも、その変数を文字列に変換しようとするからです1(C++ の operator<<() のようなものです)。
この種のことは簡単に試して遊ぶことができます。複数の文字列を試したり、数値を使ったりしてみてください(整数リテラルと浮動小数点リテラルは動作します)。
必要であれば、world の型を明示的に与えることができます。
#![allow(unused)] fn main() { let world: &'static str = "world"; }
C++ では、型 T の変数 x を宣言するために T x と書きます。Rust では、let 文でも関数シグネチャなどでも、x: T と書きます。ほとんどの場合、let 文では明示的な型を省略しますが、関数引数では必須です。動作を確認するために、別の関数を追加してみましょう。
fn foo(_x: &'static str) -> &'static str { "world" } fn main() { println!("Hello {}!", foo("bar")); }
関数 foo には、文字列リテラルである単一の引数 _x があります(main から "bar" を渡しています)2。
関数の戻り値の型は -> の後に指定します。関数が何も返さない場合(C++ の void 関数)、戻り値の型をまったく指定する必要はありません(main のように)。非常に明示的にしたい場合は、-> () と書くことができます。() は Rust の void 型です。
Rust では return キーワードは必要ありません。関数本体(または他の任意のブロック、これについては後でさらに見ます)の最後の式がセミコロンで終わっていなければ、それが戻り値になります。したがって、foo は "world" を返します。return キーワードは依然として存在するため、早期 return を行うことができます。"world" を return "world"; に置き換えることができ、同じ効果になります。
なぜ?
上記の言語機能のいくつかについて、その動機を説明したいと思います。ローカル型推論は、安全性や性能を犠牲にすることなく便利で有用です(現在では現代的なバージョンの C++ にさえ含まれています)。小さな利便性として、言語項目が一貫してキーワード(fn、let など)で表されることが挙げられます。これにより、人の目でもツールでも走査しやすくなります。一般に、Rust の構文は C++ よりも単純で一貫性があります。println! マクロは printf より安全です。引数の数は文字列内の「穴」の数に対して静的にチェックされ、引数は型チェックされます。つまり、メモリを実際とは異なる型であるかのように出力したり、誤ってスタックのさらに先のメモリを参照したりするような printf の間違いを犯すことはできません。これらはかなり小さなことですが、Rust の設計の背後にある哲学を示していることを願っています。
-
これはプログラマーが指定する変換で、
Displayトレイトを使用します。これは Java のtoStringに少し似た働きをします。{:?}を使用することもでき、これはコンパイラが生成した表現を提供し、デバッグに役立つことがあります。printf と同様に、他にも多くのオプションがあります。 ↩ -
fooでは実際にはその引数を使用していません。通常、 Rust はこれについて警告します。引数名の先頭に_を付けることで、 これらの警告を回避できます。実際には、引数に名前を付ける必要すらなく、 単に_を使用できます。 ↩
制御フロー
If
Rust の if 文は、C++ のものとほとんど同じです。違いの 1 つは、
波括弧が必須である一方、テストされる式を囲む括弧は不要であることです。
もう 1 つは、if が式であるため、C++ の三項 ?: 演算子と同じように
使えることです(前のセクションで見たように、ブロック内の最後の式が
セミコロンで終端されていない場合、それがブロックの値になります)。
Rust には三項 ?: はありません。したがって、次の 2 つの関数は同じことをします。
#![allow(unused)] fn main() { fn foo(x: i32) -> &'static str { let result: &'static str; if x < 10 { result = "less than 10"; } else { result = "10 or more"; } return result; } fn bar(x: i32) -> &'static str { if x < 10 { "less than 10" } else { "10 or more" } } }
(なぜ mut result ではないのでしょうか? foo のコードでは result は不変であり、
単に 2 つの可能な場所で初期化されているだけです。Rust は return result の時点までに、
それが初期化済みであることが保証されていると判断できます。)
1 つ目は、C++ で書くであろうものをかなり直訳したものです。 2 つ目のほうが、より Rust らしいスタイルです。
let result = if x < 10 ... などのように書くこともできます。
ループ
Rust には while ループがあり、これも C++ と同じです。
fn main() { let mut x = 10; while x > 0 { println!("Current value: {}", x); x -= 1; } }
Rust には do...while ループはありませんが、永遠にループするだけの
loop 文があります。
fn main() { loop { println!("Just looping"); } }
Rust には C++ と同じように break と continue があります。
for ループ
Rust にも for ループがありますが、これは少し異なります。整数のベクターがあり、
それらをすべて出力したいとしましょう(ベクター/配列、イテレーター、ジェネリクスについては、
今後より詳しく扱います。現時点では、Vec<T> は T の列であり、iter() は、
反復したいと思うようなものからイテレーターを返す、ということを知っていれば十分です)。
単純な for ループは次のようになります。
#![allow(unused)] fn main() { fn print_all(all: Vec<i32>) { for a in all.iter() { println!("{}", a); } } }
TODO all.iter() の代わりに &all/all も
all のインデックスを使って走査したい場合(標準的な C++ の配列に対する
for ループに少し近い形)、次のようにできます。
#![allow(unused)] fn main() { fn print_all(all: Vec<i32>) { for i in 0..all.len() { println!("{}: {}", i, all[i]); } } }
len 関数が何をするかは、うまくいけば明らかでしょう。TODO 範囲記法
前の例に相当する、より Rust らしい書き方は、列挙イテレーターを使うことです。
#![allow(unused)] fn main() { fn print_all(all: Vec<i32>) { for (i, a) in all.iter().enumerate() { println!("{}: {}", i, a); } } }
ここで enumerate() はイテレーター iter() からチェーンされ、反復中に現在の
カウントと要素を生成します。
次の例には、Borrowed Pointers のセクションで扱うより高度なトピックが含まれています。
整数のベクターがあり、そのベクターを参照渡しで関数に渡して、その場で変更したいとしましょう。
ここで for ループは可変参照を与える可変イテレーターを使用します。
* によるデリファレンスは C++ プログラマーにはおなじみのはずです。
#![allow(unused)] fn main() { fn double_all(all: &mut Vec<i32>) { for a in all.iter_mut() { *a += *a; } } }
Switch/Match
Rust には、C++ の switch 文に似ているものの、はるかに強力な match 式があります。 この単純なバージョンはかなり見慣れたものに見えるはずです。
#![allow(unused)] fn main() { fn print_some(x: i32) { match x { 0 => println!("x is zero"), 1 => println!("x is one"), 10 => println!("x is ten"), y => println!("x is something else {}", y), } } }
構文上の違いがいくつかあります。一致した値から実行する式へ進むために => を使い、
match のアームは , で区切られます(最後の , は省略可能です)。
それほど明白ではない意味論上の違いもあります。一致させるパターンは網羅的でなければなりません。
つまり、一致対象の式(上の例では x)の取り得るすべての値がカバーされていなければなりません。
y => ... の行を削除して、何が起こるか試してみてください。これは、0、1、10 に対する
マッチしかなく、明らかにそれ以外にもマッチされない整数がたくさんあるためです。
最後のアームでは、y が一致対象の値(この場合は x)に束縛されます。
次のように書くこともできます。
#![allow(unused)] fn main() { fn print_some(x: i32) { match x { x => println!("x is something else {}", x) } } }
ここでは、match アーム内の x が新しい変数を導入し、引数の x を隠蔽します。
これは内側のスコープで変数を宣言するのと同じです。
変数に名前を付けたくない場合は、名前のない変数として _ を使うことができます。
これはワイルドカードマッチのようなものです。何もしたくない場合は、空の分岐を用意できます。
#![allow(unused)] fn main() { fn print_some(x: i32) { match x { 0 => println!("x is zero"), 1 => println!("x is one"), 10 => println!("x is ten"), _ => {} } } }
もう 1 つの意味論上の違いは、あるアームから次のアームへのフォールスルーがないことです。
そのため、if...else if...else のように動作します。
後の投稿で、match が非常に強力であることを見ていきます。今は、さらに 2 つだけ機能を紹介したいと思います。
値に対する「or」演算子と、アーム上の if 節です。例を見れば自明であることを期待します。
#![allow(unused)] fn main() { fn print_some_more(x: i32) { match x { 0 | 1 | 10 => println!("x is one of zero, one, or ten"), y if y < 20 => println!("x is less than 20, but not zero, one, or ten"), y if y == 200 => println!("x is 200 (but this is not very stylish)"), _ => {} } } }
if 式と同じように、match 文も実際には式であるため、
最後の例を次のように書き換えることができます。
#![allow(unused)] fn main() { fn print_some_more(x: i32) { let msg = match x { 0 | 1 | 10 => "one of zero, one, or ten", y if y < 20 => "less than 20, but not zero, one, or ten", y if y == 200 => "200 (but this is not very stylish)", _ => "something else" }; println!("x is {}", msg); } }
閉じ波括弧の後のセミコロンに注意してください。これは let 文が文であり、
let msg = ...; という形式を取らなければならないためです。右辺には match 式を入れています
(通常はセミコロンを必要としません)が、let 文にはセミコロンが必要です。
私はこれによく引っかかります。
動機: Rust の match 文は、C++ の switch 文でよくあるバグを避けます。
break を忘れて意図せずフォールスルーすることはありません。また、enum にケースを追加した場合
(詳しくは後で扱います)、コンパイラーがそれが match 文でカバーされていることを確認してくれます。
メソッド呼び出し
最後に、Rust にも C++ と同様にメソッドが存在することを簡単に述べておきます。
メソッドは常に . 演算子を介して呼び出されます(-> はありません。これについては別の投稿でさらに説明します)。
上でいくつか例を見ました(len、iter)。それらがどのように定義され、呼び出されるかについては、
今後さらに詳しく見ていきます。C++ や Java から推測するであろうほとんどの仮定は、おそらく正しいです。
プリミティブ型と演算子
Rust には、C++ とほぼ同じ算術演算子と論理演算子があります。bool は
両方の言語で同じです(true と false リテラルも同様です)。Rust には
整数、符号なし整数、浮動小数点数について同様の概念があります。ただし構文は少し異なります。Rust では isize は整数を意味し、usize は
符号なし整数を意味します。これらの型はポインターサイズです。たとえば、32 ビットシステムでは、
usize は 32 ビット符号なし整数を意味します。Rust には明示的にサイズが指定された型もあり、
u または i の後に 8、16、32、64、または 128 が続きます。したがって、たとえば、u8 は
8 ビット符号なし整数で、i32 は 32 ビット符号付き整数です。浮動小数点数については、Rust には
f32 と f64 があります。
数値リテラルには、その型を示すサフィックスを付けることができます。サフィックスが指定されていない場合、Rust は
型を推論しようとします。推論できない場合は、i32 または f64(小数点がある場合)を使用します。
例:
fn main() { let x: bool = true; let x = 34; // 型は i32 // let x = 2147483648; // エラー: リテラルが `i32` の範囲外 let x = 34isize; let x = 34usize; let x = 34u8; let x = 34i64; let x = 34f32; }
補足として、Rust では変数を再定義できるため、上記のコードは合法です -
各 let 文は新しい変数 x を作成し、以前の変数を隠します。これは、
変数がデフォルトでイミュータブルであるため、予想以上に有用です。
数値リテラルは、10 進数だけでなく、2 進数、8 進数、16 進数としても指定できます。
それぞれ 0b、0o、0x プレフィックスを使用します。数値リテラル内ではどこでも
アンダースコアを使用でき、それは無視されます。例:
fn main() { let x = 12; let x = 0b1100; let x = 0o14; let x = 0xe; let y = 0b_1100_0011_1011_0001; }
Rust には char と string がありますが、それらは Unicode であるため、 C++ とは少し異なります。ポインター、参照、ベクター(配列)を紹介した後で、 それらについて話すことにします。
Rust は数値型を暗黙的に強制変換しません。一般に、Rust は C++ よりも暗黙的な強制変換やサブタイピングがずっと少ないです。Rust は
明示的な強制変換とキャストに as キーワードを使用します。任意の数値は別の数値型にキャストできます。
as は数値型から boolean 型への変換には使用できませんが、
逆は可能です。例:
fn main() { let x = 34usize as isize; // usize を isize にキャスト let x = 10 as f32; // isize から float へ let x = 10.45f64 as i8; // float から i8 へ(精度が失われる) let x = 4u8 as u64; // 精度が増す let x = 400u16 as u8; // 144、精度が失われる(そのため値が変わる) println!("`400u16 as u8` gives {}", x); let x = -3i8 as u8; // 253、符号付きから符号なしへ(符号が変わる) println!("`-3i8 as u8` gives {}", x); //let x = 45 as bool; // 失敗します!(代わりに 45 != 0 を使用) let x = true as usize; // bool を usize にキャスト(1 になる) }
Rust には以下の演算子があります:
| 型 | 演算子 |
|---|---|
| 数値 | +, -, *, /, % |
| ビット単位 | |, &, ^, <<, >> |
| 比較 | ==, !=, >, <, >=, <= |
| 短絡論理 | ||, && |
これらはすべて C++ と同じように振る舞いますが、Rust は演算子を適用できる型について
少し厳格です - ビット単位演算子は整数にのみ適用でき、
論理演算子は boolean にのみ適用できます。Rust には数値を負にする
単項演算子 - があります。! 演算子は boolean を否定し、
整数型では各ビットを反転します(後者の場合は C++ の ~ と同等です)。Rust には C++ と同様に複合代入演算子(たとえば +=)がありますが、
インクリメント演算子やデクリメント演算子(たとえば ++)はありません。
一意ポインター
Rust はシステム言語であるため、メモリへの生のアクセスを提供しなければなりません。 これは(C++ と同様に)ポインターを介して行われます。ポインターは、構文とセマンティクスの両面で Rust と C++ が大きく異なる領域の 1 つです。Rust はポインターを型チェックすることで メモリ安全性を強制します。これは他の言語に対する Rust の大きな利点の 1 つです。 型システムは少し複雑ですが、その見返りとしてメモリ安全性とベアメタル級の性能を得られます。
Rust のポインターをすべて 1 つの記事で扱うつもりでしたが、このテーマは大きすぎると思います。 そのため、この記事では 1 種類、すなわち一意ポインターだけを扱い、その他の種類は後続の記事で扱います。
まず、ポインターを使わない例です。
#![allow(unused)] fn main() { fn foo() { let x = 75; // ... `x` を使って何かを行う ... } }
foo の終わりに到達すると、(C++ と同様に Rust でも)x はスコープを抜けます。
これは、その変数にはもうアクセスできず、その変数用のメモリは再利用できることを意味します。
Rust では、任意の型 T について、T への所有(別名、一意)
ポインターを Box<T> と書けます。Box::new(...) を使うと、ヒープ上に領域を割り当て、
その領域を指定された値で初期化します。これは C++ の new に似ています。
例を示します。
#![allow(unused)] fn main() { fn foo() { let x = Box::new(75); } }
ここで x は、値 75 を含むヒープ上の場所へのポインターです。
x の型は Box<i32> です。let x: Box<i32> = Box::new(75); と書くこともできました。これは C++ で int* x = new int(75); と書くのに似ています。
C++ と異なり、Rust はメモリを自動的に後片付けしてくれるため、
free や delete を呼び出す必要はありません1。一意ポインターは値と同様に振る舞います。
つまり、変数がスコープを抜けると削除されます。この例では、
関数 foo の終わりで x にはもうアクセスできなくなり、x が指していたメモリは
再利用できるようになります。
所有ポインターは C++ と同様に * を使ってデリファレンスします。例を示します。
#![allow(unused)] fn main() { fn foo() { let x = Box::new(75); println!("`x` points to {}", *x); } }
Rust のプリミティブ型と同様に、所有ポインターとそれが指すデータは デフォルトでイミュータブルです。C++ と異なり、イミュータブルなデータへのミュータブルな(一意) ポインターや、その逆を持つことはできません。データのミュータビリティはポインターに従います。 例を示します。
#![allow(unused)] fn main() { fn foo() { let x = Box::new(75); let y = Box::new(42); // x = y; // 許可されません。x はイミュータブルです。 // *x = 43; // 許可されません。*x はイミュータブルです。 let mut x = Box::new(75); x = y; // OK。x はミュータブルです。 *x = 43; // OK。*x はミュータブルです。 } }
所有ポインターは関数から返され、その後も生存し続けることができます。 返された場合、そのメモリは解放されません。つまり、Rust にはダングリングポインターがありません。 メモリがリークすることもありません。ただし、いずれスコープを抜け、そのときに解放されます。 例を示します。
#![allow(unused)] fn main() { fn foo() -> Box<i32> { let x = Box::new(75); x } fn bar() { let y = foo(); // ... y を使う ... } }
ここでは、メモリは foo で初期化され、bar に返されます。x は foo から返されて
y に格納されるため、削除されません。bar の終わりで y がスコープを抜けるため、
メモリは回収されます。
所有ポインターが一意(線形とも呼ばれます)なのは、任意のメモリ領域に対して、 どの時点でも(所有)ポインターは 1 つしか存在できないからです。これは ムーブセマンティクスによって実現されます。あるポインターが値を指すと、それ以前のポインターには もうアクセスできなくなります。例を示します。
#![allow(unused)] fn main() { fn foo() { let x = Box::new(75); let y = x; // x にはもうアクセスできない // let z = *x; // エラー。 } }
同様に、所有ポインターが別の関数に渡されたり、フィールドに格納されたりすると、 それにはもうアクセスできません。
#![allow(unused)] fn main() { fn bar(y: Box<isize>) { } fn foo() { let x = Box::new(75); bar(x); // x にはもうアクセスできない // let z = *x; // エラー。 } }
Rust の一意ポインターは C++ の std::unique_ptr に似ています。Rust では C++ と同様に、
値への一意ポインターは 1 つしか存在できず、その値はポインターがスコープを抜けると削除されます。
Rust はチェックの大部分を実行時ではなく静的に行います。そのため C++ では、
値がムーブされた一意ポインターにアクセスすると(それは null になるため)実行時エラーになります。
Rust ではこれはコンパイル時エラーとなり、実行時に誤ることはありません。
後で見るように、Rust では一意ポインターの値を指す他のポインター型を作成できます。 これは C++ に似ています。しかし C++ では、解放済みメモリへのポインターを保持することで 実行時にエラーを引き起こせます。Rust ではそれは不可能です(Rust の他のポインター型を扱うときに、 どのようにしてそうなるのかを見ます)。
上で示したように、所有ポインターはその値を使うためにデリファレンスする必要があります。
しかし、メソッド呼び出しでは自動的にデリファレンスされるため、メソッド呼び出しに ->
演算子を使ったり * を使ったりする必要はありません。この点で、Rust のポインターは
C++ のポインターと参照の両方に少し似ています。例を示します。
#![allow(unused)] fn main() { fn bar(x: Box<Foo>, y: Box<Box<Box<Box<Foo>>>>) { x.foo(); y.foo(); } }
型 Foo がメソッド foo() を持っていると仮定すると、これらの式はどちらも OK です。
既存の値を指定して Box::new() を呼び出しても、その値への参照を取るのではなく、
その値をコピーします。したがって、
#![allow(unused)] fn main() { fn foo() { let x = 3; let mut y = Box::new(x); *y = 45; println!("x is still {}", x); } }
一般に、Rust はコピーセマンティクスではなくムーブセマンティクスを持ちます
(一意ポインターで上に見たとおりです)。プリミティブ型はコピーセマンティクスを持つため、
上の例では値 3 がコピーされますが、より複雑な値ではムーブされます。
これについては後でより詳しく扱います。
ただし、プログラミングでは値への参照が複数必要になることがあります。 そのために、Rust には借用ポインターがあります。それらについては次の記事で扱います。
-
C++11 で導入された
std::unique_ptr<T>は、いくつかの点で Rust のBox<T>に似ていますが、重要な相違点もあります。類似点:
- C++11 の
std::unique_ptr<T>と Rust のBox<T>が指すメモリは、std::unique_ptr<T>がスコープを抜けると自動的に解放されます。 - C++11 の
std::unique_ptr<T>と Rust のBox<T>はどちらもムーブセマンティクスのみを示します。
相違点:
- C++11 では、既存のポインターから
std::unique_ptr<T>を構築できるため、 同じメモリに対する複数の一意ポインターを許してしまいます。 この振る舞いはBox<T>では許可されません。 - 別の変数や関数にムーブされた
std::unique_ptr<T>をデリファレンスすると、 C++11 では未定義動作を引き起こします。Rust ではこれはコンパイル時に検出されます。 - ミュータビリティまたはイミュータビリティは
std::unique_ptr<T>を「通じて」 伝播しません。つまり、const std::unique_ptr<T>をデリファレンスしても、基になるデータへの ミュータブルな(非constの)参照が得られます。Rust では、イミュータブルなBox<T>はそれが指すデータの変更を許可しません。
Rust の
let x = Box::new(75)は、C++11 ではconst auto x = std::unique_ptr<const int>{new int{75}};、C++14 ではconst auto x = std::make_unique<const int>(75);と解釈できます。 ↩ - C++11 の
借用ポインター
前回の投稿では、一意ポインターを紹介しました。今回は、ほとんどの Rust プログラムではるかに一般的な別の種類のポインター、借用ポインター(別名、借用参照、または単に参照)について説明します。
既存の値への参照を持ちたい場合(一意ポインターのようにヒープ上に新しい値を作成してそれを指すのではなく)、借用参照である & を使用する必要があります。これはおそらく Rust で最も一般的な種類のポインターであり、C++ のポインターや参照の代わりになるもの(たとえば、関数にパラメーターを参照渡しするためのもの)が必要なら、おそらくこれです。
借用参照を作成したり参照型を示したりするには & 演算子を使用し、それらを逆参照するには * を使用します。自動逆参照に関する同じ規則が、一意ポインターの場合と同様に適用されます。例を示します。
#![allow(unused)] fn main() { fn foo() { let x = &3; // 型: &i32 let y = *x; // 3、型: i32 bar(x, *x); bar(&y, y); } fn bar(z: &i32, i: i32) { // ... } }
& 演算子はメモリを割り当てません(既存の値への借用参照しか作成できません)。また、借用参照がスコープを外れても、メモリは削除されません。
借用参照は一意ではありません。同じ値を指す複数の借用参照を持つことができます。たとえば、次のようになります。
#![allow(unused)] fn main() { fn foo() { let x = 5; // 型: i32 let y = &x; // 型: &i32 let z = y; // 型: &i32 let w = y; // 型: &i32 println!("These should all be 5: {} {} {}", *w, *y, *z); } }
値と同様に、借用参照はデフォルトで不変です。&mut を使って可変参照を取得したり、可変参照型を表したりすることもできます。可変の借用参照は一意です(値への可変参照は 1 つしか取得できず、不変参照が存在しない場合にのみ可変参照を持つことができます)。不変参照が求められている場所で可変参照を使用することはできますが、その逆はできません。これらをすべてまとめた例を示します。
#![allow(unused)] fn main() { fn bar(x: &i32) { ... } fn bar_mut(x: &mut i32) { ... } // &mut i32 は、変更可能な i32 への // 参照です fn foo() { let x = 5; //let xr = &mut x; // エラー - 不変変数への可変参照は // 作成できない let xr = &x; // OK(不変参照を作成) bar(xr); //bar_mut(xr); // エラー - 可変参照を期待している let mut x = 5; let xr = &x; // OK(不変参照を作成) //*xr = 4; // エラー - 不変参照を変更している //let xr = &mut x; // エラー - すでに不変参照があるため、 // 可変参照は作成できない let mut x = 5; let xr = &mut x; // OK(可変参照を作成) *xr = 4; // OK //let xr2 = &x; // エラー - すでに可変参照があるため、 // 不変参照は作成できない //let xr2 = &mut x; // エラー - 可変参照は一度に 1 つしか持てない bar(xr); // OK bar_mut(xr); // OK } }
参照が可変かどうかは、その参照を保持している変数の可変性とは独立している場合があることに注意してください。これは、ポインターが指すデータとは独立して、ポインターを const にできる(またはできない)C++ と似ています。これは、ポインターの可変性がデータの可変性に結び付いている一意ポインターとは対照的です。例を示します。
#![allow(unused)] fn main() { fn foo() { let mut x = 5; let mut y = 6; let xr = &mut x; //xr = &mut y; // エラー xr は不変 let mut x = 5; let mut y = 6; let mut xr = &mut x; xr = &mut y; // OK let x = 5; let y = 6; let mut xr = &x; xr = &y; // OK - 参照先のデータは可変ではないが、xr は mut } }
可変値が借用されると、借用の間は不変になります。借用ポインターがスコープを外れると、その値は再び変更できるようになります。これは、一度ムーブされると二度と使用できない一意ポインターとは対照的です。例を示します。
#![allow(unused)] fn main() { fn foo() { let mut x = 5; // 型: i32 { let y = &x; // 型: &i32 //x = 4; // エラー - x は借用されている println!("{} {}", y, x); // OK - x は読み取り可能 } x = 4; // OK - y はもはや存在しない } }
値への可変参照を取得した場合も、同じことが起こります。その値は依然として変更できません。一般に Rust では、データは常に 1 つの変数またはポインターを介してのみ変更できます。さらに、可変参照を持っているため、不変参照を取得することはできません。これにより、基になる値の使い方が制限されます。
#![allow(unused)] fn main() { fn foo() { let mut x = 5; // 型: i32 { let y = &mut x; // 型: &mut i32 //x = 4; // エラー - x は借用されている //println!("{}", x); // エラー - x の借用が必要 } x = 4; // OK - y はもはや存在しない } }
C++ とは異なり、Rust は自動的に値への参照を作ってはくれません。そのため、関数が参照でパラメーターを受け取る場合、呼び出し元は実引数への参照を渡さなければなりません。ただし、ポインター型は自動的に参照へ変換されます。
#![allow(unused)] fn main() { fn foo(x: &i32) { ... } fn bar(x: i32, y: Box<i32>) { foo(&x); // foo(x); // エラー - &i32 が期待されたが、i32 が見つかった foo(y); // OK foo(&*y); // これも OK で、より明示的だが、良いスタイルではない } }
mut と const
この段階で、Rust の mut と C++ の const を比較しておく価値がおそらくあります。表面的には、これらは正反対です。Rust では値はデフォルトで不変であり、mut を使うことで可変にできます。C++ では値はデフォルトで可変ですが、const を使うことで定数にできます。より微妙で重要な違いは、C++ の const 性は値の現在の使用にのみ適用されるのに対し、Rust の不変性は値のすべての使用に適用されるという点です。つまり、C++ で私が const 変数を持っている場合、他の誰かがそれへの非 const 参照を持っていて、私が知らないうちにそれが変化する可能性があります。Rust では、不変変数を持っている場合、それが変化しないことが保証されます。
上で述べたように、すべての可変変数は一意です。そのため、可変値を持っている場合、自分が変更しない限りそれが変化しないことがわかります。さらに、それが変化しないことに他の誰も依存していないとわかっているため、自由に変更できます。
借用とライフタイム
Rust の主要な安全性目標の 1 つは、ダングリングポインター(ポインターが指すメモリよりも長く生存してしまうポインター)を避けることです。Rust では、ダングリングした借用参照を持つことは不可能です。参照よりも長く生存する(まあ、少なくとも参照と同じだけ長く生存する)メモリへの借用参照を作成する場合にのみ合法です。言い換えると、参照のライフタイムは参照先の値のライフタイムより短くなければなりません。
これは、この投稿のすべての例で実現されています。{} や関数によって導入される
スコープはライフタイムの境界です。変数がスコープ外に出ると、そのライフタイムは
終わります。より狭いスコープ内など、より短いライフタイムへの参照を取ろうとすると、
コンパイラはエラーを出します。たとえば、
#![allow(unused)] fn main() { fn foo() { let x = 5; let mut xr = &x; // OK - x と xr は同じライフタイムを持つ { let y = 6; xr = &y // エラー - xr は y より長く生存する } // y はここで解放される println!("{:?}", xr); // xr はここで使用されるため、y より長く生存する。この行をコメントアウトしてみてください。 } // x と xr はここで解放される }
上の例では、xr と y は同じライフタイムを持ちません。なぜなら、y は xr より後に 始まるからです。しかし、より興味深いのはライフタイムの終わりのほうです。いずれにせよ、 変数が存在する前にその変数を参照することはできないからです。これは Rust が強制する もう 1 つのことであり、Rust を C++ より安全にしているものです。
明示的なライフタイム
借用ポインタをしばらく扱っていると、おそらく明示的なライフタイムを持つ借用ポインタに
出会うでしょう。これらは &'a T(参照
&T)という構文を持ちます。ライフタイムポリモーフィズムも同時に扱う必要があるため、
これはかなり大きなトピックです。そのため、別の投稿に回すことにします(ただし、その前に
扱うべき、もう少し一般的でないポインタ型がいくつかあります)。今のところは、&T は
&'a T の省略形であり、ここで a は現在のスコープ、つまりその型が宣言されている
スコープである、ということだけを述べておきます。
参照カウントポインターと raw ポインター
TODO カスタムポインターと Deref トレイトの議論を追加する(たぶん後で、ここではない)
ここまで、一意ポインターと借用ポインターについて扱ってきました。一意ポインターは C++ の新しい std::unique_ptr と非常によく似ており、借用参照は、C++ でポインターや参照を使う場面で通常まず手に取る「デフォルト」のポインターです。Rust には、ライブラリ内または言語組み込みとして、もう少し多くの、よりまれなポインターがあります。これらはおおむね、C++ でおなじみかもしれないさまざまな種類のスマートポインターに似ています。
この投稿を書くのには時間がかかりましたし、今でも気に入っていません。ここには、私の説明にも Rust 自体にも、多くの未整理な部分があります。後の投稿でいくつかは改善され、言語の発展に伴っていくつかは改善されることを願っています。Rust を学んでいるなら、今のところこの内容は飛ばしてもよいかもしれません。おそらく必要にはならないでしょう。これは他のポインター型に関する投稿の後で、完全性のためにここにあるだけです。
Rust には多くのポインター型があるように感じるかもしれませんが、ライブラリで利用できるさまざまなスマートポインターについて考えれば、C++ とかなり似ています。ただし Rust では、言語を学び始めたときにそれらに出会う可能性が高くなります。また、Rust のポインターはコンパイラのサポートを受けているため、それらを使うときにエラーを起こす可能性もはるかに低くなります。
これらについては、一意参照や借用参照ほど詳しくは扱いません。率直に言って、それほど重要ではないからです。後でより詳しく戻ってくるかもしれません。
Rc
参照カウントポインターは Rust 標準ライブラリの一部として提供されます。これらは std::rc モジュールにあります(モジュールについては近いうちに扱います。例に出てくる use という呪文の理由はモジュールにあります)。型 T のオブジェクトへの参照カウントポインターの型は Rc<T> です。参照カウントポインターは、静的メソッド(今のところは C++ のもののように考えてよいですが、後で少し違うことが分かります)である Rc::new(...) を使って作成します。これはポインターの指し先となる値を受け取ります。このコンストラクタメソッドは Rust の通常の move/copy セマンティクス(一意ポインターについて議論したものと同様)に従います。いずれの場合も、Rc::new を呼び出した後は、その値にはポインターを介してしかアクセスできなくなります。
他のポインター型と同様に、. 演算子は必要な参照外しをすべて行います。手動で参照外しするには * を使えます。
参照カウントポインターを渡すには、clone メソッドを使う必要があります。これは少し残念で、いずれ修正できるとよいのですが、確実ではありません(残念ながら)。指し先の値への(借用)参照を取ることができるので、あまり頻繁に clone しなくて済むことを期待できます。Rust の型システムは、参照が期限切れになる前に参照カウントされた変数が削除されないことを保証します。参照を取ることには、参照カウントをインクリメントまたはデクリメントする必要がないという追加の利点があり、そのため性能が向上します(ただし、Rc オブジェクトは単一スレッドに制限されているため、参照カウント操作はアトミックである必要がなく、その差はおそらくわずかです)。C++ と同様に、Rc ポインターへの参照を取ることもできます。
Rc の例:
#![allow(unused)] fn main() { use std::rc::Rc; fn bar(x: Rc<i32>) { } fn baz(x: &i32) { } fn foo() { let x = Rc::new(45); bar(x.clone()); // 参照カウントをインクリメントする baz(&*x); // インクリメントしない println!("{}", 100 - *x); } // このスコープが閉じると、すべての Rc ポインターがなくなるため、ref-count == 0 // となり、メモリが削除される。 }
参照カウントポインターは常に不変です。可変な参照カウントオブジェクトが必要な場合は、Rc でラップした RefCell(または Cell)を使う必要があります。
Cell と RefCell
Cell と RefCell は、可変性のルールを「ごまかす」ことを可能にする構造体です。これは、Rust のデータ構造と、それらが可変性とどのように連携するかを先に扱わないと説明が少し難しいため、これらの少し扱いにくいオブジェクトについては後で戻ってきます。今のところ、可変で参照カウントされたオブジェクトが必要な場合は、Rc でラップした Cell または RefCell が必要であることを知っておいてください。第一近似としては、プリミティブデータには Cell、move セマンティクスを持つオブジェクトには RefCell が必要になるでしょう。したがって、可変で参照カウントされた int には Rc<Cell<int>> を使うことになります。
*T - raw ポインター
最後に、Rust には 2 種類の raw ポインター(別名 unsafe ポインター)があります。不変 raw ポインター用の *const T と、可変 raw ポインター用の *mut T です。これらは & または &mut を使って作成されます(& 演算子は借用参照と raw ポインターのどちらも作成できるため、&T ではなく *T を得るには型を指定する必要があるかもしれません)。raw ポインターは C のポインターに似ており、どのように使われるかについて制限のない、単なるメモリへのポインターです(キャストなしにポインター演算はできませんが、必要であればその方法で行えます)。raw ポインターは、Rust で null になり得る唯一のポインター型です。raw ポインターには自動参照外しはありません(そのためメソッドを呼び出すには (*x).foo() と書く必要があります)し、自動参照もありません。最も重要な制限は、unsafe ブロックの外では参照外しできない(したがって使用できない)ことです。通常の Rust コードでは、それらを受け渡しすることしかできません。
では、unsafe コードとは何でしょうか。Rust には強力な安全性の保証があり、それによって(まれに)必要なことができなくなる場合があります。Rust はシステム言語であることを目指しているため、可能なことは何でもできなければならず、ときにはそれが、コンパイラが安全だと検証できないことを行うことを意味します。それを実現するために、Rust には unsafe キーワードで示される unsafe ブロックという概念があります。unsafe コードでは、raw ポインターを参照外しする、境界チェックなしで配列にインデックスアクセスする、FFI 経由で別の言語で書かれたコードを呼び出す、変数をキャストする、といった unsafe なことができます。明らかに、unsafe コードを書くときは通常の Rust コードを書くときよりもはるかに注意する必要があります。実際、unsafe コードを書くべき場面はごくまれです。多くの場合、クライアントコードではなくライブラリ内の非常に小さな部分で使われます。unsafe コードでは、安全性を確保するために通常 C++ で行うすべてのことを行わなければなりません。さらに、コンパイラが通常強制する不変条件を維持していることを手動で保証しなければなりません。unsafe ブロックは、Rust の不変条件を手動で強制できるようにするものであり、それらの不変条件を破ることを許可するものではありません。もし破れば、安全なコードと unsafe なコードの両方にバグを持ち込む可能性があります。
raw ポインターを使う例:
#![allow(unused)] fn main() { fn foo() { let mut x = 5; let x_p: *mut i32 = &mut x; println!("x+5={}", add_5(x_p)); } fn add_5(p: *mut i32) -> i32 { unsafe { if !p.is_null() { // *ポインターは自動 deref しないことに注意。そのためこれは // i32 ではなく *i32 に実装されたメソッドである。 *p + 5 } else { -1 // 推奨されるエラーハンドリング戦略ではない。 } } } }
これで Rust のポインターのツアーは終わりです。次回はポインターから少し離れて、Rust のデータ構造を見ていきます。ただし、借用参照については後の投稿で再び戻ってきます。
データ型
この記事では Rust のデータ型について説明します。これらはおおよそ C++ の
クラス、構造体、列挙型に相当します。Rust と C++ の違いの 1 つは、Rust では
データと振る舞いが C++(または Java、その他の
オブジェクト指向言語)よりもはるかに厳密に分離されていることです。振る舞いは関数によって定義され、それらは
トレイトや impl(実装)で定義できますが、トレイトはデータを含むことができず、
その点では Java のインターフェイスに似ています。トレイトと impl については後の記事で扱います。この記事はデータについてのものです。
構造体
Rust の構造体は、C の構造体やメソッドを持たない C++ の構造体に似ています。単に 名前付きフィールドのリストです。構文は例で見るのが最もわかりやすいでしょう。
#![allow(unused)] fn main() { struct S { field1: i32, field2: SomeOtherStruct } }
ここでは、2 つのフィールドを持つ S という構造体を定義しています。フィールドはカンマで
区切られます。好みに応じて、最後のフィールドの末尾にもカンマを付けることができます。
構造体は型を導入します。この例では、S を型として使用できます。
SomeOtherStruct は別の構造体(この例では型として使用されている)であると仮定しており、
(C++ と同様に)値として含まれます。つまり、メモリ内の別の構造体オブジェクトへのポインタは
存在しません。
構造体のフィールドには、. 演算子とフィールド名を使ってアクセスします。構造体を使用する例を示します。
#![allow(unused)] fn main() { fn foo(s1: S, s2: &S) { let f = s1.field1; if f == s2.field1 { println!("field1 matches!"); } } }
ここで s1 は値渡しされた構造体オブジェクトで、s2 は参照渡しされた構造体オブジェクトです。
メソッド呼び出しの場合と同様に、どちらのフィールドにアクセスする場合も同じ . を使用し、
-> は必要ありません。
構造体は構造体リテラルを使って初期化します。構造体リテラルは構造体名と 各フィールドの値で構成されます。たとえば、
#![allow(unused)] fn main() { fn foo(sos: SomeOtherStruct) { let x = S { field1: 45, field2: sos }; // 構造体リテラルで x を初期化する println!("x.field1 = {}", x.field1); } }
構造体を再帰的にすることはできません。つまり、定義とフィールド型に関わる構造体名の循環を
持つことはできません。これは構造体の値セマンティクスによるものです。
したがって、たとえば struct R { r: Option<R> } は不正であり、
コンパイラエラーになります(Option については後述します)。このような構造が必要な場合は、
何らかのポインタを使用するべきです。ポインタを使った循環は許可されています。
#![allow(unused)] fn main() { struct R { r: Option<Box<R>> } }
上の構造体に Option がなければ、その構造体をインスタンス化する方法がなく、
Rust はエラーを通知します。
フィールドを持たない構造体では、定義でもリテラルでの使用でも波括弧を使いません。 ただし、定義には終端のセミコロンが必要です。おそらくこれは単に パースを容易にするためです。
#![allow(unused)] fn main() { struct Empty; fn foo() { let e = Empty; } }
タプル
タプルは、匿名で異種混在のデータ列です。型としては、
括弧内に型の列として宣言されます。名前がないため、構造によって識別されます。
たとえば、型 (i32, i32) は整数のペアであり、(i32, f32, S) は 3 要素の組です。
タプル値はタプル型の宣言と同じ方法で初期化されますが、
構成要素には型ではなく値を使います。たとえば (4, 5) です。例を示します。
#![allow(unused)] fn main() { // foo は構造体を受け取り、タプルを返す fn foo(x: SomeOtherStruct) -> (i32, f32, S) { (23, 45.82, S { field1: 54, field2: x }) } }
タプルは let 式による分配束縛を使って利用できます。たとえば、
#![allow(unused)] fn main() { fn bar(x: (i32, i32)) { let (a, b) = x; println!("x was ({}, {})", a, b); } }
分配束縛については次回さらに説明します。
タプル構造体
タプル構造体は名前付きタプル、あるいは別の言い方をすれば、名前のないフィールドを持つ構造体です。
これらは struct キーワード、括弧内の型のリスト、
そしてセミコロンを使って宣言されます。このような宣言は、その名前を型として導入します。
フィールドには名前ではなく、(タプルのように)分配束縛によってアクセスする必要があります。
タプル構造体はあまり一般的ではありません。
#![allow(unused)] fn main() { struct IntPoint (i32, i32); fn foo(x: IntPoint) { let IntPoint(a, b) = x; // タプルを分配束縛するには、その名前が必要であることに注意 // 構造体を分配束縛する。 println!("x was ({}, {})", a, b); } }
列挙型
列挙型は C++ の列挙型や共用体のような型で、複数の値を取り得る型です。 最も単純な種類の列挙型は、C++ の列挙型とまったく同じようなものです。
#![allow(unused)] fn main() { enum E1 { Var1, Var2, Var3 } fn foo() { let x: E1 = Var2; match x { Var2 => println!("var2"), _ => {} } } }
しかし、Rust の列挙型はそれよりもはるかに強力です。各バリアントは データを含むことができます。タプルと同様に、これらは型のリストによって定義されます。この場合、 C++ の列挙型というより共用体に近いものです。Rust の列挙型は、タグなし共用体(C++ におけるもの)ではなくタグ付き共用体です。 つまり、実行時に列挙型のあるバリアントを別のバリアントと取り違えることはありません1。例を示します。
#![allow(unused)] fn main() { enum Expr { Add(i32, i32), Or(bool, bool), Lit(i32) } fn foo() { let x = Or(true, false); // x の型は Expr } }
オブジェクト指向のポリモーフィズムの単純なケースの多くは、Rust では 列挙型を使う方がうまく扱えます。
列挙型を使うには、通常 match 式を使用します。これらは C++ の switch 文に似ていることを思い出してください。 これらや、データを分配束縛する他の方法については次回さらに詳しく説明します。例を示します。
#![allow(unused)] fn main() { fn bar(e: Expr) { match e { Add(x, y) => println!("An `Add` variant: {} + {}", x, y), Or(..) => println!("An `Or` variant"), _ => println!("Something else (in this case, a `Lit`)"), } } }
match 式の各アームは Expr のバリアントにマッチします。すべてのバリアントを
網羅しなければなりません。最後のケース(_)は残りのすべてのバリアントを扱いますが、この
例では Lit だけです。バリアント内の任意のデータは変数に束縛できます。
Add アームでは、Add 内の 2 つの i32 を x と y に束縛しています。
データに関心がない場合は、Or で行っているように .. を使って任意のデータにマッチできます。
Option
Rust で特に一般的な列挙型の 1 つが Option です。これには Some と
None という 2 つのバリアントがあります。None はデータを持たず、Some は型 T の単一のフィールドを持ちます
(Option はジェネリックな列挙型で、これについては後で扱いますが、
一般的な考え方は C++ から理解できるはずです)。Option は、値が
存在するかもしれないし、存在しないかもしれないことを示すために使われます。C++ で null ポインタを使う任意の場所2、
つまり何らかの形で未定義、未初期化、または false である値を示すために使う場所では、
Rust ではおそらく Option を使うべきです。Option を使う方が安全なのは、
使用前に必ず確認しなければならないためです。null ポインタをデリファレンスするのと同等のことを行う方法はありません。
また、Option はより汎用的であり、ポインタだけでなく値にも使用できます。例を示します。
#![allow(unused)] fn main() { use std::rc::Rc; struct Node { parent: Option<Rc<Node>>, value: i32 } fn is_root(node: Node) -> bool { match node.parent { Some(_) => false, None => true } } }
ここで、parent フィールドは None か、Rc<Node> を含む Some のどちらかです。
この例では、そのペイロードを実際には使用していませんが、実際の場面では
通常は使用するでしょう。
Option には便利なメソッドもあるため、is_root の本体を
node.parent.is_none() または !node.parent.is_some() と書くこともできます。
継承された可変性と Cell/RefCell
Rust のローカル変数はデフォルトではイミュータブルであり、mut を使ってミュータブルとしてマークできます。構造体や列挙型のフィールドをミュータブルとしてマークすることはありません。それらのミュータビリティは継承されます。これは、構造体オブジェクト内のフィールドがミュータブルかイミュータブルかは、そのオブジェクト自体がミュータブルかイミュータブルかによって決まることを意味します。例:
struct S1 { field1: i32, field2: S2 } struct S2 { field: i32 } fn main() { let s = S1 { field1: 45, field2: S2 { field: 23 } }; // s は深くイミュータブルであり、以下の変更は禁止される // s.field1 = 46; // s.field2.field = 24; let mut s = S1 { field1: 45, field2: S2 { field: 23 } }; // s はミュータブルであり、これらは OK s.field1 = 46; s.field2.field = 24; }
Rust における継承されたミュータビリティは、参照で止まります。これは C++ と似ており、const オブジェクトからのポインタを介して非 const オブジェクトを変更できます。参照フィールドをミュータブルにしたい場合は、そのフィールド型に &mut を使う必要があります:
struct S1 { f: i32 } struct S2<'a> { f: &'a mut S1 // ミュータブルな参照フィールド } struct S3<'a> { f: &'a S1 // イミュータブルな参照フィールド } fn main() { let mut s1 = S1{f:56}; let s2 = S2 { f: &mut s1}; s2.f.f = 45; // s2 がイミュータブルであっても合法 // s2.f = &mut s1; // 不正 - s2 はミュータブルではない let s1 = S1{f:56}; let mut s3 = S3 { f: &s1}; s3.f = &s1; // 合法 - s3 はミュータブル // s3.f.f = 45; // 不正 - s3.f はイミュータブル }
(S2 と S3 の 'a パラメータはライフタイムパラメータです。これについてはすぐに扱います)。
オブジェクトは論理的にはイミュータブルであっても、内部的にミュータブルである必要がある部分を持つことがあります。さまざまな種類のキャッシュや参照カウントを考えてみてください(参照カウントの変更の影響はデストラクタを通じて観測できるため、真の論理的イミュータビリティは得られません)。C++ では、オブジェクトが const であってもそのような変更を許可するために mutable キーワードを使います。Rust には Cell 構造体と RefCell 構造体があります。これらにより、イミュータブルなオブジェクトの一部を変更できます。これは便利ですが、Rust でイミュータブルなオブジェクトを見たとき、その一部が実際にはミュータブルである可能性があることを認識しておく必要がある、ということでもあります。
RefCell と Cell は、Rust の変更とエイリアス可能性に関する厳格なルールを回避できるようにします。これらが安全に使えるのは、コンパイラがそれらの不変条件が静的に成り立つことを保証できない場合でも、Rust の不変条件が動的に尊重されることを保証するためです。Cell と RefCell はどちらもシングルスレッドのオブジェクトです。
コピーセマンティクスを持つ型(ほぼプリミティブ型だけ)には Cell を使います。Cell には、格納された値を変更するための get メソッドと set メソッド、および値でセルを初期化するための new メソッドがあります。Cell は非常に単純なオブジェクトです。コピーセマンティクスを持つオブジェクトは(Rust では)他の場所への参照を保持できず、スレッド間で共有することもできないため、問題が起きる余地があまりなく、賢いことをする必要がありません。
ムーブセマンティクスを持つ型には RefCell を使います。これは Rust のほぼすべてを意味し、構造体オブジェクトが一般的な例です。RefCell も new を使って作成され、set メソッドを持ちます。RefCell 内の値を取得するには、borrow メソッド(borrow, borrow_mut, try_borrow, try_borrow_mut)を使って借用しなければなりません。これらは RefCell 内のオブジェクトへの借用参照を返します。これらのメソッドは静的な借用と同じルールに従います。つまり、ミュータブルな借用は 1 つだけしか持てず、同時にミュータブルにもイミュータブルにも借用することはできません。ただし、コンパイルエラーではなく、実行時の失敗になります。try_ バリアントは Option を返します。値を借用できる場合は Some(val) を、できない場合は None を得ます。値が借用されている場合、set の呼び出しも失敗します。
RefCell への参照カウント付きポインタを使った例を示します(一般的なユースケースです):
use std::rc::Rc; use std::cell::RefCell; struct S { field: i32 } fn foo(x: Rc<RefCell<S>>) { { let s = x.borrow(); println!("the field, twice {} {}", s.field, x.borrow().field); // let s = x.borrow_mut(); // エラー - x の内容はすでに借用している } let mut s = x.borrow_mut(); // OK、以前の借用はスコープ外 s.field = 45; // println!("The field {}", x.borrow().field); // エラー - ミュータブル借用とイミュータブル借用は同時にできない println!("The field {}", s.field); } fn main() { let s = S{field:12}; let x: Rc<RefCell<S>> = Rc::new(RefCell::new(s)); foo(x.clone()); println!("The field {}", x.borrow().field); }
Cell/RefCell を使う場合は、できるだけ小さいオブジェクトに配置するようにするべきです。つまり、構造体全体ではなく、構造体のいくつかのフィールドに配置することを好んでください。これらをシングルスレッドのロックのように考えてください。より細粒度のロックのほうが、ロックの衝突を避けられる可能性が高いため、より良いです。
-
C++17 には、union よりも Rust の列挙型に近い
std::variant<T>型があります。 ↩ -
C++17 以降では、
std::optional<T>が Rust の Option の最良の代替です。 ↩
分解
前回は Rust のデータ型について見ました。構造体の中に何らかのデータがあるなら、そのデータを取り出したくなるでしょう。構造体については、Rust には C++ と同様にフィールドアクセスがあります。タプル、タプル構造体、列挙型については、分解を使わなければなりません(ライブラリにはさまざまな便利関数がありますが、それらは内部的に分解を使っています)。データ構造の分解は C++ では C++17 以降にしか存在しないため、おそらく Python やさまざまな関数型言語のような言語でおなじみでしょう。その考え方は、ローカル変数のまとまりからデータをフィールドに埋めてデータ構造を初期化できるのと同じように、データ構造からデータを取り出してローカル変数のまとまりを埋めることができる、というものです。この単純な出発点から、分解は Rust の最も強力な機能の 1 つになりました。別の言い方をすると、分解はパターンマッチングとローカル変数への代入を組み合わせたものです。
分解は主に let 文と match 文を通じて行います。match 文は、分解される構造が(列挙型のように)異なるバリアントを持ち得る場合に使います。let 式は変数を現在のスコープに取り出すのに対し、match は新しいスコープを導入します。比較すると次のようになります。
#![allow(unused)] fn main() { fn foo(pair: (int, int)) { let (x, y) = pair; // これで foo のどこでも x と y を使える match pair { (x, y) => { // x と y はこのスコープ内でのみ使える } } } }
どちらの場合も、パターン(上の例で let の後、=> の前に使われているもの)の構文は(ほぼ)同じです。これらのパターンは、関数宣言の引数位置でも使えます。
#![allow(unused)] fn main() { fn foo((x, y): (int, int)) { } }
(これはタプルよりも、構造体やタプル構造体でより有用です)。
ほとんどの初期化式は分解パターンの中に現れることができ、任意に複雑にできます。これには参照やプリミティブリテラルだけでなく、データ構造も含められます。たとえば次のとおりです。
#![allow(unused)] fn main() { struct St { f1: int, f2: f32 } enum En { Var1, Var2, Var3(int), Var4(int, St, int) } fn foo(x: &En) { match x { &Var1 => println!("最初のバリアント"), &Var3(5) => println!("数値 5 を持つ 3 番目のバリアント"), &Var3(x) => println!("数値 {}(5 ではない)を持つ 3 番目のバリアント", x), &Var4(3, St { f1: 3, f2: x }, 45) => { println!("埋め込まれた構造体を分解し、f2 に {} が見つかった", x) } &Var4(_, ref x, _) => { println!("f1 に {}、f2 に {} を持つ別の Var4", x.f1, x.f2) } _ => println!("その他 (Var2)") } } }
パターンの中で & を使うことで参照を通して分解していること、またリテラル(5、3、St { ... })、ワイルドカード(_)、変数(x)を混在させて使っていることに注目してください。
パターン内の単一の項目を無視したい場合、変数が期待される場所ならどこでも _ を使えます。そのため、整数に関心がなければ &Var3(_) を使うこともできました。最初の Var4 アームでは埋め込まれた構造体を分解しており(ネストしたパターン)、2 番目の Var4 アームでは構造体全体を変数に束縛しています。タプルや構造体のすべてのフィールドを表すものとして .. を使うこともできます。したがって、列挙型の各バリアントごとに何かをしたいが、バリアントの内容には関心がない場合は、次のように書けます。
#![allow(unused)] fn main() { fn foo(x: En) { match x { Var1 => println!("最初のバリアント"), Var2 => println!("2 番目のバリアント"), Var3(..) => println!("3 番目のバリアント"), Var4(..) => println!("4 番目のバリアント") } } }
構造体を分解するとき、フィールドは順番どおりである必要はなく、残りのフィールドを省くために .. を使えます。たとえば次のとおりです。
#![allow(unused)] fn main() { struct Big { field1: int, field2: int, field3: int, field4: int, field5: int, field6: int, field7: int, field8: int, field9: int, } fn foo(b: Big) { let Big { field6: x, field3: y, ..} = b; println!("{} と {} を取り出した", x, y); } }
構造体では省略記法として、フィールド名だけを使うことができ、その名前を持つローカル変数が作られます。上の例の let 文は、x と y という 2 つの新しいローカル変数を作りました。代わりに、次のように書くこともできます。
#![allow(unused)] fn main() { fn foo(b: Big) { let Big { field6, field3, .. } = b; println!("{} と {} を取り出した", field3, field6); } }
これで、フィールドと同じ名前を持つローカル変数が作られます。この場合は field3 と field6 です。
Rust の分解には、さらにいくつかの技巧があります。パターン内の変数への参照が欲しいとしましょう。& は参照を作るのではなく参照にマッチする(したがってオブジェクトをデリファレンスする効果を持つ)ため、使えません。たとえば次のとおりです。
#![allow(unused)] fn main() { struct Foo { field: &'static int } fn foo(x: Foo) { let Foo { field: &y } = x; } }
ここで、y の型は int であり、x のフィールドのコピーです。
パターン内の何かへの参照を作るには、ref キーワードを使います。たとえば次のとおりです。
#![allow(unused)] fn main() { fn foo(b: Big) { let Big { field3: ref x, ref field6, ..} = b; println!("{} と {} を取り出した", *x, *field6); } }
ここで、x と field6 はどちらも型 &int を持ち、b 内のフィールドへの参照です。
分解に関する最後の技巧として、複雑なオブジェクトを分解している場合、個々のフィールドだけでなく中間のオブジェクトにも名前を付けたいことがあります。以前の例に戻ると、&Var4(3, St{ f1: 3, f2: x }, 45) というパターンがありました。このパターンでは構造体の 1 つのフィールドに名前を付けましたが、構造体オブジェクト全体にも名前を付けたいかもしれません。&Var4(3, s, 45) と書けば構造体オブジェクトを s に束縛できますが、その場合、フィールドにはフィールドアクセスを使う必要があります。あるいは、フィールド内の特定の値とだけマッチさせたい場合は、ネストした match を使わなければならないでしょう。それは楽しくありません。Rust では @ 構文を使ってパターンの一部に名前を付けられます。たとえば &Var4(3, s @ St{ f1: 3, f2: x }, 45) と書くと、フィールド(f2 に対する x)と構造体全体(s)の両方に名前を付けられます。
これで Rust のパターンマッチングで使える選択肢はほぼ網羅できました。ベクターのマッチングなど、ここで扱っていない機能はいくつかありますが、match と let の使い方や、それらでできる強力なことの一端を理解してもらえたなら幸いです。次回は、Rust を学んでいるときにかなりつまずいた、match と借用の間にある微妙な相互作用について取り上げます。
分解 pt2 - match と借用
分解を行うとき、借用に関してはいくつか思いがけないことがあります。借用参照を本当に十分理解していれば、驚くことは何もないはずですが、議論する価値はあります(私が理解するまでにはしばらくかかったことは確かです。実際、このブログ記事の最初のバージョンで間違えていたので、自分で思っていたよりも長くかかっていました)。
何らかの &Enum 型の変数 x があるとします(ここで Enum は何らかの enum 型です)。選択肢は 2 つあります。*x を match してすべてのバリアントを列挙する(Variant1 => ... など)か、x を match してバリアントパターンへの参照を列挙する(&Variant1 => ... など)かです。(スタイルとしては、構文上のノイズが少ないため、可能な場合は前者の形式を好むべきです)。x は借用参照であり、借用参照をどのようにデリファレンスできるかには厳格なルールがあります。これらは match 式と、思いがけない形で相互作用します(少なくとも私にとっては思いがけないものでした)。特に、既存の enum を一見無害に見える方法で変更したところ、どこかの match でコンパイラが爆発するような場合です。
match 式の詳細に入る前に、Rust の値渡しに関するルールを振り返っておきましょう。C++ では、値を変数に代入したり関数に渡したりするとき、選択肢は 2 つあります。値渡しと参照渡しです。前者がデフォルトのケースであり、コピーコンストラクタまたはビット単位のコピーを使って値がコピーされることを意味します。パラメータ渡しや代入の宛先に & を付けると、値は参照渡しされます。つまり、値へのポインタだけがコピーされ、新しい変数を操作すると、古い値も操作していることになります。
Rust にも参照渡しの選択肢がありますが、Rust では宛先だけでなくソースにも & を付ける必要があります。Rust の値渡しには、さらに 2 つの選択肢があります。コピーとムーブです。コピーは C++ のセマンティクスと同じです(ただし Rust にはコピーコンストラクタはありません)。ムーブは値をコピーしますが、古い値を破棄します。Rust の型システムにより、古い値にはもはやアクセスできないことが保証されます。例として、i32 はコピーセマンティクスを持ち、Box<i32> はムーブセマンティクスを持ちます。
#![allow(unused)] fn main() { fn foo() { let x = 7i32; let y = x; // x はコピーされる println!("x is {}", x); // OK let x = Box::new(7i32); let y = x; // x はムーブされる //println!("x is {}", x); // エラー: ムーブされた値 `x` の使用 } }
ユーザー定義型についても、Copy トレイトを実装することでコピーセマンティクスを持たせることができます。そのための単純な方法の 1 つは、struct の定義の前に #[derive(Copy)] を追加することです。すべてのユーザー定義型が Copy トレイトを実装できるわけではありません。型のすべてのフィールドが Copy を実装していなければならず、その型はデストラクタを持っていてはいけません。デストラクタについては、おそらくそれ自体で 1 つの記事が必要ですが、今のところ、Rust のオブジェクトは Drop トレイトを実装している場合にデストラクタを持つ、としておきます。C++ と同様に、デストラクタはオブジェクトが破棄される直前に実行されます。
さて、借用されたオブジェクトがムーブされないことは重要です。そうでないと、もはや有効ではない古いオブジェクトへの参照を持つことになります。これは、スコープを抜けた後に破棄されたオブジェクトへの参照を保持することと同じであり、一種のダングリングポインタです。あるオブジェクトへのポインタを持っている場合、そのオブジェクトには他の参照が存在する可能性があります。したがって、オブジェクトがムーブセマンティクスを持ち、そのオブジェクトへのポインタを持っている場合、そのポインタをデリファレンスするのは安全ではありません。(オブジェクトがコピーセマンティクスを持つ場合、デリファレンスによりコピーが作成され、古いオブジェクトもまだ存在するため、他の参照は問題ありません)。
では、match 式に戻りましょう。先ほど述べたように、&T 型の何らかの x を match したい場合、match 節で一度デリファレンスするか、match 式の各アームで参照を match することができます。例を示します。
#![allow(unused)] fn main() { enum Enum1 { Var1, Var2, Var3 } fn foo(x: &Enum1) { match *x { // 選択肢 1: ここで deref する。 Enum1::Var1 => {} Enum1::Var2 => {} Enum1::Var3 => {} } match x { // 選択肢 2: 各アームで 'deref' する。 &Enum1::Var1 => {} &Enum1::Var2 => {} &Enum1::Var3 => {} } } }
この場合、Enum1 はコピーセマンティクスを持つため、どちらのアプローチも取ることができます。それぞれのアプローチをもう少し詳しく見てみましょう。最初のアプローチでは、x を Enum1 型の一時変数へデリファレンスし(これにより x 内の値がコピーされます)、それから Enum1 の 3 つのバリアントに対して match を行います。これは「1 レベル」の match です。値の型の奥深くまでは入らないからです。2 つ目のアプローチでは、デリファレンスはありません。&Enum1 型の値を、各バリアントへの参照と match します。この match は 2 レベル深く進みます。型(常に参照)を match し、その型の内側を見て参照先の型(これは Enum1 です)を match します。
どちらの場合でも、私たち(つまりコンパイラ)は、ムーブと参照に関する Rust の不変条件を尊重しなければなりません。参照されている場合、そのオブジェクトのどの部分もムーブしてはいけません。match される値がコピーセマンティクスを持つ場合、それは自明です。ムーブセマンティクスを持つ場合は、どの match アームでもムーブが起こらないようにしなければなりません。これは、ムーブされるデータを無視するか、それへの参照を作る(つまりムーブ渡しではなく参照渡しにする)ことで実現されます。
#![allow(unused)] fn main() { enum Enum2 { // Box にはデストラクタがあるため、Enum2 はムーブセマンティクスを持つ。 Var1(Box<i32>), Var2, Var3 } fn foo(x: &Enum2) { match *x { // ネストされたデータを無視しているため、これは OK Enum2::Var1(..) => {} // 他のアームに変更はない。 Enum2::Var2 => {} Enum2::Var3 => {} } match x { // ネストされたデータを無視しているため、これは OK &Enum2::Var1(..) => {} // 他のアームに変更はない。 &Enum2::Var2 => {} &Enum2::Var3 => {} } } }
どちらのアプローチでも、ネストされたデータを一切参照していないため、そのどれもムーブされません。最初のアプローチでは、x は参照されているものの、デリファレンスのスコープ(つまり match 式)内でその内部には触れないため、何も外へ逃げることはできません。また、値全体を束縛する(つまり *x を変数に束縛する)こともしていないため、オブジェクト全体をムーブすることもできません。
2 つ目の match では任意のバリアントへの参照を取ることができますが、デリファレンスしたバージョンではできません。したがって、2 つ目のアプローチでは 2 番目のアームを a @ &Var2 => {} に置き換えても問題ありません(a は参照です)が、最初のアプローチでは a @ Var2 => {} と書くことはできません。なぜなら、それは *x を a にムーブすることを意味するからです。ref a @ Var2 => {} と書くことはできます(この場合も a は参照です)が、これはあまり頻繁に見る構文ではありません。
では、Var1 の内側にネストされたデータを使いたい場合はどうでしょうか。次のようには書けません。
#![allow(unused)] fn main() { match *x { Enum2::Var1(y) => {} _ => {} } }
または
#![allow(unused)] fn main() { match x { &Enum2::Var1(y) => {} _ => {} } }
どちらの場合も、x の一部を y にムーブすることを意味するからです。ref キーワードを使えば、Var1 内のデータへの参照を得ることができます。&Var1(ref y) => {} です。これは問題ありません。なぜなら、これでどこでもデリファレンスしておらず、したがって x のどの部分もムーブしていないからです。代わりに、x の内部を指すポインタを作成しています。
Alternatively、Box を分解することもできます(この match は 3 階層深くまで入っています): &Var1(box y) => {}(box パターン構文は rustc 1.58 時点で実験的であり、rustc の nightly バージョンでのみ利用可能であることに注意してください)。
これは問題ありません。なぜなら i32 はコピーセマンティクスを持ち、y は Var1 の内側にある Box の内側にある i32 のコピーだからです(これは借用された参照の「内側」にあります)。i32 はコピーセマンティクスを持つため、x のどの部分もムーブする必要はありません。int をコピーするのではなく、それへの参照を作ることもできます:
&Var1(box ref y) => {}。繰り返しますが、これも問題ありません。なぜなら、デリファレンスを一切行わないため、x のどの部分もムーブする必要がないからです。Box の内容がムーブセマンティクスを持っていた場合、&Var1(box y) => {} と書くことはできず、参照版を使わざるを得ません。同様のテクニックを最初のマッチング方法でも使うことができ、その場合は最初の & がないだけで同じように見えます。たとえば、Var1(box ref y) => {} です。
では、もう少し複雑にしてみましょう。enum 値への参照のペアに対してマッチしたいとします。この場合、最初の方法はまったく使えません:
#![allow(unused)] fn main() { fn bar(x: &Enum2, y: &Enum2) { // エラー: x と y がムーブされています。 // match (*x, *y) { // (Enum2::Var2, _) => {} // _ => {} // } // OK。 match (x, y) { (&Enum2::Var2, _) => {} _ => {} } } }
最初の方法が不正なのは、マッチされる値が、x と y をデリファレンスしてから、それらの両方を新しいタプルオブジェクトへムーブすることで作られるためです。そのため、この状況では 2 つ目の方法だけが機能します。そしてもちろん、x と y の一部をムーブしないようにするため、上記のルールに従う必要は依然としてあります。
何らかのデータへの参照しか取得できず、その値自体が必要になった場合、そのデータをコピーする以外に選択肢はありません。通常、それは clone() を使うことを意味します。そのデータが Clone を実装していない場合は、さらに分解して手動でコピーするか、自分で Clone を実装する必要があります。
では、ムーブセマンティクスを持つ値への参照ではなく、その値自体を持っている場合はどうでしょうか。この場合、ムーブは問題ありません。なぜなら、その値への参照を他の誰も持っていないことが分かっているからです(もし持っているなら、その値を使えないようにコンパイラが保証します)。たとえば、
#![allow(unused)] fn main() { fn baz(x: Enum2) { match x { Enum2::Var1(y) => {} _ => {} } } }
それでも、注意すべきことがいくつかあります。第一に、ムーブできる先は 1 か所だけです。上の例では、x の一部を y へムーブしており、残りは忘れることになります。もし a @ Var1(y) => {} と書いた場合、x 全体を a へムーブし、さらに x の一部を y へムーブしようとしていることになります。それは許可されておらず、そのようなアームは不正です。a または y のどちらか一方を参照にする(ref a などを使う)ことも選択肢にはなりません。その場合、参照を保持しながらムーブするという、上で説明した問題が起こるからです。a と y の両方を参照にすれば問題ありません。どちらもムーブしていないため、x はそのまま残り、全体とその一部へのポインターを持つことになります。
同様に(そしてより一般的に)、ネストされたデータを複数持つバリアントがある場合、あるデータへの参照を取り、別のデータをムーブすることはできません。たとえば Var4(Box<int>, Box<int>) として宣言された Var4 がある場合、両方を参照する match アーム(Var4(ref y, ref z) => {})や、両方をムーブする match アーム(Var4(y, z) => {})を持つことはできますが、一方をムーブし、もう一方を参照する match アーム(Var4(ref y, z) => {})を持つことはできません。これは、部分的なムーブであってもオブジェクト全体が破棄されるため、その参照が無効になるからです。
配列とベクター
Rust の配列は C の配列とはかなり異なります。まず、静的サイズと動的サイズの種類があります。これらはより一般的には固定長配列とスライスとして知られています。これから見るように、前者はやや不適切な名前です。なぜなら、どちらの種類の配列も(拡張可能ではなく)固定された長さを持つからです。拡張可能な「配列」として、Rust は Vec コレクションを提供しています。
固定長配列
固定長配列の長さは静的に分かっており、その型に含まれます。たとえば、[i32; 4] は長さ 4 の i32 の配列の型です。
配列リテラルと配列アクセスの構文は C と同じです。
#![allow(unused)] fn main() { let a: [i32; 4] = [1, 2, 3, 4]; // 通常どおり、型注釈は任意です。 println!("The second element is {}", a[1]); }
配列のインデックスが C と同じく 0 始まりであることに気づくでしょう。
しかし、C/C++1 とは異なり、配列のインデックス指定は境界チェックされます。実際、配列へのすべてのアクセスは境界チェックされます。これは Rust がより安全な言語であるもう 1 つの理由です。
a[4] を実行しようとすると、実行時パニックが発生します。残念ながら、この例のように明らかな場合であっても、Rust コンパイラはコンパイル時エラーを出せるほど賢くはありません。
危険を承知で使いたい場合や、プログラムから最後の一滴まで性能を引き出す必要がある場合でも、配列への未チェックアクセスを行うことはできます。これを行うには、配列の get_unchecked メソッドを使用します。未チェックの配列アクセスは unsafe ブロック内になければなりません。これが必要になるのは、ごくまれな状況に限られるはずです。
Rust の他のデータ構造と同じように、配列はデフォルトでイミュータブルであり、ミュータビリティは継承されます。変更もインデックス指定構文を通じて行います。
#![allow(unused)] fn main() { let mut a = [1, 2, 3, 4]; a[3] = 5; println!("{:?}", a); }
また他のデータと同じように、配列への参照を取ることで配列を借用できます。
fn foo(a: &[i32; 4]) { println!("First: {}; last: {}", a[0], a[3]); } fn main() { foo(&[1, 2, 3, 4]); }
借用された配列に対してもインデックス指定が機能することに注意してください。
ここで、C++ プログラマーにとって Rust の配列で最も興味深い側面、つまりその表現について話すのにちょうどよいタイミングです。Rust の配列は値型です。配列は他の値と同じようにスタック上に割り当てられ、配列オブジェクトは値の列であって、それらの値へのポインターではありません(C の場合とは異なります)。したがって、上の例で言えば、let a = [1_i32, 2, 3, 4]; はスタック上に 16 バイトを割り当て、let b = a; を実行すると 16 バイトがコピーされます。C 風の配列が欲しい場合は、配列へのポインターを明示的に作る必要があります。そうすると、最初の要素へのポインターが得られます。
Rust と C++ の配列における最後の違いは、Rust の配列はトレイトを実装でき、そのためメソッドを持てることです。たとえば、配列の長さを調べるには a.len() を使います。
スライス
Rust におけるスライスとは、単に長さがコンパイル時に分からない配列です。型の構文は固定長配列と同じですが、長さがありません。たとえば、[i32] は 32 ビット整数のスライス(静的に分かる長さを持たないもの)です。
スライスには注意点があります。Rust ではコンパイラがすべてのオブジェクトのサイズを知っていなければならず、スライスのサイズは知ることができないため、スライス型の値を持つことは決してできません。たとえば fn foo(x: [i32]) と書こうとすると、コンパイラはエラーを出します。
そのため、常にスライスへのポインターを持たなければなりません(この規則には、自作のスマートポインターを実装できるようにするための非常に技術的な例外がいくつかありますが、今のところは安全に無視できます)。fn foo(x: &[i32])(スライスへの借用参照)や fn foo(x: *mut [i32])(スライスへのミュータブルな生ポインター)などと書く必要があります。
スライスを作成する最も簡単な方法は型強制です。Rust には C++ よりもはるかに少ない暗黙の型強制しかありません。その 1 つが固定長配列からスライスへの型強制です。スライスはポインター値でなければならないため、これは実質的にはポインター間の型強制です。たとえば、&[i32; 4] を &[i32] に型強制できます。
#![allow(unused)] fn main() { let a: &[i32] = &[1, 2, 3, 4]; }
ここで右辺は、スタック上に割り当てられた長さ 4 の固定長配列です。次に、その参照を取ります(型は &[i32; 4])。その参照は &[i32] 型へ型強制され、let 文によって a という名前が与えられます。
ここでも、アクセスは C と同じく([...] を使って)行い、アクセスは境界チェックされます。len() を使って自分で長さを確認することもできます。したがって、配列の長さはどこかで分かっていることは明らかです。実際、Rust のあらゆる種類の配列は既知の長さを持っています。これは境界チェックに不可欠であり、境界チェックはメモリ安全性の不可欠な部分だからです。サイズは(固定長配列の場合のように静的にではなく)動的に分かっており、スライス型は動的サイズ型(DST。他にも動的サイズ型の種類があり、それらは別の箇所で扱います)であると言います。
スライスは単なる値の列なので、そのサイズをスライスの一部として格納することはできません。代わりに、ポインターの一部として格納されます(スライスは常にポインター型として存在しなければならないことを思い出してください)。スライスへのポインター(DST へのすべてのポインターと同様)はファットポインターです。つまり、1 ワードではなく 2 ワード幅で、データへのポインターに加えてペイロードを含みます。スライスの場合、ペイロードはスライスの長さです。
したがって上の例では、ポインター a は(64 ビットシステム上で)128 ビット幅になります。最初の 64 ビットには列 [1, 2, 3, 4] の中の 1 のアドレスが格納され、2 つ目の 64 ビットには 4 が含まれます。通常、Rust プログラマーとしては、これらのファットポインターを通常のポインターとして扱えば問題ありません。しかし、それについて知っておくのはよいことです(たとえば、キャストでできることに影響する場合があります)。
スライス記法と範囲
スライスは、配列の(借用された)ビューと考えることができます。これまでは配列全体のスライスだけを見てきましたが、配列の一部のスライスを取ることもできます。そのための特別な記法があり、インデックス指定構文に似ていますが、単一の整数ではなく範囲を取ります。たとえば a[0..4] は、a の最初の 4 要素のスライスを取ります。範囲は上端を含まず、下端を含むことに注意してください。例:
#![allow(unused)] fn main() { let a: [i32; 4] = [1, 2, 3, 4]; let b: &[i32] = &a; // 配列全体のスライス。 let c = &a[0..4]; // 配列全体の別のスライスで、これも型は &[i32]。 let c = &a[1..3]; // 中央の 2 要素、&[i32]。 let c = &a[1..]; // 最後の 3 要素。 let c = &a[..3]; // 最初の 3 要素。 let c = &a[..]; // 再び、配列全体。 let c = &b[1..3]; // スライスをさらにスライスすることもできます。 }
最後の例では、スライスした結果をなお借用する必要があることに注意してください。スライス構文は借用されていないスライス(型: [i32])を生成し、それをさらに借用する必要があります(&[i32] にするため)。これは、借用されたスライスをスライスしている場合でも同じです。
範囲構文はスライス構文の外でも使用できます。a..b は、a から b-1 まで進むイテレーターを生成します。これは通常どおり他のイテレーターと組み合わせることも、for ループで使用することもできます。
#![allow(unused)] fn main() { // 1 から 10 までのすべての数値を表示します。 for i in 1..11 { println!("{}", i); } }
Vec
ベクターはヒープに割り当てられ、所有参照です。そのため(Box<_> と同様に)、ムーブセマンティクスを持ちます。固定長配列は値に、スライスは借用参照になぞらえて考えることができます。同様に、Rust のベクターは Box<_> ポインターに似ています。
Vec<_> は値そのものとしてではなく、Box<_> と同じようなスマートポインターの一種として考えると理解しやすくなります。スライスと同様に、長さは「ポインター」に格納されます。この場合の「ポインター」は Vec の値です。
i32 のベクターは Vec<i32> 型です。ベクターリテラルはありませんが、vec! マクロを使うことで同じ効果を得られます。また、Vec::new() を使って空のベクターを作成することもできます。
#![allow(unused)] fn main() { let v = vec![1, 2, 3, 4]; // 長さ4のVec<i32>。 let v: Vec<i32> = Vec::new(); // i32の空のベクター。 }
上の2つ目のケースでは、ベクターが何のベクターなのかをコンパイラーが知ることができるように、型注釈が必要です。そのベクターを実際に使う場合には、型注釈はおそらく不要でしょう。
配列やスライスと同じように、インデックス表記を使ってベクターから値を取得できます(例: v[2])。ここでも境界チェックが行われます。また、スライス表記を使ってベクターのスライスを取ることもできます(例: &v[1..3])。
ベクターの追加機能は、そのサイズを変更できることです。必要に応じて長くしたり短くしたりできます。たとえば、v.push(5) は要素 5 をベクターの末尾に追加します(これには v がミュータブルである必要があります)。ベクターを大きくすると再割り当てが発生する可能性があり、大きなベクターでは大量のコピーを意味する場合があることに注意してください。これを防ぐために、with_capacity を使ってベクター内の領域を事前に割り当てることができます。詳細については Vec docs を参照してください。
Index トレイト
読者への注意: このセクションには、まだ十分に扱っていない内容がたくさんあります。チュートリアルに沿って読んでいる場合、このセクションは飛ばしてもかまいません。いずれにせよ、やや高度なトピックです。
配列やベクターに使われるのと同じインデックス構文は、HashMap などの他のコレクションにも使われます。また、自分自身のコレクションにも使うことができます。Index トレイトを実装することで、インデックス(およびスライス)構文の使用を選択できます。これは、Rust が組み込み型だけでなくユーザー型にも便利な構文を提供する仕組みの良い例です(スマートポインターの参照外しに使う Deref や、Add およびその他さまざまなトレイトも、同様の方法で機能します)。
Index トレイトは次のようなものです。
#![allow(unused)] fn main() { pub trait Index<Idx: ?Sized> { type Output: ?Sized; fn index(&self, index: Idx) -> &Self::Output; } }
Idx はインデックスに使われる型です。インデックスのほとんどの用途では、これは usize です。スライスでは、これは std::ops::Range 型のいずれかです。Output はインデックスによって返される型で、これはコレクションごとに異なります。スライスの場合は、単一要素の型ではなくスライスになります。index は、コレクションから要素を取り出す処理を行うメソッドです。コレクションは参照で受け取られ、このメソッドは同じライフタイムを持つ要素への参照を返すことに注意してください。
実装がどのようなものかを確認するために、Vec の実装を見てみましょう。
#![allow(unused)] fn main() { impl<T> Index<usize> for Vec<T> { type Output = T; fn index(&self, index: usize) -> &T { &(**self)[index] } } }
上で述べたように、インデックスには usize が使われます。Vec<T> の場合、インデックスは T 型の単一要素を返すため、それが Output の値になります。index の実装は少し奇妙です。(**self) は vec 全体をスライスとして見るビューを取得し、その後スライスに対するインデックスを使って要素を取得し、最後にその参照を取っています。
自分自身のコレクションがある場合、同様の方法で Index を実装することで、そのコレクションに対してインデックス構文やスライス構文を使えるようにできます。
初期化子構文
Rust のすべてのデータと同様に、配列とベクターは適切に初期化されていなければなりません。多くの場合、最初はゼロで埋められた配列が欲しいだけであり、配列リテラル構文を使うのは面倒です。そこで Rust は、指定した値で埋められた配列を初期化するためのちょっとした構文糖を用意しています: [value; len]。たとえば、長さ100でゼロ埋めされた配列を作成するには、[0; 100] を使います。
ベクターについても同様に、vec![42; 100] は100個の要素を持ち、それぞれの値が42であるベクターを生成します。
初期値は整数に限定されず、任意の式にできます。配列初期化子では、長さは整数定数式でなければなりません。vec! では、usize 型の任意の式にできます。
-
C++11 には
std::array<T, N>があり、at()メソッドが使われるときに境界チェックを提供します。 ↩
グラフとアリーナ割り当て
(この章の例は、このディレクトリをダウンロードして cargo run を実行することで動かせます)。
Rust では、ライフタイムと可変性に関する厳格な要件があるため、グラフを構築するのは少し扱いにくいです。オブジェクトのグラフは、OO プログラミングでは非常に一般的です。このチュートリアルでは、実装に対するいくつかの異なるアプローチを見ていきます。私が好むアプローチでは、アリーナ割り当てを使い、明示的なライフタイムをやや高度に利用します。最後に、このようなアプローチをより使いやすくする可能性のある Rust の機能についていくつか議論します。
グラフは、いくつかのノード間にエッジを持つノードの集合です。グラフはリストや木を一般化したものです。各ノードは複数の子と複数の親を持つことができます(通常は親/子というより、ノードに入るエッジとノードから出るエッジについて話します)。グラフは隣接リストまたは隣接行列で表現できます。前者は基本的に、グラフ内の各ノードに対するノードオブジェクトであり、各ノードオブジェクトが隣接ノードのリストを保持します。隣接行列は、行のノードから列のノードへのエッジが存在するかどうかを示す真偽値の行列です。ここでは隣接リスト表現のみを扱います。隣接行列には、Rust 固有性が比較的低い、まったく異なる問題があります。
本質的には、直交する 2 つの問題があります。グラフのライフタイムをどう扱うか、そしてその可変性をどう扱うかです。
最初の問題は、本質的にはグラフ内の他のノードを指すためにどの種類のポインタを使うかに帰着します。グラフのようなデータ構造は再帰的であるため(データ自体がそうでなくても型は再帰的です)、完全に値ベースの構造にするのではなく、何らかの種類のポインタを使わざるを得ません。グラフは循環を持つことができ、Rust の所有権は循環できないため、(木のようなデータ構造や連結リストで行うかもしれないように)ポインタ型として Box<Node> を使うことはできません。
完全に不変なグラフはありません。循環が存在し得るため、グラフを単一の文で作成することはできません。したがって、少なくとも初期化フェーズの間は、グラフは可変でなければなりません。Rust における通常の不変条件は、すべてのポインタは一意であるか不変でなければならない、というものです。グラフのエッジは可変でなければならず(少なくとも初期化中は)、任意のノードに入るエッジは複数存在し得るため、どのエッジも一意であることは保証されません。そのため、可変性を扱うには少し高度なことをする必要があります。
1 つの解決策は、可変な生ポインタ(*mut Node)を使うことです。これは最も柔軟なアプローチですが、最も危険でもあります。型システムからの助けなしに、すべてのライフタイム管理を自分で処理しなければなりません。この方法では非常に柔軟で効率的なデータ構造を作れますが、非常に注意深く行う必要があります。このアプローチは、ライフタイムと可変性の問題の両方を一挙に扱います。しかし、それらを扱う方法は本質的に Rust の利点をすべて無視することです。ここではコンパイラから何の助けも得られません(また、生ポインタは自動的に参照/参照外しされないため、特にエルゴノミックでもありません)。生ポインタを使うグラフは C++ のグラフと大きく変わらないため、ここではその選択肢は扱いません。
ライフタイム管理の選択肢としては、参照カウント(共有所有権、Rc<...> を使用)またはアリーナ割り当て(すべてのノードが同じライフタイムを持ち、アリーナによって管理される。借用参照 &... を使用)があります。前者はより柔軟で(グラフの外部から個々のノードへ任意のライフタイムを持つ参照を持てます)、後者はそれ以外のあらゆる点で優れています。
可変性の管理については、RefCell を使う、つまり Rust の動的な内部可変性の仕組みを利用するか、自分で可変性を管理するかのどちらかです(この場合、内部可変性をコンパイラに伝えるために UnsafeCell を使う必要があります)。前者はより安全で、後者はより効率的です。どちらも特にエルゴノミックではありません。
グラフが循環を持つ可能性がある場合、Rc を使うなら、循環を断ち切ってメモリをリークしないようにするための追加の対処が必要であることに注意してください。Rust には Rc ポインタの循環回収がないため、グラフ内に循環があると参照カウントは決して 0 にならず、グラフは決して解放されません。これは、グラフ内で Weak ポインタを使うか、グラフが破棄されるべきだと分かっているときに手動で循環を断ち切ることで解決できます。前者の方がより信頼できます。ここではどちらも扱わず、例では単にメモリをリークします。借用参照とアリーナ割り当てを使うアプローチにはこの問題がないため、その点で優れています。
異なるアプローチを比較するために、かなり単純な例を使います。グラフ内のノードを表す Node オブジェクトだけを用意します。これは文字列データ(より複雑なデータペイロードを代表するもの)と、隣接ノード(edges)の Vec を保持します。ノードの単純なグラフを作成する init 関数と、グラフを行きがけ順・深さ優先で走査する traverse 関数を用意します。これを使って、グラフ内の各ノードのペイロードを出力します。最後に、self ノードに隣接する最初のノードへの参照を返す Node::first メソッドと、個々のノードのペイロードを出力する foo 関数を用意します。これらの関数は、グラフ内部のノードの操作を伴う、より複雑な操作の代わりです。
退屈させずにできるだけ有益にするため、可能性のある組み合わせのうち 2 つを扱います。参照カウントと RefCell、そしてアリーナ割り当てと UnsafeCell です。他の 2 つの組み合わせは演習として残しておきます。
Rc<RefCell<Node>>
完全な例を参照してください。
これは unsafe コードがないため、より安全な選択肢です。また、最も効率が悪く、最もエルゴノミックでない選択肢でもあります。ただし、かなり柔軟ではあります。グラフのノードは参照カウントされているため、グラフの外部で簡単に再利用できます。完全に可変なグラフが必要な場合、またはノードをグラフから独立して存在させる必要がある場合には、このアプローチをお勧めします。
ノード構造は次のようになります。
#![allow(unused)] fn main() { struct Node { datum: &'static str, edges: Vec<Rc<RefCell<Node>>>, } }
新しいノードの作成はそれほど悪くありません: Rc::new(RefCell::new(Node { ... }))。初期化中にエッジを追加するには、始点ノードを可変として借用し、終点ノードをエッジの Vec に clone する必要があります(これは実際のノードではなくポインタを clone し、参照カウントを増やします)。例えば、次のようになります。
#![allow(unused)] fn main() { let mut mut_root = root.borrow_mut(); mut_root.edges.push(b.clone()); }
RefCell は、ノードに書き込むときに、そのノードをすでに読み取り中または書き込み中でないことを動的に保証します。
ノードにアクセスするときはいつでも、RefCell を借用するために .borrow() を使う必要があります。私たちの first メソッドは、借用参照ではなく参照カウントされたポインタを返す必要があるため、first の呼び出し元も借用しなければなりません。
fn first(&self) -> Rc<RefCell<Node>> { self.edges[0].clone() } pub fn main() { let g = ...; let f = g.first(); foo(&*f.borrow()); }
&Node と UnsafeCell
完全な例を参照してください。
このアプローチでは、借用参照をエッジとして使用します。これは優れていて扱いやすく、
主に借用参照を操作する「通常の」Rust ライブラリでノードを使用できます
(Rust の参照カウントオブジェクトの良い点の 1 つは、ライフタイムシステムとうまく連携することです。
Rc の内部への借用参照を作成して、データを直接(かつ安全に)参照できます。
前の例では、RefCell がこれを妨げていますが、Rc/UnsafeCell
アプローチなら可能になるはずです)。
破棄も正しく処理されます。唯一の制約は、すべてのノードを同時に破棄しなければならないことです。 ノードの破棄と割り当てはアリーナを使って処理されます。
一方で、かなり多くの明示的なライフタイムを使用する必要があります。 残念ながら、ここではライフタイムエリジョンの恩恵は受けられません。 このセクションの最後では、状況を改善できるかもしれない言語の今後の方向性について説明します。
構築中には、複数から参照されている可能性のあるノードを変更します。
これは安全な Rust コードでは不可能なので、unsafe ブロック内で初期化しなければなりません。
ノードは可変であり、かつ複数から参照されるため、Rust コンパイラに対して、
通常の不変条件に依存できないことを伝えるために UnsafeCell を使用しなければなりません。
このアプローチが実行可能なのはどのような場合でしょうか。グラフは初期化中にのみ変更されなければなりません。 さらに、グラフ内のすべてのノードが同じライフタイムを持つことを要求します (すべてを同時に破棄できる限り、後からノードを追加できるようにするために、 これらの制約をある程度緩めることはできます)。同様に、ノードをいつ変更できるかについて、 より複雑な不変条件に依存することもできますが、そうした点ではプログラマーが安全性に責任を持つため、 物事をシンプルに保つことには価値があります。
アリーナ割り当ては、あるオブジェクトの集合が同じライフタイムを持ち、 同時に解放できる場合のメモリ管理手法です。アリーナは、メモリの割り当てと解放を担うオブジェクトです。 個々のオブジェクトを割り当てるのではなく、大きなメモリの塊をまとめて割り当て・解放するため、 アリーナ割り当ては非常に効率的です。通常、すべてのオブジェクトは連続したメモリ領域から割り当てられます。 これにより、グラフを走査するときのキャッシュコヒーレンシが向上します。
Rust では、アリーナ割り当ては libarena crate によってサポートされており、コンパイラ全体で使用されています。アリーナには 2 種類あります。 型付きと型なしです。前者はより効率的で使いやすい一方、単一の型のオブジェクトしか割り当てられません。 後者はより柔軟で、任意のオブジェクトを割り当てられます。アリーナに割り当てられたオブジェクトはすべて同じライフタイムを持ち、 それはアリーナオブジェクトのパラメータです。型システムにより、アリーナに割り当てられたオブジェクトへの参照が、 アリーナ自体より長く生存できないことが保証されます。
ノード構造体には、グラフのライフタイムである 'a を含める必要があります。
隣接ノードの Vec を UnsafeCell でラップし、それが不変であるべきときでも
変更することを示します。
#![allow(unused)] fn main() { struct Node<'a> { datum: &'static str, edges: UnsafeCell<Vec<&'a Node<'a>>>, } }
新しい関数もこのライフタイムを使用しなければならず、割り当てを行うアリーナを引数として受け取らなければなりません。
#![allow(unused)] fn main() { fn new<'a>(datum: &'static str, arena: &'a TypedArena<Node<'a>>) -> &'a Node<'a> { arena.alloc(Node { datum: datum, edges: UnsafeCell::new(Vec::new()), }) } }
ノードを割り当てるためにアリーナを使用します。グラフのライフタイムは、アリーナへの参照のライフタイムから導かれるため、
アリーナはグラフのライフタイムを覆うスコープから渡されなければなりません。例では、これは
init メソッドに渡すことを意味します。(字句的なスコープの外側のスコープで値を作成できるようにする
型システムの拡張を想像することはできますが、近いうちにそのようなものを追加する計画はありません)。
アリーナがスコープ外になると、グラフ全体が破棄されます(Rust の型システムにより、
その時点を越えてグラフへの参照を保持できないことが保証されます)。
エッジの追加は少し違った見た目になります。
#![allow(unused)] fn main() { (*root.edges.get()).push(b); }
本質的には、ノード(b)をエッジのリストに追加するために、明らかな
root.edges.push(b) を行っています。しかし、edges は UnsafeCell でラップされているため、
それに対して get() を呼び出す必要があります。これにより、edges への可変な生ポインタ(*mut Vec<&Node>)が得られ、edges を変更できるようになります。ただし、それにはポインタを手動で
デリファレンスする必要もあります(生ポインタは自動デリファレンスされません)。したがって
(*...) という構造になります。最後に、生ポインタのデリファレンスは unsafe なので、
全体を unsafe ブロックで囲む必要があります。
traverse の興味深い部分は次のとおりです。
#![allow(unused)] fn main() { for n in &(*self.edges.get()) { n.traverse(f, seen); } }
エッジリストにアクセスするために、前と同じパターンに従います。これには unsafe ブロックが必要です。 この場合、実際には安全であることがわかっています。なぜなら、初期化後でなければならず、 したがって変更は発生しないからです。
繰り返しになりますが、first メソッドも edges リストにアクセスするために同じパターンに従います。
そしてこれも unsafe ブロック内になければなりません。しかし、Rc<RefCell<_>> を使用するグラフとは対照的に、
ノードへの素直な借用参照を返すことができます。これは非常に便利です。
変更を行わず、初期化後であるため、この unsafe ブロックは安全であると判断できます。
#![allow(unused)] fn main() { fn first(&'a self) -> &'a Node<'a> { unsafe { (*self.edges.get())[0] } } }
このアプローチに対する将来の言語改善
アリーナ割り当てと借用参照の使用は、Rust における重要なパターンだと私は考えています。 これらのパターンをより安全で使いやすくするために、言語側でさらに多くのことを行うべきです。 allocators に関する進行中の作業によって、 アリーナの使用がより扱いやすくなることを期待しています。ほかに 3 つの改善点があると考えています。
安全な初期化
OO の世界では、初期化中にのみ可変性を保証する仕組みについて多くの研究が行われてきました。 これが Rust で具体的にどのように機能するかは未解決の研究課題ですが、 可変で一意ではないものの、スコープが制限されたポインタを表現する必要があるように思われます。 そのスコープの外側では、既存のポインタは通常の借用参照、すなわち不変 または 一意なものになります。
このような方式の利点は、初期化中は可変で、その後は不変という一般的なパターンを表現する方法が得られることです。
またこれは、個々のオブジェクトは複数から所有されている一方で、集約体
(この場合はグラフ)は一意に所有されている、という不変条件にも依存しています。
そうすれば、UnsafeCell や unsafe ブロックなしで、参照と UnsafeCell のアプローチを採用できるようになり、
そのアプローチをより扱いやすく、より安全にできるはずです。
ETH Zurich の Alex Summers と Julian Viereck がこれをさらに調査しています。
ジェネリックモジュール
「グラフのライフタイム」は、特定のグラフに対して一定です。ライフタイムを繰り返すのは単なるボイラープレートです。これをより人間工学的にする方法の 1 つは、グラフモジュールをライフタイムでパラメーター化できるようにして、すべての struct、impl、関数にライフタイムを追加する必要をなくすことです。グラフのライフタイムは依然としてモジュールの外部から指定する必要がありますが、ほとんどの用途では推論が処理してくれることが期待できます(現在、関数呼び出しでそうなっているように)。
それがどのように見えるかについては、ref_graph_generic_mod.rs を参照してください。 (上で提案した安全な初期化も使用できるはずで、それにより unsafe コードを取り除けるはずです)。
こちらの RFC issue も参照してください。
この機能により、参照と UnsafeCell によるアプローチの構文上のオーバーヘッドが大幅に削減されるでしょう。
ライフタイム省略
現在、エルゴノミクスを向上させるために、プログラマーが関数シグネチャ内の一部のライフタイムを省略できるようにしています。グラフに対する &Node アプローチが少し見苦しい理由の 1 つは、ライフタイム省略規則の恩恵をまったく受けないためです。
Rust でよく見られるパターンは、共通のライフタイムを持つデータ構造です。そのようなデータ構造への参照は、たとえばグラフの例における &'a Node<'a> のように、&'a Foo<'a> のような型を生じさせます。このケースに役立つ省略規則があるとよいでしょう。ただし、それがどのように機能すべきかは、私にはあまり確信がありません。
ジェネリックモジュールを使った例を見ると、ライフタイム省略規則をそれほど拡張する必要はなさそうです(実際のところ、指定されたライフタイムなしで Node::new が機能するかどうかは確信がありませんが、もし機能しないとしても、機能させるための拡張はかなり些細なものに思えます)。モジュールジェネリックなライフタイムがスコープ内で唯一のものである場合('static 以外)に、それを省略できるようにする新しい規則を追加したくなるかもしれませんが、スコープ内に複数のライフタイムがある場合にそれがどう機能するかはよく分かりません(たとえば foo 関数と init 関数を参照してください)。
ジェネリックモジュールを追加しないとしても、&'a Node<'a> を特に対象とする省略規則を追加できるかもしれませんが、どのようにするかは分かりません。
クロージャと第一級関数
クロージャ、第一級関数、高階関数は Rust の中核的な要素です。C や C++ には関数ポインタがあります(そして C++ には、私には結局うまく理解できなかった、あの奇妙なメンバー/メソッドポインタのようなものもあります)。しかし、それらは比較的まれにしか使われず、あまり使いやすいものでもありません。C++11 ではラムダが導入されましたが、これは Rust のクロージャにかなり近く、特に実装戦略が非常によく似ています。
まずは、これらについて直感をつかむところから始めたいと思います。その後で、詳細に入っていきます。
関数 foo があるとしましょう: pub fn foo() -> u32 { 42 }。次に、関数を引数として受け取る別の関数 bar を想像してみます(bar のシグネチャは後で示します): fn bar(f: ...) { ... }。C で関数ポインタを渡すのと少し似たように、foo を bar に渡すことができます: bar(foo)。bar の本体では、f を関数であるかのように呼び出せます: let x = f();。
Rust に第一級関数があると言うのは、関数を持ち回ったり、他の値と同じように使ったりできるからです。bar が高階関数であると言うのは、関数を引数として受け取るからです。つまり、関数に対して作用する関数です。
Rust のクロージャは、便利な構文を持つ匿名関数です。クロージャ |x| x + 2 は引数を 1 つ受け取り、それに 2 を足して返します。クロージャの引数に型を与える必要はないことに注意してください(通常は推論できます)。戻り値の型を指定する必要もありません。クロージャ本体を単なる 1 つの式以上にしたい場合は、波括弧を使えます: |x: i32| { let y = x + 2; y }。関数と同じようにクロージャを渡すことができます: bar(|| 42)。
クロージャと他の関数の大きな違いは、クロージャがその環境をキャプチャすることです。これは、クロージャの外側にある変数をクロージャ内から参照できることを意味します。例:
#![allow(unused)] fn main() { let x = 42; bar(|| x); }
クロージャ内で x がスコープ内にあることに注目してください。
これまでにも、イテレータと一緒に使われるクロージャを見てきましたが、これはクロージャの一般的なユースケースです。たとえば、ベクターの各要素に値を加えるには次のようにします。
#![allow(unused)] fn main() { fn baz(v: Vec<i32>) -> Vec<i32> { let z = 3; v.iter().map(|x| x + z).collect() } }
ここで x はクロージャの引数であり、v の各メンバーが x として渡されます。z はクロージャの外側で宣言されていますが、クロージャなので z を参照できます。関数を map に渡すこともできます。
#![allow(unused)] fn main() { fn add_two(x: i32) -> i32 { x + 2 } fn baz(v: Vec<i32>) -> Vec<i32> { v.iter().map(add_two).collect() } }
Rust では関数の内部で関数を宣言することもできる点に注意してください。これらはクロージャではありません。つまり、自分の環境にアクセスできません。単にスコープを限定するための便利機能にすぎません。
#![allow(unused)] fn main() { fn qux(x: i32) { fn quxx() -> i32 { x // エラー: x はスコープ内にありません。 } let a = quxx(); } }
関数型
新しい例の関数を導入しましょう。
#![allow(unused)] fn main() { fn add_42(x: i32) -> i64 { x as i64 + 42 } }
前に見たように、関数を変数に格納できます: let a = add_42;。a の最も正確な型は Rust では書けません。コンパイラがエラーメッセージ内でそれを fn(i32) -> i64 {add_42} と表示するのを見かけることがあります。各関数は、それぞれ固有で匿名の型を持ちます。fn add_41(x: i32) -> i64 は、同じシグネチャを持っていても異なる型です。
より正確でない型を書くことはできます。たとえば、let a: fn(i32) -> i64 = add_42; です。同じシグネチャを持つすべての関数型は、fn 型(プログラマが書ける型)に強制変換できます。
a はコンパイラによって関数ポインタとして表現されます。しかし、コンパイラが正確な型を知っている場合、実際にはその関数ポインタは使いません。a() のような呼び出しは、a の型に基づいて静的にディスパッチされます。コンパイラが正確な型を知らない場合(たとえば、fn 型だけを知っている場合)、呼び出しは値の中の関数ポインタを使ってディスパッチされます。
Fn 型(大文字の 'F' に注意)もあります。これらの Fn 型は、トレイトと同じように境界です(実際、後で見るように、これらはトレイトそのものです)。Fn(i32) -> i64 は、そのシグネチャを持つすべての関数のようなオブジェクトの型に対する境界です。関数ポインタへの参照を取るとき、実際には fat pointer(DST を参照)で表現されるトレイトオブジェクトを作成しています。
関数を別の関数へ渡す場合、または関数をフィールドに格納する場合は、型を書かなければなりません。選択肢はいくつかあり、fn 型か Fn 型のどちらかを使えます。後者の方が優れています。なぜなら、クロージャ(および潜在的には他の関数のようなもの)を含められる一方で、fn 型は含められないからです。Fn 型は動的サイズ型であり、つまり値型として使うことはできません。関数オブジェクトを渡すか、ジェネリクスを使う必要があります。まずはジェネリクスのアプローチを見てみましょう。たとえば:
#![allow(unused)] fn main() { fn bar<F>(f: F) -> i64 where F: Fn(i32) -> i64 { f(0) } }
bar はシグネチャ Fn(i32) -> i64 を持つ任意の関数を受け取ります。つまり、F 型パラメータを任意の関数のような型でインスタンス化できます。bar(add_42) を呼び出して add_42 を bar に渡すことができ、その場合 F は add_42 の匿名型でインスタンス化されます。bar(add_41) を呼び出すこともでき、それも動作します。
bar にクロージャを渡すこともできます。たとえば、bar(|x| x as i64) です。これが動作するのは、クロージャ型も、そのシグネチャに一致する Fn 境界によって境界付けられるからです(関数と同じく、各クロージャはそれぞれ独自の匿名型を持ちます)。
最後に、関数やクロージャへの参照も渡せます: bar(&add_42) または
bar(&|x| x as i64)。
bar を fn bar(f: &Fn(i32) -> i64) ... と書くこともできます。これら 2 つのアプローチ(ジェネリクスと、関数/トレイトオブジェクト)には、かなり異なるセマンティクスがあります。ジェネリクスの場合、bar は単相化されるため、コードが生成されるとき、コンパイラは f の正確な型を知っています。つまり、静的にディスパッチできます。関数オブジェクトを使う場合、関数は単相化されません。f の正確な型は分からないため、コンパイラは仮想ディスパッチを生成しなければなりません。後者は遅くなりますが、前者はより多くのコードを生成します(型パラメータのインスタンスごとに 1 つの単相化された関数)。
実際には、Fn 以外にも関数トレイトがあります。FnMut と FnOnce もあります。これらは Fn と同じように使われます。たとえば、FnOnce(i32) -> i64 です。FnMut は、呼び出すことができ、その呼び出し中に変更され得るオブジェクトを表します。これは通常の関数には当てはまりませんが、クロージャでは、クロージャがその環境を変更できることを意味します。FnOnce は、(多くても)一度だけ呼び出せる関数です。これもまた、クロージャにのみ関係します。
Fn、FnMut、FnOnce はサブトレイト階層にあります。Fn は FnMut です(Fn 関数は変更する権限を持って呼び出しても害がないからです。ただし、その逆は成り立ちません)。Fn と FnMut は FnOnce です(通常の関数が一度だけ呼び出されても害はないからです。ただし、その逆は成り立ちません)。
したがって、高階関数をできるだけ柔軟にするには、Fn 境界ではなく FnOnce 境界を使うべきです(または、関数を複数回呼び出さなければならない場合は FnMut 境界を使います)。
メソッド
メソッドは関数と同じように使用できます。つまり、メソッドへのポインターを取得して
変数に格納する、といったことができます。ドット構文は使用できず、
完全明示の名前付け形式(universal function call syntax の略で UFCS と呼ばれることもあります)を使って、
メソッドを明示的に指定する必要があります。`self` パラメーターはメソッドの最初の引数です。例:
```rust
struct Foo;
impl Foo {
fn bar(&self) {}
}
trait T {
fn baz(&self);
}
impl T for Foo {
fn baz(&self) {}
}
fn main() {
// 固有メソッド。
let x = Foo::bar;
x(&Foo);
// トレイトメソッド。完全明示の名前付け形式に注意。
let y = <Foo as T>::baz;
y(&Foo);
}
ジェネリック関数
ジェネリック関数へのポインターを取得することはできず、ジェネリック関数型を表現する方法もありません。 ただし、すべての型パラメーターがインスタンス化されている場合は、関数への参照を取得できます。例:
fn foo<T>(x: &T) {} fn main() { let x = &foo::<i32>; x(&42); }
ジェネリッククロージャーを定義する方法はありません。多くの型に対して動作するクロージャーが必要な場合は、 トレイトオブジェクト、マクロ(クロージャーを生成するため)、またはクロージャーを返すクロージャーを渡すことができます (返される各クロージャーは異なる型に対して操作できます)。
ライフタイムジェネリック関数と高ランク型
ライフタイムに対してジェネリックな関数型とクロージャーを持つことは可能です。
借用参照を受け取るクロージャーがあるとします。このクロージャーは、参照がどのライフタイムを持っていても 同じように動作できます(実際、コンパイル後のコードではライフタイムは消去されています)。しかし、その型はどのような形になるのでしょうか?
例:
#![allow(unused)] fn main() { fn foo<F>(x: &Bar, f: F) -> &Baz where F: Fn(&Bar) -> &Baz { f(x) } }
ここでの参照のライフタイムは何でしょうか?この単純な例では、 単一のライフタイムを使用できます(ジェネリッククロージャーは不要です)。
#![allow(unused)] fn main() { fn foo<'b, F>(x: &'b Bar, f: F) -> &'b Baz where F: Fn(&'b Bar) -> &'b Baz { f(x) } }
しかし、f が異なるライフタイムを持つ入力に対して動作するようにしたい場合はどうでしょうか?
その場合はジェネリック関数型が必要です。
#![allow(unused)] fn main() { fn foo<'b, 'c, F>(x: &'b Bar, y: &'c Bar, f: F) -> (&'b Baz, &'c Baz) where F: for<'a> Fn(&'a Bar) -> &'a Baz { (f(x), f(y)) } }
ここで新しいのは for<'a> 構文です。これは、ライフタイムに対してジェネリックな関数型を表すために使われます。
これは「すべての 'a について、...」と読みます。理論的には、この関数型は全称量化されています。
上の例では 'a を foo へ持ち上げることはできない点に注意してください。反例:
#![allow(unused)] fn main() { fn foo<'a, 'b, 'c, F>(x: &'b Bar, y: &'c Bar, f: F) -> (&'b Baz, &'c Baz) where F: Fn(&'a Bar) -> &'a Baz { (f(x), f(y)) } }
これはコンパイルされません。なぜなら、コンパイラーが foo の呼び出しに対してライフタイムを推論するとき、
'a に対して単一のライフタイムを選ばなければならないためです。'b と 'c が異なる場合、それはできません。
このようにジェネリックな関数型は高ランク型と呼ばれます。
外側のレベルにあるライフタイム変数はランク 1 を持ちます。上の例の 'a は
外側のレベルへ移動できないため、そのランクは 1 より高くなります。
高ランク関数型の引数を持つ関数の呼び出しは簡単です。コンパイラーがライフタイムパラメーターを推論します。
例: foo(&Bar { ... }, &Bar {...}, |b| &b.field)。
実際のところ、ほとんどの場合、このようなことを気にする必要すらありません。 関数引数上の多くのライフタイムを省略できるのと同じように、コンパイラーは量化されたライフタイムを省略することを許可します。 たとえば、上の例は単に次のように書けます。
#![allow(unused)] fn main() { fn foo<'b, 'c, F>(x: &'b Bar, y: &'c Bar, f: F) -> (&'b Baz, &'c Baz) where F: Fn(&Bar) -> &Baz { (f(x), f(y)) } }
(そして、これは作為的な例なので 'b と 'c が必要なだけです)。
Rust が借用参照を持つ関数型を見ると、通常の省略規則を適用し、 省略された変数をその関数型のスコープで量化します(つまり、高ランクで)。
かなりニッチなユースケースに見えるもののために、なぜこれほどの複雑さが必要なのか疑問に思うかもしれません。 本当の動機は、外側の関数によって提供されるデータに対して操作するための関数を受け取る関数です。例:
#![allow(unused)] fn main() { fn foo<F>(f: F) where F: Fn(&i32) // 完全明示の型: for<'a> Fn(&'a i32) { let data = 42; f(&data) } }
このような場合、私たちには高ランク型が必要です。代わりに foo にライフタイムパラメーターを追加した場合、
正しいライフタイムを推論することは決してできません。その理由を見るために、どのように動作し得るかを見てみましょう。
fn foo<'a, F: Fn(&'a i32)> ... を考えます。Rust では、任意のライフタイムパラメーターは、
それが宣言されているアイテムより長く生存しなければなりません(そうでなければ、そのライフタイムを持つ引数が、
その関数内で使用される可能性がありますが、そこで生存していることは保証されません)。foo の本体では
f(&data) を使用します。その参照に対して Rust が推論するライフタイムは、(長くても)
data が宣言された場所からスコープを外れる場所まで続きます。'a は
foo より長く生存しなければなりませんが、その推論されたライフタイムはそうではないため、
この方法で f を呼び出すことはできません。
しかし、高ランクライフタイムを使うと、f は任意のライフタイムを受け入れられるため、
&data から来る匿名のライフタイムでも問題なく、関数は型チェックに通ります。
列挙型コンストラクター
これは少し脱線ですが、ときには便利なテクニックです。列挙型のすべてのバリアントは、 そのバリアントのフィールドから列挙型への関数を定義します。例:
#![allow(unused)] fn main() { enum Foo { Bar, Baz(i32), } }
これは 2 つの関数、Foo::Bar: Fn() -> Foo と Foo::Baz: Fn(i32) -> Foo を定義します。
通常、私たちはバリアントをこのような方法では使わず、関数ではなくデータ型として扱います。
しかし、たとえば i32 のリストがある場合、次のようにして Foo のリストを作成できるので便利なことがあります。
#![allow(unused)] fn main() { list_of_i32.iter().map(Foo::Baz).collect() }
クロージャーの種類
クロージャーには 2 種類の入力があります。明示的に渡される引数と、 環境からキャプチャーする変数です。通常、どちらの入力についてもすべて推論されますが、 必要であればより細かく制御できます。
引数については、Rust に推論させる代わりに型を宣言できます。
戻り値の型を宣言することもできます。|x| { ... } と書く代わりに、
|x: i32| -> String { ... } と書けます。引数が所有されるか借用されるかは、
型(宣言されたもの、または推論されたもの)によって決まります。
キャプチャーされる変数については、型はほとんど環境から分かりますが、 Rust は少し追加の魔法を行います。変数は参照でキャプチャーされるべきでしょうか、それとも値でキャプチャーされるべきでしょうか? Rust はこれをクロージャーの本体から推論します。可能であれば、Rust は参照でキャプチャーします。例:
#![allow(unused)] fn main() { fn foo(x: Bar) { let f = || { ... x ... }; } }
すべてがうまくいけば、f の本体では、x は &Bar 型になり、そのライフタイムは
foo のスコープによって制限されます。しかし、x が変更される場合、Rust は
キャプチャーが可変参照によるもの、つまり x の型が &mut Bar であると推論します。
x が f の中でムーブされる場合(たとえば、値型を持つ変数やフィールドに格納される場合)、
Rust はその変数が値でキャプチャーされなければならない、つまり Bar 型を持つと推論します。
これはプログラマーが上書きできます(クロージャをフィールドに格納したり、関数から返したりする場合には必要になることがあります)。クロージャの前に move キーワードを使用します。すると、キャプチャされたすべての変数は値でキャプチャされます。たとえば、let f = move || { ... x ... }; では、x は常に Bar 型になります。
以前、さまざまな関数の種類、すなわち Fn、FnMut、FnOnce について話しました。これで、なぜそれらが必要なのかを説明できます。クロージャでは、可変性と一回性はキャプチャされた変数を指します。クロージャが、キャプチャする変数のいずれかを変更する場合、そのクロージャは FnMut 型になります(これはコンパイラーによって完全に推論されるため、注釈は不要です)。変数がクロージャにムーブされる場合、つまり値でキャプチャされる場合(明示的な move による場合でも、推論による場合でも)、そのクロージャは FnOnce 型になります。そのようなクロージャを複数回呼び出すのは安全ではありません。キャプチャされた変数が複数回ムーブされることになるためです。
Rust は可能な限り、そのクロージャに対して最も柔軟な型を推論しようとします。
実装
クロージャは匿名構造体として実装されます。その構造体には、クロージャによってキャプチャされた各変数に対応するフィールドがあります。これはライフタイムに関してパラメトリックであり、キャプチャされた変数のライフタイムに対する境界となる単一のライフタイムパラメーターを持ちます。この匿名構造体は、クロージャを実行するために呼び出される call メソッドを実装します。
たとえば、次を考えてみます。
fn main() { let x = Foo { ... }; let f = |y| x.get_number() + y; let z = f(42); }
コンパイラーはこれを次のように扱います。
struct Closure14<'env> { x: &'env Foo, } // 実際にはこのようには実装されていません。以下を参照してください。 impl<'env> Closure14<'env> { fn call(&self, y: i32) -> i32 { self.x.get_number() + y } } fn main() { let x = Foo { ... }; let f = Closure14 { x: x } let z = f.call(42); }
上で述べたように、関数トレイトには Fn、FnMut、FnOnce の 3 種類があります。実際には、call メソッドは固有の impl に含まれるのではなく、これらのトレイトによって要求されます。Fn には self を参照で受け取る call メソッドがあり、FnMut には self を可変参照で受け取る call_mut があり、FnOnce には self を値として受け取る call_once があります。
上で見てきた関数型は Fn(i32) -> i32 のような形で、トレイト型にはあまり見えません。ここには少し魔法があります。Rust はこの丸括弧の糖衣構文を関数型に対してのみ許可しています。通常の型(「山括弧型」)に脱糖するには、引数の型はタプル型として扱われ、型パラメーターとして渡され、戻り値の型は Output という関連型として扱われます。したがって、Fn(i32) -> i32 は Fn<(i32,), Output=i32> に脱糖され、Fn トレイトの定義は次のようになります。
#![allow(unused)] fn main() { pub trait Fn<Args> : FnMut<Args> { fn call(&self, args: Args) -> Self::Output; } }
したがって、上の Closure14 の実装は、より実際には次のような形になります。
#![allow(unused)] fn main() { impl<'env> FnOnce<(i32,)> for Closure14<'env> { type Output = i32; fn call_once(self, args: (i32,)) -> i32 { ... } } impl<'env> FnMut<(i32,)> for Closure14<'env> { fn call_mut(&mut self, args: (i32,)) -> i32 { ... } } impl<'env> Fn<(i32,)> for Closure14<'env> { fn call(&self, args: (i32,)) -> i32 { ... } } }
関数トレイトは core::ops で確認できます。
上では、ジェネリクスを使用すると静的ディスパッチになり、トレイトオブジェクトを使用すると仮想ディスパッチになることについて話しました。これで、その理由をもう少し詳しく見ることができます。
call を呼び出すとき、それは静的にディスパッチされるメソッド呼び出しであり、仮想ディスパッチはありません。モノモーフィゼーションされた関数に渡す場合でも、型は依然として静的に分かっており、静的ディスパッチが行われます。
クロージャをトレイトオブジェクトにすることもできます。たとえば、&Fn(i32)->i32 型または Box<Fn(i32)->i32> 型を持つ &f や Box::new(f) です。これらはポインター型であり、トレイトへのポインター型であるため、そのポインターはファットポインターです。つまり、それらはデータそのものへのポインターと、vtable へのポインターで構成されます。vtable は call(または call_mut など)のアドレスを検索するために使用されます。
クロージャのこれら 2 つの表現は、boxed クロージャおよび unboxed クロージャと呼ばれることがあります。unboxed クロージャは、静的ディスパッチを伴う値渡しのバージョンです。boxed バージョンは、動的ディスパッチを伴うトレイトオブジェクト版です。昔の Rust には boxed クロージャしかありませんでした(そしてそのシステムはかなり異なっていました)。
参考資料
FIXME: C++ 11 のクロージャとの関連付け
Copyright 2015 The Rust for C++ programmers Developers.
Apache License, Version 2.0 <LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0> または MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT> のいずれかを 選択してライセンスされています。このファイルは、これらの条項に 従う場合を除き、複製、変更、または配布することはできません。