https://xtech.nikkei.com/atcl/nxt/column/18/01920/051200009/
https://xtech.nikkei.com/atcl/nxt/column/18/01920/051200009/
前回はRustに「所有権」という概念があることを学んだ。もう少し一般化した規則を学んでいこう。
Rustでは変数の値の所有権が移動(ムーブ)する。ムーブ後、どこかのタイミングで値の生存期間が終わるとそこで値は解放され、以降の処理では一切アクセスできなくなる。
ムーブは次の個所で発生する。
- パターンマッチ(match式だけでなく変数の束縛も含む)
- 関数呼び出し
- 関数やブロックからのリターン
- コンストラクター(構造体やenumの作成時に使用するもの)
- クロージャーのmoveキーワード
ムーブした後は元の変数では値を使ったり参照したりすることはできない。解放されているからだ。
Rustではムーブを利用することを「ムーブセマンティクス」と呼ぶ。
所有権の移動について、簡潔な例で確認しておこう。次のコードでは、変数「s」「t」「u」の順に値を束縛し、意図的に所有権を移動させている。
fn main() {
// s に「text」という文字列の値を束縛
let s = String::from("text");
// tにsの値を束縛
// この時点でsは値が解放済みになる
let t = s;
// uにtの値を束縛
// この時点でtは値が解放済みになる
let u = t;
// これらは解放済みなのでコメントアウトするとコンパイルエラー
// println!("{}", s);
// println!("{}", t);
// これはOK
println!("{}", u);
}
これを模式図にしてみると、次のような動きになっている。
まず、変数sに値を束縛すると、その値に対する所有権が生まれる。
次に変数tにsの値を束縛する。すると、sが持っていた「text」の所有権がtに移る。
最後に変数uにtの値を束縛すると、今度は所有権がuに移動する。
型によってムーブかコピーかが変わる
前回の「move occurs because `article` has type `Article`, which does not implement the `Copy` trait」というコンパイルエラーのメッセージは、値の「コピー」に関するものだ。実はRustでは、一部の型は「コピーセマンティクス」に対応している。ムーブではなくコピーを利用するという意味だ。
次のプログラムを実行してみてほしい。これまで説明してきたムーブセマンティクスの例から「このコードもコンパイルエラーになるのではないか」と思うかもしれない。
fn main() {
let s: i32 = 1;
let t = s;
let u = t;
println!("{}", s);
println!("{}", t);
println!("{}", u);
}
ところが、このコードは問題なく実行され、3つの「1」が表示される。理由は、Rustのi32型にはコピーセマンティクスが適用されるためだ。
変数tにsの値を束縛しようとしたときには、所有権がムーブするのではなく、所有権が残ったまま値がコピーされる。変数uにtの値を束縛しようとしたときも同様だ。したがって、3つの変数がすべて値にアクセスできる。
コピーセマンティクスは、「Copy」というトレイトが実装された型に適用される。このトレイトが実装されていることが、コピーできることを示すのだ。Copyトレイトが実装されていなければ、常にムーブセマンティクスが適用される。
Rustでは、i32などのプリミティブ型にはCopyトレイトがあらかじめ実装されている。したがって先のプログラムでは、明示的にCopyトレイトを実装しなくてもコピーセマンティクスが適用され、コンパイルエラーにならなかった。
Rustの標準ライブラリーでCopyトレイトを実装している型の一覧は、Copyトレイトのドキュメントで確認できる。
参考リンク:Trait std::marker::Copy型によってムーブかコピーかが変わる
前回の「move occurs because `article` has type `Article`, which does not implement the `Copy` trait」というコンパイルエラーのメッセージは、値の「コピー」に関するものだ。実はRustでは、一部の型は「コピーセマンティクス」に対応している。ムーブではなくコピーを利用するという意味だ。
次のプログラムを実行してみてほしい。これまで説明してきたムーブセマンティクスの例から「このコードもコンパイルエラーになるのではないか」と思うかもしれない。
fn main() {
let s: i32 = 1;
let t = s;
let u = t;
println!("{}", s);
println!("{}", t);
println!("{}", u);
}
ところが、このコードは問題なく実行され、3つの「1」が表示される。理由は、Rustのi32型にはコピーセマンティクスが適用されるためだ。
変数tにsの値を束縛しようとしたときには、所有権がムーブするのではなく、所有権が残ったまま値がコピーされる。変数uにtの値を束縛しようとしたときも同様だ。したがって、3つの変数がすべて値にアクセスできる。
コピーセマンティクスは、「Copy」というトレイトが実装された型に適用される。このトレイトが実装されていることが、コピーできることを示すのだ。Copyトレイトが実装されていなければ、常にムーブセマンティクスが適用される。
Rustでは、i32などのプリミティブ型にはCopyトレイトがあらかじめ実装されている。したがって先のプログラムでは、明示的にCopyトレイトを実装しなくてもコピーセマンティクスが適用され、コンパイルエラーにならなかった。
Rustの標準ライブラリーでCopyトレイトを実装している型の一覧は、Copyトレイトのドキュメントで確認できる。
参考リンク:Trait std::marker::Copy借用を使って低コストで問題を解決
実はRustには、この問題を低コストで解決する手段が用意されている。所有権を移動させることなく「借りる」ことができるのだ。これを「借用」と呼ぶ。所有権を持つ側から見れば「貸す」、所有権を受け取る側から見れば「借りる」という関係性になる。
Rustの借用は「参照(reference)」を利用する。参照は、CやC++といったプログラミング言語を使ったことがある人にはおなじみの概念だろう。借用では、複製するのではなくメモリー上にある同じ値を参照する。
借用を利用すると、先ほどのプログラムを次のように書き換えられる。
struct Article {
title: String,
description: String,
author: String,
body: Vec<u8>,
}
fn print_meta_info(article: &Article) {
println!("title: {}, author: {}", article.title, article.author);
println!("description: {}", article.description);
}
fn display_body(article: &Article) {
println!("{}", std::str::from_utf8(&article.body).unwrap());
}
fn main() {
let article = Article {
title: "Rustで作るWebアプリケーション".to_string(),
description: "Rustで実際にWebアプリケーションを作りながら、Rustの魅力を伝える記事です。"
.to_string(),
author: "Yuki Toyoda".to_string(),
body: "ここに記事の内容が入ります。以下続く。".as_bytes().to_vec(),
};
// articleの値の所有権を貸し出すだけで所有権は移動しない
print_meta_info(&article);
// 所有権が移動しなかったので値が解放されておらずアクセスできる
display_body(&article);
}
参照を知っていれば、当たり前の解決策に見えるかもしれない。CやC++では日常的に使われている手法だ。ただし、Rustの参照はコンパイラーによる安全性チェックを通らなければならない。これにより、少ない負荷で安全なコードを実現できる。
何が行われたかについて確認しよう。まず、main関数で変数articleがArticle構造体の値の所有権を持つ。
次にprint_meta_info関数にarticle変数の所有権を貸し出す。所有権のムーブを使ったコードでは所有権がこの時点で移動していたが、ここではarticle変数が値の所有権を持ち続けている。
print_meta_info関数が処理を終え、main関数に処理を戻す際もまだarticle変数の値への所有権は残っている。
値が解放されていないため、display_body関数はarticle変数の持つ値を利用できる。
借用を利用したパターンでは値の所有権が残り続けるため、メモリー上の値が解放されることはない。この例では、値はmain関数の処理の最後まで生存する。
Rustで参照を利用する際には、参照の生存期間である「ライフタイム」を考慮する必要がある。次回はライフタイムについて説明しよう。
0 コメント:
コメントを投稿