Rust 勉強メモ
Rust の教材
勉強の順序
The Book -> 歯車本 -> 蟹本
歯車本はkenkooooさん、matsu7874さんが書いてる方の歯車本。蟹本はかなり面白いらしい。
基礎
The Book
Zenn の Rust 入門本
Rust で競プロ本
非同期処理、並行処理
エラーハンドリング
Rust で〇〇とか使用例とか
- AtCoder Problems
rustc
設計
Rust のメモリ管理
Rust プログラムのメモリ管理の基礎
Rustのメモリに関する話.スタック,ヒープならびにメモリリークについて簡単に解説をして,そしてGCの話.そこから,Rustのメモリ安全性のロジックについて説明していて,非常に分かりやすかった.なるほどこう説明すればいいのか感.
Rust memory safety revolution
Discord が Go の GC が理由で Rust に書き直した話
部分的にRustを導入しているDiscordがGoで書かれたキャッシュをRustで書き直したら速くなった話
- GCによるハネがない
- あらゆるメトリクスが向上
- メモリ使用量が減ったのでキャッシュサイズを増やせた
記事:Why Discord is switching from Go to Rust - Discord Blog
日本語訳:なぜDiscordはGoからRustへ移行するのか
Rustで処理速度を改善 - 実装言語を「Go」から「Rust」に変更、ゲーマー向けチャットアプリ「Discord」の課題とは
目次
- はじめに
-
1. 事始め
- 1.1. インストール
- 1.2. Hello, World!
- 1.3. Hello, Cargo!
- 2. 数当てゲームのプログラミング
-
3. 一般的なプログラミングの概念
- 3.1. 変数と可変性
- 3.2. データ型
- 3.3. 関数
- 3.4. コメント
- 3.5. 制御フロー
-
4. 所有権を理解する
- 4.1. 所有権とは?
- 4.2. 参照と借用
- 4.3. スライス型
- 5. 構造体を使用して関係のあるデータを構造化する
- 6. Enum とパターンマッチング
- 7. 肥大化していくプロジェクトをパッケージ、クレート、モジュールを利用して管理する
- 8. 一般的なコレクション
- 9. エラー処理
- 10. ジェネリック型、トレイト、ライフタイム
- 11. 自動テストを書く
- 12. 入出力プロジェクト:コマンドラインプログラムを構築する
- 13. 関数型言語の機能:イテレータとクロージャ
- 14. Cargo と Crates.io についてより詳しく
- 15. スマートポインタ
- 16. 恐れるな!並行性
- 17. Rust のオブジェクト指向プログラミング機能
- 18. パターンとマッチング
- 19. 高度な機能
- 20. 最後のプロジェクト:マルチスレッドの Web サーバを構築する
-
21. 付録
- 21.1. 付録 A:キーワード
- 21.2. 付録 B:演算子と記号
- 21.3. 付録 C:導出可能なトレイト
- 21.4. 付録 D:便利な開発ツール
- 21.5. 付録 E:エディション
- 21.6. 付録 F:本の翻訳
- 21.7. 付録 G:Rust の作られ方と“Nightly Rust”
勉強ノート:The Book chapXX ~
勉強ノート:The Book chap04
chap04:所有権
所有権とは?
所有権は Rust の中心的な機能。
プログラムのメモリの使用方法
- 明示的にメモリの確保、解放
- 定期的にメモリの解放(ガベージコレクション、GC)
- 所有権システム -> New!!
Rustの中心的な機能は、所有権です。この機能は、説明するのは簡単なのですが、言語の残りの機能全てにかかるほど 深い裏の意味を含んでいるのです。
全てのプログラムは、実行中にコンピュータのメモリの使用方法を管理する必要があります。プログラムが動作するにつれて、 定期的に使用されていないメモリを検索するガベージコレクションを持つ言語もありますが、他の言語では、 プログラマが明示的にメモリを確保したり、解放したりしなければなりません。Rustでは第3の選択肢を取っています: メモリは、コンパイラがコンパイル時にチェックする一定の規則とともに所有権システムを通じて管理されています。 どの所有権機能も、実行中にプログラムの動作を遅くすることはありません。
所有権を理解しよう。
所有権は多くのプログラマにとって新しい概念なので、慣れるまでに時間がかかります。 嬉しいことに、Rustと、所有権システムの規則の経験を積むと、より自然に安全かつ効率的なコードを構築できるようになります。 その調子でいきましょう!
所有権を理解した時、Rustを際立たせる機能の理解に対する強固な礎を得ることになるでしょう。この章では、 非常に一般的なデータ構造に着目した例を取り扱うことで所有権を学んでいきます: 文字列です。
スタックとヒープ
スタック
コードが関数を呼び出すと、関数に渡された値(ヒープのデータへのポインタも含まれる可能性あり)と、 関数のローカル変数がスタックに載ります。関数の実行が終了すると、それらの値はスタックから取り除かれます。
Rust の所有権が解決する問題
- どの部分のコードがどのヒープ上のデータを使用しているか把握すること
- ヒープ上の重複するデータを最小化すること
- メモリ不足にならないようにヒープ上の未使用のデータを掃除すること
ヒープデータを管理することが所有権の存在する理由である。
どの部分のコードがどのヒープ上のデータを使用しているか把握すること、ヒープ上の重複するデータを最小化すること、 メモリ不足にならないようにヒープ上の未使用のデータを掃除することは全て、所有権が解決する問題です。 一度所有権を理解したら、あまり頻繁にスタックとヒープに関して考える必要はなくなるでしょうが、 ヒープデータを管理することが所有権の存在する理由だと知っていると、所有権がありのままで動作する理由を 説明するのに役立つこともあります。
所有権規則
まず、所有権のルールについて見ていきましょう。 この規則を具体化する例を扱っていく間もこれらのルールを肝に銘じておいてください:
- Rust の各値は、所有者と呼ばれる変数と対応している
- いかなる時も所有者は一つである
- 所有者がスコープから外れたら、値は破棄される
メモリの二重解放
同じメモリを解放しようとします。これは二重解放エラーとして知られ、以前触れたメモリ安全性上のバグの一つになります。 メモリを2回解放することは、memory corruption (訳注: メモリの崩壊。意図せぬメモリの書き換え) につながり、 セキュリティ上の脆弱性を生む可能性があります。
ムーブによる二重解放の防止
以下のコードでは「ムーブ」が起きている。
let s1 = String::from("hello");
let s2 = s1; // ここでムーブが起きた
println!("{}, world!", s1);
他の言語を触っている間に"shallow copy"と"deep copy"という用語を耳にしたことがあるなら、 データのコピーなしにポインタと長さ、許容量をコピーするという概念は、shallow copyのように思えるかもしれません。ですが、コンパイラは最初の変数をも無効化するので、shallow copyと呼ばれる代わりに、 ムーブとして知られているわけです。この例では、s1はs2にムーブされたと表現するでしょう。 以上より、実際に起きることを図4-4に示してみました。
Rust では、変数を別の変数に代入したときに、他の言語における shallow copy、deep copy とは別の「ムーブ move」と呼ばれる処理を実行する。以下に三者の違いを図示する。
shallow copy
deep copy
move
クローンによるヒープデータのコピー
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
これは問題なく動作し、図4-3で示した動作を明示的に生み出します。ここでは、 ヒープデータが実際にコピーされています。
cloneメソッドの呼び出しを見かけたら、何らかの任意のコードが実行され、その実行コストは高いと把握できます。 何か違うことが起こっているなと見た目でわかるわけです。
スタックのみのデータのコピー
以下のコードでは x のムーブ(スコープ内で無効化される)が起きない。
let x = 5;
let y = x;
println!("x = {}, y = {}", x, y);
ですが、このコードは一見、今学んだことと矛盾しているように見えます: cloneメソッドの呼び出しがないのに、xは有効で、yにムーブされませんでした。
コンパイル時に既知のサイズを持つ型はスタック上に保持される。逆に、String 型などの既知のサイズを持たない方はヒープ上にメモリが確保され保持される。
その理由は、整数のようなコンパイル時に既知のサイズを持つ型は、スタック上にすっぽり保持されるので、 実際の値をコピーするのも高速だからです。これは、変数yを生成した後にもxを無効化したくなる理由がないことを意味します。 換言すると、ここでは、shallow copyとdeep copyの違いがないことになり、 cloneメソッドを呼び出しても、一般的なshallow copy以上のことをしなくなり、 そのまま放置しておけるということです。
コピートレイトについて。変数の型がコピートレイトに適合していればその変数を別の変数に代入してもムーブが起きない。
RustにはCopyトレイトと呼ばれる特別な注釈があり、 整数のようなスタックに保持される型に対して配置することができます(トレイトについては第10章でもっと詳しく話します)。 型がCopyトレイトに適合していれば、代入後も古い変数が使用可能になります。コンパイラは、 型やその一部分でもDropトレイトを実装している場合、Copyトレイトによる注釈をさせてくれません。 型の値がスコープを外れた時に何か特別なことを起こす必要がある場合に、Copy注釈を追加すると、コンパイルエラーが出ます。 型にCopy注釈をつける方法について学ぶには、付録Cの「導出可能なトレイト」をご覧ください。
コピートレイトに適合している型(コンパイル時に必要なメモリサイズが決まる型が多い)
- あらゆる整数型。u32など。
- 論理値型であるbool。trueとfalseという値がある。
- あらゆる浮動小数点型、f64など。
- 文字型であるchar。
- タプル。ただ、Copyの型だけを含む場合。例えば、(i32, i32)はCopyだが、 (i32, String)は違う。
所有権と関数
意味論的に、関数に値を渡すことと、値を変数に代入することは似ています。関数に変数を渡すと、 代入のようにムーブやコピーされます。リスト4-3は変数がスコープに入ったり、 抜けたりする地点について注釈してある例です。
fn main() {
let s = String::from("hello"); // sがスコープに入る
takes_ownership(s); // sの値が関数にムーブされ...
// ... ここではもう有効ではない
let x = 5; // xがスコープに入る
makes_copy(x); // xも関数にムーブされるが、
// i32はCopyなので、この後にxを使っても
// 大丈夫
} // ここでxがスコープを抜け、sもスコープを抜ける。ただし、sの値はムーブされているので、何も特別なことは起こらない。
//
fn takes_ownership(some_string: String) { // some_stringがスコープに入る。
println!("{}", some_string);
} // ここでsome_stringがスコープを抜け、`drop`が呼ばれる。後ろ盾してたメモリが解放される。
//
fn makes_copy(some_integer: i32) { // some_integerがスコープに入る
println!("{}", some_integer);
} // ここでsome_integerがスコープを抜ける。何も特別なことはない。
所有権と戻り値
関数の引数として変数を渡すと、変数のムーブが起きるが、戻り値として所有権を返すこともできる。ただし、これを毎回していては面倒。
変数の所有権は、毎回同じパターンを辿っています: 別の変数に値を代入すると、ムーブされます。 ヒープにデータを含む変数がスコープを抜けると、データが別の変数に所有されるようムーブされていない限り、 dropにより片付けられるでしょう。
所有権を取り、またその所有権を戻す、ということを全ての関数でしていたら、ちょっとめんどくさいですね。 関数に値は使わせるものの所有権を取らないようにさせるにはどうするべきでしょうか。 返したいと思うかもしれない関数本体で発生したあらゆるデータとともに、再利用したかったら、渡されたものをまた返さなきゃいけないのは、 非常に煩わしいことです。
毎度関数に所有権を取られていては煩わしすぎるので、この概念に対する機能として参照がある。
借用
関数が実際の値の代わりに参照を引数に取ると、所有権をもらわない(ムーブが起きない)ため、所有権を返す目的で値を返す必要はない。
参照を作成することを借用 borrowingと呼ぶ。(原著:We call the action of creating a reference borrowing)。現実生活のように、誰かが何かを所有していたら、 それを借りることができるが、用が済んだら、返さなきゃいけないということ。
変数が標準で不変なのと全く同様に、参照も不変。参照している何かを変更することはできない。
不変な参照、可変な参照
可変な参照には大きな制約が一つある。
特定のスコープにおいて、ある特定の変数に対しては、一つしか可変な参照を持てないこと。以下のコードはコンパイルエラーになる。
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{}, {}", r1, r2);
これもコンパイルエラー
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{}, {}", r1);
これはコンパイルが通る
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{}, {}", r2);
Dangling References 宙に浮いた参照
ポインタのある言語では、誤ってダングリングポインタを生成してしまいやすいです。ダングリングポインタとは、 他人に渡されてしまった可能性のあるメモリを指すポインタのことであり、その箇所へのポインタを保持している間に、 メモリを解放してしまうことで発生します。対照的にRustでは、コンパイラが、 参照がダングリング参照に絶対ならないよう保証してくれます: つまり、何らかのデータへの参照があったら、 コンパイラは参照がスコープを抜けるまで、データがスコープを抜けることがないよう確認してくれるわけです。
ダングリング参照を試みるコード
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}
上記コードではないもの(dangle
関数のスコープを抜けることでなくなる s のヒープデータ)に対する参照を返している。これをコンパイラが防いでくれる。
sは、dangle内で生成されているので、dangleのコードが終わったら、sは解放されてしまいますが、 そこへの参照を返そうとしました。つまり、この参照は無効なStringを指していると思われるのです。 よくないことです!コンパイラは、これを阻止してくれるのです。
ここでの解決策は、Stringを直接返すことです:
これは no_dangle
関数内で宣言した s
の所有権を関数呼び出し元へムーブしているので関数のスコープを抜けても解放されない。
fn no_dangle() -> String {
let s = String::from("hello");
s
}
借用の規則
借用 borrowing = 参照を作成すること
- 不変参照(
&
)は複数存在して良い- -> データを読み込むだけ(read only)ならそのデータに影響を与えないので問題ない
- 不変参照(
&
)と可変参照(&mut
)は同時に存在することはできない- -> 不変参照が存在している間に急に値が変わることは予想できない
- 可変参照(
&mut
)は同時に 1 つしか存在することができない- -> データ競合 data race を防ぐ
- 参照は常に有効でなければならない
- -> dangling reference を防ぐ
これらのエラーは、時としてイライラするものではありますが、Rust コンパイラがバグの可能性を早期に指摘してくれ(それも実行時ではなくコンパイル時に)、 問題の発生箇所をズバリ示してくれるのだと覚えておいてください。そうして想定通りにデータが変わらない理由を追いかける必要がなくなります。
任意のタイミングで、一つの可変参照か不変な参照いくつでものどちらかを行える。
参照は常に有効でなければならない。
スライス
スライスのデータ構造
まとめ
所有権、借用、スライスの概念は、Rustプログラムにおいて、コンパイル時にメモリ安全性を保証します。 Rust言語も他のシステムプログラミング言語と同じように、メモリの使用法について制御させてくれるわけですが、データの所有者がスコープを抜けたときに、所有者に自動的にデータを片付けさせることは、この制御をするために、 余計なコードを書いたりデバッグしたりする必要がないことを意味します。
所有権は、Rustの他のいろんな部分が動作する方法に影響を与えるので、これ以降もこれらの概念についてさらに語っていく予定です。 第5章に移って、structでデータをグループ化することについて見ていきましょう。
勉強ノート:The book chap01 ~ chap03
シャドーイングと mut の違い
シャドーイングは、変数をmutにするのとは違います。なぜなら、letキーワードを使わずに、 誤ってこの変数に再代入を試みようものなら、コンパイルエラーが出るからです。letを使うことで、 値にちょっとした加工は行えますが、その加工が終わったら、変数は不変になるわけです。
mutと上書きのもう一つの違いは、再度letキーワードを使用したら、実効的には新しい変数を生成していることになるので、 値の型を変えつつ、同じ変数名を使いまわせることです。例えば、 プログラムがユーザに何らかのテキストに対して空白文字を入力することで何個分のスペースを表示したいかを尋ねますが、 ただ、実際にはこの入力を数値として保持したいとしましょう:
fn main() {
let spaces = " ";
let spaces = spaces.len();
}
この文法要素は、容認されます。というのも、最初のspaces変数は文字列型であり、2番目のspaces変数は、 たまたま最初の変数と同じ名前になったまっさらな変数のわけですが、数値型になるからです。故に、シャドーイングのおかげで、 異なる名前を思いつく必要がなくなるわけです。spaces_strとspaces_numなどですね; 代わりに、 よりシンプルなspacesという名前を再利用できるわけです。一方で、この場合にmutを使おうとすると、 以下に示した通りですが、コンパイルエラーになるわけです:
fn main() {
let mut spaces = " ";
spaces = spaces.len();
}
変数の型を可変にすることは許されていないということ。
整数型と浮動小数点数型の "基準型"
「基準型」=「デフォルトの型」=「型注釈なしのときに型推論で割り当てられる型」と解釈。原著では defaults とか default type として言及されている。
整数型の基準型は i32
。浮動小数点数型の基準型は f64
。現代の CPU では f32
と f64
でほとんど速度が変わらないのはへーってなった。なんでだろ。
Rustにはさらに、浮動小数点数に対しても、2種類の基本型があり、浮動小数点数とは数値に小数点がついたもののことです。 Rustの浮動小数点型は、f32とf64で、それぞれ32ビットと64ビットサイズです。基準型はf64です。 なぜなら、現代のCPUでは、f32とほぼ同スピードにもかかわらず、より精度が高くなるからです。
文字列出力の padding
assert_eq!("00000110", format!("{:0>8}", "110"));
// |||
// ||+-- width
// |+--- align
// +---- fill
ドキュメント
use のスコープ?
疑問
以下の例で mod my_module
内の utils
が名前解決できない理由がわからない
ちょっとまだ use と mod の使い方が分かってない。名前解決できる範囲がわからない。てっきりトップレベルで use
を記述すれば mod 含めそのファイル内全体 utils
の名前解決ができると思ったけど、そうじゃなかった。たとえ同一ファイル内に記述されていたとしても mod
は独立した名前空間を持てるってことかな?
extern crate my_projects;
use my_projects::utils;
fn main() {
// こっちは OK
utils::str2int(...);
}
mod my_module {
// ここに use my_projects::utils; を追加するとコンパイルが通る
// use my_projects::utils;
pub fn my_func() {
// こっちはコンパイルエラー
// use of undeclared crate or module `utils`
// error[E0433]: failed to resolve: use of undeclared crate or module `utils`
utils::str2int(...);
}
}
理由
あとで調べる。
Rust の char はユニコード
C / C++ とかだと char
型は 1 バイトで、ASCII コードしか表現できないが、Rust では char
型は 4 バイトでユニコード文字のスカラー値を表現できる。
fn main() {
let c = 'z';
let z = 'ℤ';
let heart_eyed_cat = '😻'; //ハート目の猫
}
Rustのchar型は、ユニコードのスカラー値を表します。これはつまり、アスキーよりもずっとたくさんのものを表せるということです。 アクセント文字; 中国語、日本語、韓国語文字; 絵文字; ゼロ幅スペースは、全てRustでは、有効なchar型になります。ユニコードスカラー値は、 U+0000からU+D7FFまでとU+E000からU+10FFFFまでの範囲になります。 ところが、「文字」は実はユニコードの概念ではないので、文字とは何かという人間としての直観は、 Rustにおけるchar値が何かとは合致しない可能性があります。この話題については、第8章の「文字列」で詳しく議論しましょう。
Rust の配列
- 配列型:固定長
- ベクター型:可変長
配列は、ヒープよりもスタック(スタックとヒープについては第4章で詳つまびらかに議論します)にデータのメモリを確保したい時、 または、常に固定長の要素があることを確認したい時に有効です。 ただ、配列は、ベクタ型ほど柔軟ではありません。ベクタは、標準ライブラリによって提供されている配列と似たようなコレクション型で、 こちらは、サイズを伸縮させることができます。配列とベクタ型、どちらを使うべきか確信が持てない時は、 おそらくベクタ型を使うべきです。第8章でベクタについて詳細に議論します。
ベクタ型よりも配列を使いたくなるかもしれない例は、1年の月の名前を扱うプログラムです。そのようなプログラムで、 月を追加したり削除したりすることまずないので、配列を使用できます。常に12個要素があることもわかってますからね:
配列の要素へのアクセス
配列は、スタック上に確保される一塊のメモリです。添え字によって、 配列の要素にこのようにアクセスすることができます:
コンパイル時に添字の値が決定されない場合、範囲外要素へのアクセスはコンパイルでは弾けない。実行時エラーとなる。もちろんコンパイル時に添字の値が決定する場合はちゃんと弾いてくれる。
let arr = [1, 2, 3, 4, 5]
// どちらもコンパイルエラーになる
let idx = 7;
println!("arr[7]: {}", arr[idx]);
println!("arr[100]: {}", arr[100]);
/* コンパイルエラー
error: this operation will panic at runtime
--> src/sections/sec02.rs:123:32
|
123 | println!("arr[6]: {}", arr[idx]);
| ^^^^^^^^ index out of bounds: the length is 6 but the index is 6
*/
コンパイルでは何もエラーが出なかったものの、プログラムは実行時エラーに陥り、 正常終了しませんでした。要素に添え字アクセスを試みると、言語は、 指定されたその添え字が配列長よりも小さいかを確認してくれます。添え字が配列長よりも大きければ、言語はパニックします。 パニックとは、プログラムがエラーで終了したことを表すRust用語です。
これは、実際に稼働しているRustの安全機構の最初の例になります。低レベル言語の多くでは、 この種のチェックは行われないため、間違った添え字を与えると、無効なメモリにアクセスできてしまいます。 Rustでは、メモリアクセスを許可し、処理を継続する代わりに即座にプログラムを終了することで、 この種のエラーからプログラマを保護しています。Rustのエラー処理については、第9章でもっと議論します。
関数本体は、文と式を含む
Rust は式指向言語。
- 文:処理は実行するが値を返さない命令
- 式:処理を実行し値が返る(評価される)命令
ブロック
ブロックは式の 1 つ。{
と }
で囲んだもの。
{0}
というブロックは 0 を返す式。ブロックが返す値は省略できる。その場合、ブロックは ()
を返す。この ()
はユニットと言う。つまり、{}
は {()}
ということになる。
ブロックの機能は以下 2 つ。
- スコープの作成
- 文をいくつも記述できる
{ statement; statement; statement; ...; (expression) }
セミコロンは式を文に変化させる記号。
ブロックの例
式と文の違いを認識する。
式は何かに評価され、これからあなたが書くRustコードの多くを構成します。 簡単な数学演算(
5 + 6
など)を思い浮かべましょう。この例は、値11
に評価される式です。式は文の一部になりえます: リスト3-1において、let y = 6
という文の6
は値6
に評価される式です。関数呼び出しも式です。マクロ呼び出しも式です。 新しいスコープを作る際に使用するブロック({}
)も式です:
fn main() {
let y = {
let x = 3;
x + 1
};
println!("The value of y is: {}", y);
}
上記コードにて、以下の式は 4
に評価されるブロック(式)。
{
let x = 3;
x + 1
}
ブロックを評価して得られた値が let
文の一部として y
に束縛される(英訳:That value gets bound to y
as part of the let statement. )。今まで見てきたソースコードの各行と異なり、文末にセミコロンがついていない x+1
の行に注意。式は終端にセミコロンを含まない。式の終端にセミコロンを付けたら、文に変わってしまう。文は値を返さない。
あえて明示的に型注釈を書くと以下のようになる。
-
x + 1
という式でブロックを終える- ブロックの評価値は
4
- ブロックの評価値は
let y: i32 = {
let x = 3;
x + 1
}
println!("y: {:?}", y) // --> "y: 4"
-
x + 1
という式でブロックを終える- ブロックの評価値は
()
(ユニット)
- ブロックの評価値は
let y: () = {
let x = 3;
x + 1
}
println!("y: {:?}", y) // --> "y: ()"
ユニット () とは?
あとで調べる。
↓見ると「空のタプル」って書いてある。
ユニットとユニット構造体は違うの?
戻り値のある関数
Rustでは、関数の戻り値は、関数本体ブロックの最後の式の値と同義です。 returnキーワードで関数から早期リターンし、値を指定することもできますが、多くの関数は最後の式を暗黙的に返します。
以下の five1
、five2
は実質的に同じ処理となる。
fn five1() -> i32 {
// 関数の戻り値=関数本体ブロックの最後の式の値
5
}
fn five2() -> i32 {
// 明示的な return 文による戻り値の指定
return 5;
}
fn main() {
let x1 = five1();
let x2 = five2();
println!("five1: {}", five1)
println!("five2: {}", five2)
}
Rust の if 式
Rust では if 式の条件式の評価値は bool
型でなければならない。条件式が bool
型でない場合、コンパイルエラーとなる。
Python、JavaScript、Ruby など他の言語における "truthy" / "falsy" な値というのがない。個人的には Rust のこの if 式の仕様は好み。
let 文内で if 式を使う
以下のコードはコンパイルエラーになる。
fn main() {
let condition = true;
let number = if condition { 5 } else { "six" };
println!("The value of number is: {number}");
}
変数は単独の型でなければならないため、コンパイル時に型が定まらない変数はそもそもコンパイルが通らない。
ifブロックの式は整数に評価され、elseブロックの式は文字列に評価されます。これでは動作しません。 変数は単独の型でなければならないからです。コンパイラは、コンパイル時にnumber変数の型を確実に把握する必要があるため、 コンパイル時にnumberが使われている箇所全部で型が有効かどうか検査することができるのです。 numberの型が実行時にしか決まらないのであれば、コンパイラはそれを実行することができなくなってしまいます; どの変数に対しても、架空の複数の型があることを追いかけなければならないのであれば、コンパイラはより複雑になり、 コードに対して行える保証が少なくなってしまうでしょう。
loop
break, continue は他の言語とほぼ変わらず。
ループ内にループがある場合、breakとcontinueは最も内側のループに適用されます。 ループラベルを使用することで、breakやcontinueが適用されるループを指定することができます。
loop にラベルを付けて大域脱出(2 つ以上のネストされた loop ブロックから抜け出す)もできる。
fn main() {
let mut count = 0;
'counting_up: loop {
println!("count = {}", count);
let mut remaining = 10;
loop {
println!("remaining = {}", remaining);
if remaining == 9 {
// 内側のループを脱出
break;
}
if count == 2 {
// 大域脱出
break 'counting_up;
}
remaining -= 1;
}
count += 1;
}
println!("End count = {}", count);
}
外側のループには'counting_upというラベルがついていて、0から2まで数え上げます。 内側のラベルのないループは10から9までカウントダウンします。最初のラベルの無いbreakは内側のループを終了させます。 break 'counting_up;は外側のループを終了させます。
Q&A
- 現代の CPU において、Rust の浮動小数点数型である
f32
、f64
がほぼ同スピードなのはなぜ? - Rust ってなんで基礎的な関数(
println!
、assert_eq!
とか)がマクロベースなの?関数として定義しなかったのはなぜ? - loop 内のパターンマッチで
let x = match...
したときに、Err(error)
の方にcontinue
書くとコンパイルが通るのはなぜ?println!
だけだと戻り値の型エラーになる。 - 言語処理系の用語の式、文について理解できてない。
評価される
とはどういう意味の言葉か?「処理は実行するが値は返さない」のが文ではあるが、「処理の実行時」は「評価」とは違うんだよね? - while, loop, for は式?
Rust 用語
- クレート crate
- Cargo によって管理される Rust のパッケージのこと。Python や Node.js などのパッケージの Rust 版の呼び方。
- 関連関数
- 特定の方に対して実装される関数。
[型]::[関数名]
構文によって定義される。 - 例
String::new()
- 特定の方に対して実装される関数。
- Crates.io、レジストリ
- Rust のオープンソースのクレートを管理しているサーバ。レジストリはそのデータのコピー。
-
外部依存を持つようになると、Cargoはその依存関係が必要とするすべてについて最新のバージョンをレジストリから取得します。 レジストリとはCrates.ioのデータのコピーです。 Crates.ioは、Rustのエコシステムにいる人たちがオープンソースのRustプロジェクトを投稿し、他の人が使えるようにする場所です。
- 多分 PyPI みたいなもん
- シャドーイング
- 同一スコープ内に既に宣言されている変数名を再利用して変数を宣言すること。実質的には新しい変数が生成される。
-
guessという名前の変数を作成しています。 しかし待ってください、このプログラムには既にguessという名前の変数がありませんでしたか? たしかにありますが、Rustではguessの前の値を新しい値で覆い隠す(shadowする)ことが許されているのです。 シャドーイング(shadowing)は、guess_strとguessのような重複しない変数を二つ作る代わりに、guessという変数名を再利用させてくれるのです。 これについては第3章で詳しく説明しますが、今のところ、この機能はある型から別の型に値を変換するときによく使われることを知っておいてください。
- 列挙子、バリアント variant
- enum 型の各値のこと
- パニック panic
- プログラムがエラーで終了したことを表す Rust の用語。
ドキュメントへの PR
あとでまとめてやる
3.2 データ型:原著とあっていない
tuple の説明に「固定長」を書いた方が新設
原著
A tuple is a general way of grouping together a number of values with a variety of types into one compound type. Tuples have a fixed length: once declared, they cannot grow or shrink in size.
日本語訳の修正箇所 "Tuples have..." 以降が訳されてない。
タプルは、複数の型の何らかの値を一つの複合型にまとめ上げる一般的な手段です。
3.3 関数:原著とあっていない
This example creates a function named print_labeled_measurement with two parameters. The first parameter is named value and is an i32. The second is named unit_label and is type char. The function then prints text containing both the value and the unit_label.
「引数はどちらも i32 型です」の部分が間違っている。サンプルコードでは 2 つの引数の型が i32
と char
になっている。
この例では、2引数の関数を生成しています。そして、引数はどちらもi32型です。それからこの関数は、 仮引数の値を両方出力します。関数引数は、全てが同じ型である必要はありません。今回は、 偶然同じになっただけです。
メモ書き
Rust と他の言語の比較
なるほどな記事だった。言語比較というより、「言語の特性はこういう観点で整理される」という一例として良い記事だった。言語を俯瞰して見るときに非常に参考になる。Rust の部分は Rust 触ってみないと実感ないから参考程度。
筆者が書いている通り、言語比較自体さまざまな観点からなされるため、そもそも完璧な比較はできない。今回は「実用性」=「プログラミング言語の様々なユースケースで実務レベルで対応できる広さ」として筆者は定義していた。
記事のコメントにもあったが、自分も公式だけでなくサードパーティパッケージが豊富か否かが「実用性」に大きく影響を与える(そもそも JS / TS、Python はここが強いので選択される。Python は numpy が無ければ云々...みたいな)と考えているので、選定の際の観点としては加えてほしいなと思った。広げすぎる話がまとまらなくなるので難しいが、公式から提供されていなくてもデファクトスタンダードが決まっていればコメントとして書いてほしかった(npm とか)。ただし、Go を書いていて公式の手厚い開発環境のサポートがありがたかったので筆者がこの観点を推すのも理解できる。余計なことツール選定に時間取られないのはホント楽。
Cargo.toml のバージョン記法の意味
npm の package.json
でよく見るけど意味知らんかったーー
0.8.3という数字は実際には^0.8.3の省略記法で、0.8.3以上0.9.0未満の任意のバージョンを意味します。 Cargoはこれらのバージョンを、バージョン0.8.3と互換性のある公開APIを持つものとみなします。 この仕様により、この章のコードが引き続きコンパイルできるようにしつつ、最新のパッチリリースを取得できるようになります。 0.9.0以降のバージョンは、以下の例で使用しているものと同じAPIを持つことを保証しません。
ビルド時に起きたエラー(コンパイルエラー以外)
Blocking waiting for file lock on package cache
いつ起きた?
プロジェクトに新たにクレートを追加した(Cargo.toml
の [dependencies]
にクレートを追加した)直後に cargo build
を実行したとき。
原因
エディタで rust-analyzer を使用している場合、保存などをトリガーとして rust-analyzer がバックグラウンドで cargo check
を実行している。このとき、ターミナルから cargo run
などのコマンドで Cargo プロセスを起動すると、この 2 つのプロセスが衝突することがある。
解決方法
rm -rf ~/.cargo/.package-cache
- メモリ効率
- プログラムの実行時間帯でどれだけメモリを効率的に使えたかかを示します。メモリのフットプリントが大きいとメモリ効率が悪く、フットプリントが小さいとメモリ効率が良いものとします。
- メモリ安全性
- バッファオーバーフローやダングリングポインタ等のメモリアクセスに関するバグやセキュリティホールから守られている場合、メモリ安全性を満たしている状態になります。
これコンパイラ通ったんだけど 2 回目の println!
で r
の怒られないのなんで?
pub fn main() {
println!("----- chap02 ~ chap03 -----");
let width = 40;
let height = 70;
let r = Rectangle::create(width, height);
println!("The area of the rectangle is {} square pixels.", area(&r));
// print Rectangle
// これなんでムーブ起こらないの?
println!("r: {:?}", r);
println!("r: {:#?}", r); // pretty print
}
chap05
chap05 構造体
インスタンスの可変性
一部のフィールドのみを可変にすることはできない。
インスタンス全体が可変でなければならないことに注意してください; Rustでは、一部のフィールドのみを可変にすることはできないのです。 また、あらゆる式同様、構造体の新規インスタンスを関数本体の最後の式として生成して、 そのインスタンスを返すことを暗示できます。
その他重要な概念
- タプル構造体
- ユニット様構造体
- 関連関数
- メソッド
フィールドのないユニット様構造体
ユニット ()
は戻り値が省略された際のブロック式のデフォルトの戻り値として使われている。
また、一切フィールドのない構造体を定義することもできます!これらは、()、ユニット型と似たような振る舞いをすることから、 ユニット様構造体と呼ばれます。ユニット様構造体は、ある型にトレイトを実装するけれども、 型自体に保持させるデータは一切ない場面に有効になります。トレイトについては第10章で議論します。
ここ、まだちょっとわかんないね。よく出てくるけど「トレイト trait」って用語が全く分かってない。
ユニット様構造体は、ある型にトレイトを実装するけれども、 型自体に保持させるデータは一切ない場面に有効になります
「型自体に保持させるデータは一切ない」ってのは多分↓みたいな感じかな?(例が良くなさそうだけど)
pub struct Utils;
impl Utils {
pub fn utc_to_jst()...
}
構造体の所有権
構造体のフィールドには当然参照を持たせることもできる。しかし、これには Rust の「ライフタイム lifetime」という概念、機能を理解する必要があり、ここでは触れない。第 10 章で構造体に参照を保持する方法について議論する。
構造体に、他の何かに所有されたデータへの参照を保持させることもできますが、 そうするにはライフタイムという第10章で議論するRustの機能を使用しなければなりません。 ライフタイムのおかげで構造体に参照されたデータが、構造体自体が有効な間、ずっと有効であることを保証してくれるのです。 ライフタイムを指定せずに構造体に参照を保持させようとしたとしましょう。以下の通りですが、これは動きません:
struct User {
username: &str,
email: &str,
sign_in_count: u64,
active: bool,
}
fn main() {
let user1 = User {
email: "someone@example.com",
username: "someusername123",
active: true,
sign_in_count: 1,
};
}
構造体のメソッドの第一引数
-
self
:所有権を奪う -
&self
:所有権を奪わない。読み込み専用。 -
&mut self
:所有権を奪わない。読み書き。
->
演算子はどこに行ったの?
CとC++では、メソッド呼び出しには2種類の異なる演算子が使用されます: オブジェクトに対して直接メソッドを呼び出すのなら、.を使用するし、オブジェクトのポインタに対してメソッドを呼び出し、 先にポインタを参照外しする必要があるなら、->を使用するわけです。 言い換えると、objectがポインタなら、object->something()は、(*object).something()と同等なのです。
Rustには->演算子の代わりとなるようなものはありません; その代わり、Rustには、 自動参照および参照外しという機能があります。Rustにおいてメソッド呼び出しは、 この動作が行われる数少ない箇所なのです。
動作方法はこうです: object.something()とメソッドを呼び出すと、 コンパイラはobjectがメソッドのシグニチャと合致するように、自動で&か&mut、*を付与するのです。 要するに、以下のコードは同じものです:
p1.distance(&p2);
(&p1).distance(&p2);
前者の方がずっと明確です。メソッドには自明な受け手(selfの型)がいるので、この自動参照機能は動作するのです。 受け手とメソッド名が与えられれば、コンパイラは確実にメソッドが読み込み専用(&self)か、書き込みもする(&mut self)のか、 所有権を奪う(self)のか判断できるわけです。メソッドの受け手に関して借用が明示されないというのが、 所有権を実際に使うのがRustにおいて簡単である大きな理由です。
コンパイラ側で自動で借用の解釈を行ってくれる。
5 章終わり🎉
Rust におけるテスト
単体テスト
- 特定のモジュールのテストのみ実行する
cargo test -- --test [module name]
# e.g.
cargo test -- --test types::rectangle
つづきはこっちで