はじめに
Rust は良い言語です。
良い言語ですが学習コストが高いと言われており、実際に自分も適当にググりながら Rust を使ってみようとして一度挫折しました。
Rust の学習コストが高い理由は言語仕様で速度、並行性、安全性を保証していることに関係があります。
普通のプログラミング言語であれば適当に遅くて危険なコードを書いてもとりあえず実行することはできますが、Rust は遅かったり危険だったりする状態でコードが実行されないように厳しいルールを設けており適当に書いたコードはコンパイルすら通りません。
つまりは「他のプログラミング言語で安全で高速なコードを書けるようになるまでの学習コスト」と「Rust で普通のコードを書けるようになるまでの学習コスト」が同程度になるのではないかと個人的には感じており、適当にググって書くのは厳しいがオライリーの「プログラミング Rust」を読めば理解できる程度だと思います。
コンパイルが通っているだけである程度の速度や安全性が保証されているというのは Rust のいいところだと思いますが、初心者が挫折をして諦めてしまうのはもったいないので、挫折する前に特に概念を理解しづらい所有権システムについてわかりやすい読み物があるとうれしいかと思いこの記事を書いてみようと思いました。
自分も勉強中なのでまちがっているところがあればご指摘歓迎です。
所有権システムはなぜ難しいか
所有権・借用・ライフタイムなどの所有権システムが難しく感じられる理由は以下のようにいくつかあると思います。
- なんのためにあるものなのかよくわからない
- どういうことなのか概念がよくわからない
- ルールが多いのでつらい
次節からひとつづつ解説をしていきます。
所有権システムはなんのためにあるのか
自分が初めて所有権について学習しようとしたときに見つけたウェブページなどでは具体的なルールについての解説が多く、そもそもなんなのか?なんのためにあるのか?がいまいちわからなかったのでよく頭に入ってきませんでした。
そこでざっくりとした概要から話をしていきたいと思います。
まず、所有権システムがなんのためにあるのかというと「高速で安全にメモリ管理をするため」です。
値が変数に所有されるとか、所有権が移動するとか、変数のスコープを意識して借用を行うなど聞き慣れない細かいルールは、すべてこの「高速で安全にメモリ管理をする」という目的のために作られています。
例えば、所有権によってメモリを確保するタイミングと開放するタイミングがコンパイル時に決定するので実行時のオーバーヘッドがなく高速にメモリ管理できます。
例えば、所有権によって値が複数箇所から同時に変更されなくなるので安全にメモリのデータが管理されます。
「値がたった一つの変数に束縛される」というだけのルールでこれらの複数の効用があるというのはよくできていますね。
一方で「この仕様はこの目的のためにある」というのが分かりづらくはあるので、ひとつひとつのルールを見るだけだと全体像を想像しづらくなっているのが難しく感じる原因かもしれません。
所有権システムとはどういうことなのか
- 所有権・所有権の移動・借用・ライフタイムの概念について簡単に説明していきます
所有権とは
- 所有権とは「ある値はあるひとつの変数に所有され、この変数からしか読み込みも書き込みも行われない」というルールのことです
- このルールが有ることによって「変数が宣言されたブロックでの処理が終了」すると「変数に所有された値は開放することができる」ということになります
- これにより値のためのメモリ確保と開放のタイミングがコンパイル時に決定されるため、ガーベッジコレクションのように実行時にメモリ解放の判定を行う場合に比べて高速になります
- また、所有権のルールによって「値が複数変数から同時に更新されて壊れる」という不具合であったり「値を開放し忘れてメモリがリークする」という不具合を防ぐことができて安全性が高まります
fn example() {
let v1 = 10; // v1 は 10 を所有する
let v2 = "hello"; // v2 は "hello" を所有する
} // v1, v2 が無効になり値が開放される
所有権の移動とは
- 所有権の移動とは「変数Aから変数Bへの代入では、変数Aの所有する値の所有権を変数Bへ移動させ、変数Aを空(未初期化状態)とする」というルールのことです
- はじめて聞いたときはかなり変わったルールだと思いましたが、移動がないと複数の変数から同じ値を所有できるようになるなど所有権ルールを守るのが難しくなるので仕組み上必要なものなのだと思います。
- また「未初期化扱いの変数は利用できない」というルールがあるので、意図せずに所有権を移動したあとに移動元の変数を利用しようとするとコンパイルエラーになります。
- このルールは面倒に感じますが、null 参照エラーがコンパイル時に防がれるというのは Rust の魅力であります
fn main() {
let v1 = vec![1, 2, 3]; // v1 はベクター [1, 2, 3] を所有する
let v2 = v1; // ベクター [1, 2, 3] の所有権は v1 から v2 に移動する. v1 は未初期化状態になる
println!("{:?}", v2); // [1, 2, 3] と表示される
// println!("{:?}", v1); // 未初期化状態の v1 を利用するとエラーになる
} // v2 が無効になり所有するベクター [1, 2, 3] が開放される
借用とは
- ある変数の所有する値を使おうとしたときに毎回毎回所有権を移動させてしまうと面倒だということで「「読み込み権限」あるいは「読み込み書き込み権限」を期間限定で貸し出せるようにする」というのが借用のルールです
- 単純にポインターを貸し出すというだけではなく、貸し出されたポインターと本体の値が安全に取り扱われるように副次的なルールがいくつも用意されています。
- 貸出元が貸出先より先に無効になると、所有権を持つ値を安全に開放できなかったり、値のないポインタが参照される可能性がある、など不都合が多いので「貸出先の宣言されたスコープは貸出元の宣言されたスコープよりも狭くなければならない」という追加ルールがあります
- また、値の整合性を保つために「読み込み権限貸出中は値の変更はできない」「読み込み権限貸出中は読み込み書き込み権限は貸出できない」「各権限の貸出中は所有権の移動はできない」などなど細かい追加ルールが有ります。
- いまいち全部把握できていませんが、値が壊れるような操作はできないという理解をしておけばいいと思います。
fn main() {
let mut v1 = 10; // v1 は 10 を所有する
{
let v2 = &mut v1; // v1 の読み書き権限を v2 に貸し出す
*v2 = 15; // 貸し出された値を更新する
println!("{}", v2); // 15 と表示される
} // v2 が無効になり借用された権限が開放される
let v3 = &v1; // v1 の読み込み権限を v3 に貸し出す
println!("{} {}", v1, v3); // v1, v3 ともに参照することができ 15, 15 と表示される
}
ライフタイムとは
- 変数は宣言されたスコープに束縛され、処理がスコープを抜ける際に所有する値の開放が行われたり、束縛されたスコープより小さいスコープの値を借用できないなど、変数とスコープの間には密接な関係があります。
- ライフタイムとはこのスコープを抽象化して扱うための仕組みで、ジェネリクスで変数の所属するスコープに名前をつけて取り扱うことができます。
fn main() {
println!("{}", x_or_y("hello", "world")); //=> "hello"
}
// 関数 x_or_y では 'a と 'b という2つのスコープを扱うことを `x_or_y<'a, 'b>` で宣言します
// `_x: &'a str` で変数 _x はスコープ 'a に所属する文字列型であることを宣言します. _y と返り値の方も同様です
fn x_or_y<'a, 'b>(_x: &'a str, _y: &'b str) -> &'a str {
// 返り値の宣言の通りスコープ 'a の変数 _x を返却しているのでコンパイルが成功します
// return _y; とするとコンパイルエラーになります
return _x;
}
所有権システムのルールが多くてつらい
- 以上の内容でなんとなく所有権システムの概念がつかめたらオライリーの「プログラミング Rust」や プログラミング言語Rust: 2nd Edition などで詳細な仕様について順に学習していけばよいのではないでしょうか。
おわりに
- 本当はスタックやヒープなどメモリ構造から理解していったほうが良くはあるとは思いますが、まずは概要を掴むためにとりあえずそのへんはスキップしました。
- 繰り返しになりますが、自分も勉強中なので間違っている所があればご指摘いただけるとうれしいです。