こんにちは!
お盆休み暇だったので、新しい言語を何か学ぼうと思い、Rustに手を出してみました。
今回はその学習メモとして、備忘録的な記事を書きたいと思います。
Rustを始めてまだ一週間の状態で記事を書いていますので、不正確な記述などあるかもしれません。
ポエムだと思ってお楽しみください。
Rustのコンパイラは厳しい
当方、Python、JavaScript、TypeScript、Kotlin、Goなど、いろいろな言語を触ってきましたが、Rustのコンパイラはダントツで厳しい!
所有権システムによって厳格にコードが検査されるので、たいていの言語だと許されるものが許されないです。
そこで、Rustコンパイラに怒られたことを備忘録として記事にしたいと思います。
所有権の移動
fn main() {
let aisatsu = String::from("こんにちは");
let aisatsu_2 = aisatsu;
println!("{}", aisatsu) // コンパイルエラー!
}
エラーメッセージ
error[E0382]: borrow of moved value: `aisatsu`
--> src/main.rs:5:20
|
2 | let aisatsu = String::from("こんにちは");
| ------- move occurs because `aisatsu` has type `String`, which does not implement the `Copy` trait
3 | let aisatsu_2 = aisatsu;
| ------- value moved here
4 |
5 | println!("{}", aisatsu)
| ^^^^^^^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
|
3 | let aisatsu_2 = aisatsu.clone();
| ++++++++
まず2行目で"こんにちは"文字列が作成され、文字列のデータはヒープ領域に置かれるのですが、そのメモリ領域の所有権は「aisatsu」変数が持ちます。
let aisatsu = String::from("こんにちは");
3行目で「aisatsu」を「aisatsu_2」に代入すると、"こんにちは"文字列のメモリの所有権も「aisatsu_2」に移動します。この際、「aisatsu」変数は未初期化の状態に戻ってしまいます。
let aisatsu_2 = aisatsu;
なので、最後の行でprintしようとした時、「aisatsu」変数が未初期化状態なので、エラーとなってしまいます。
println!("{}", aisatsu)
解消するには、クローン(他の言語でいうディープコピー)してあげるとよいです。
fn main() {
let aisatsu = String::from("こんにちは");
let aisatsu_2 = aisatsu.clone(); // cloneをする
println!("{}", aisatsu) // コンパイルOK!
}
もちろん、文字列がまるまるコピーされるので、その分コストかかるので、ディープコピーしたい時の対応って感じですね。
Rustは一部の基本的な型を除いて、暗黙的なコピーは行われない方針なので、明示的にコピーをしなければならないです。
また、「aisatsu」変数は未初期化状態なので、他の文字列を再代入して初期化し直してあげることもできます。
fn main() {
let mut aisatsu = String::from("こんにちは"); // 再代入できるようにmutをつける
let aisatsu_2 = aisatsu;
aisatsu = String::from("こんばんは"); // 再代入
println!("{}", aisatsu) // コンパイルOK!
}
不変参照と可変参照
Rustには、所有権はもらわずに、参照だけ借りる仕組みがあります。
参照を使うと前述のような問題は起きなくなるのですが、また別の落とし穴が。
fn main() {
let mut aisatsu = String::from("こんにちは");
let aisatsu_2 = &aisatsu;
let aisatsu_3 = &mut aisatsu; // コンパイルエラー!
println!("{}", aisatsu_2)
}
エラーメッセージ
error[E0502]: cannot borrow `aisatsu` as mutable because it is also borrowed as immutable
--> src/main.rs:5:21
|
4 | let aisatsu_2 = &aisatsu;
| -------- immutable borrow occurs here
5 | let aisatsu_3 = &mut aisatsu;
| ^^^^^^^^^^^^ mutable borrow occurs here
6 |
7 | println!("{}", aisatsu_2)
| --------- immutable borrow later used here
4行目は読み取り専用の参照。
let aisatsu_2 = &aisatsu;
5行目は書き換えも可能な参照。
let aisatsu_3 = &mut aisatsu;
Rustでは、特定のスコープの中に、書き換え可能な参照と読み取り専用の参照が混在するとエラーになってしまいます。
データ競合の発生を防ぐためというのが理由なようです。
詳しくはこちら。
同様に、書き換え可能な参照を2つ以上持ってもエラーになります。
fn main() {
let mut aisatsu = String::from("こんにちは");
let aisatsu_2 = &mut aisatsu;
let aisatsu_3 = &mut aisatsu; // コンパイルエラー!
println!("{}", aisatsu_2)
}
エラーメッセージ
error[E0499]: cannot borrow `aisatsu` as mutable more than once at a time
--> src/main.rs:5:21
|
4 | let aisatsu_2 = &mut aisatsu;
| ------------ first mutable borrow occurs here
5 | let aisatsu_3 = &mut aisatsu;
| ^^^^^^^^^^^^ second mutable borrow occurs here
6 |
7 | println!("{}", aisatsu_2)
| --------- first borrow later used here
プログラマは常に、参照が読み取り専用だけで十分か、書き換えも必要ならそれを一つだけに限定できるか、注意を払わなければなりません。
他の言語から来た人にとっては凄く厳しいルールに見えますが、マルチスレッドのデータ競合を防ぐためには必要なルールなのでしょう。
借用後の変更
関連して、「aisatsu」の参照を「aisatsu_2」に貸し出した後、「aisatsu」を変更しようとするとエラーになります。
fn main() {
let mut aisatsu = String::from("こんにちは");
let aisatsu_2 = &aisatsu;
aisatsu.push_str("おはよう"); // コンパイルエラー!
println!("{}", aisatsu_2)
}
エラーメッセージ
error[E0502]: cannot borrow `aisatsu` as mutable because it is also borrowed as immutable
--> src/main.rs:6:5
|
4 | let aisatsu_2 = &aisatsu;
| -------- immutable borrow occurs here
5 |
6 | aisatsu.push_str("おはよう");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
7 |
8 | println!("{}", aisatsu_2)
| --------- immutable borrow later used here
人に貸し出し中のものを勝手にいじってはいけない、みたいなルールですね。
参照は所有権のないポインタなので、複数箇所から同時に読み書きされないよう、しっかりと守られているようです。
ベクター型
借用後の変更のトピックスに関連して、ベクター型での話題をひとつ。
fn main() {
// ベクター型の作成
let mut numbers: Vec<i32> = vec![1, 2, 3, 4];
// 最初の要素の参照を取得
let first_val = &numbers[0];
// 値の追加
numbers.push(5);
println!("{}", first_val) // コンパイルエラー!
}
エラーメッセージ
error[E0502]: cannot borrow `numbers` as mutable because it is also borrowed as immutable
--> src/main.rs:6:5
|
4 | let first_val = &numbers[0];
| ------- immutable borrow occurs here
5 |
6 | numbers.push(5);
| ^^^^^^^^^^^^^^^ mutable borrow occurs here
7 |
8 | println!("{}", first_val)
| --------- immutable borrow later used here
上の場合も、読み取り専用の参照を貸し出した後で「numbers」をいじっているので、コンパイルエラーとなります。
では、これがもしエラーにならず、コンパイルできてしまった場合どうなるでしょうか。
Vector型は動的にメモリを確保する可変長配列ですので、pushを呼んだ際に、新しいメモリ割り当てが発生し、古い配列が丸っと新しいメモリ領域に移動するかもしれません。
もしそうなったら、「first_val」変数が参照しているメモリアドレスが開放済みアドレスとなるかもしれず、予期せぬ不具合に繋がる危険があります。
厳しいルールによって、危険な状況が防がれているのです。
関数と所有権
関数を呼び出す際にも所有権の移動は発生します。
fn main() {
let aisatsu = String::from("こんにちは");
print_message(aisatsu);
println!("{}", aisatsu) // コンパイルエラー!
}
fn print_message(msg: String) {
println!("{}", msg)
}
エラーメッセージ
error[E0382]: borrow of moved value: `aisatsu`
--> src/main.rs:6:20
|
2 | let aisatsu = String::from("こんにちは");
| ------- move occurs because `aisatsu` has type `String`, which does not implement the `Copy` trait
3 |
4 | print_message(aisatsu);
| ------- value moved here
5 |
6 | println!("{}", aisatsu)
| ^^^^^^^ value borrowed here after move
|
note: consider changing this parameter type in function `print_message` to borrow instead if owning the value isn't necessary
--> src/main.rs:9:23
|
9 | fn print_message(msg: String) {
| ------------- ^^^^^^ this parameter takes ownership of the value
| |
| in this function
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
|
4 | print_message(aisatsu.clone());
| ++++++++
上の例だと、「print_message」関数を呼んだ時、引数に渡された「aisatsu」変数の所有権も「print_message」関数の引数msgに移ってしまっています。
なので、6行目の、
println!("{}", aisatsu)
この段階では、「aisatsu」変数が未初期化状態に戻っているので、コンパイルエラーとなるわけです。
解決策としては、先ほども登場した参照にしてあげることでしょうか。
fn main() {
let aisatsu = String::from("こんにちは");
print_message(&aisatsu); // 所有権ではなく参照を渡す
println!("{}", aisatsu)
}
fn print_message(msg: &String) { // 引数の型を参照に変更
println!("{}", msg)
}
ループ内での関数呼び出し
同様に、ループ内で関数を呼び出そうとした時も、所有権が渡ってしまっているとコンパイルエラーになります。
fn main() {
let aisatsu = String::from("こんにちは");
for _ in 0..10 {
print_message(aisatsu) // コンパイルエラー!
}
}
fn print_message(msg: String) {
println!("{}", msg)
}
エラーメッセージ
error[E0382]: use of moved value: `aisatsu`
--> src/main.rs:5:23
|
2 | let aisatsu = String::from("こんにちは");
| ------- move occurs because `aisatsu` has type `String`, which does not implement the `Copy` trait
3 |
4 | for i in 0..10 {
| -------------- inside of this loop
5 | print_message(aisatsu)
| ^^^^^^^ value moved here, in previous iteration of loop
|
この場合も、参照渡しにしてあげることで解決できます。
fn main() {
let aisatsu = String::from("こんにちは");
for _ in 0..10 {
print_message(&aisatsu) // 参照を渡す
}
}
fn print_message(msg: &String) { // 参照型に変更
println!("{}", msg)
}
まとめ
まだ学び始めたばかりでコンパイラに怒られまくっていますが、少しずつRustの気持ちが分かってきました。
コンパイラが教えてくれることは、本来プログラマが考えないといけない、メモリの大事な話ですから、しっかりとメモリを意識して正しいコーディングを学びたいです。