Rustはトレイト(ScalaのトレイトやHaskellの型クラスのようなもの)に基づく安全で強力な型システム、並行計算、高速な実行などを売りにした新しい言語であるが、他のメジャーな言語にはない特徴として、メモリの管理を静的にチェックする機構を持っていて、デフォルトでデータをスタックに置くことでガベージコレクションを使わないプログラムを書ける。だがこの静的メモリ管理の仕組みがなかなか理解するのが難しい。(個人的な印象。以前に何回か挫折した。)
具体的には、所有権(ownership)、参照による借用(borrowing)、参照の寿命(lifetime)、可変性(mutability)といった概念で、Rustの鬼門だと思われる。
基本的には公式ドキュメントに書いてあることであるが、自分が理解するためという目的もあり、所有権、借用、可変性について以下にまとめてみる。
所有権(ownership)の移動
まず、ある変数が持つ構造体の値を別の変数に束縛(bind)すると、それは所有権(ownership)の移動が起こり(moveセマンティクス)、代入元の変数はそれ以降使えなくなる。一つの値についての所有権を複数の変数が同時に持つことはできない。
struct Point {
x: i32,
y: i32
}
fn main() {
let a = Point{x: 100, y: 230};
let b = a;
println!("{}, {}", a.x, a.y); // => コンパイルエラー(この行を除けばコンパイルが通る)
}
ここでは、Point
の値に対するa
の所有権がb
に移っている
関数に引数を渡す時も同様。この際、構造体のデータの複製は起こらず、単にポインタを複製しているだけである。いわば、暗黙の参照渡しであるが、所有権を手放してしまうところがRustの特徴的なところだ。この仕組みは、「参照渡しはタダ(無制限にできる)」が当たり前の世界に生きている人たち(僕自身含む)にとっては、先入観にとらわれて理解しづらいところかもしれない。
struct Point {x: i32, y: i32}
fn func(v: Point) {
println!("{}, {}", v.x, v.y);
}
fn main() {
let a = Point{x: 100, y: 230};
func(a);
println!("{}, {}", a.x, a.y); // => コンパイルエラー(この行を除けばコンパイルが通る)
}
ただし、Copy
トレイトの型は代入しても所有権が移動するのではなく、データが複製され、代入先が新しい値の所有権を持つ。代入元も元の値の所有権を依然として持つ。プリミティブ型(数値とブーリアン)はすべてCopy
なので、以下はコンパイルが通る。
fn main() {
let a = 1;
let b = a;
println!("{}", a);
}
参照(reference)と借用(borrowing)
上で見たように、(Copy
でない構造体の場合に)他の変数に値を束縛(関数呼び出しを含む)したら所有権が移ってその後使えなくなってしまう。それを防ぐためには、&
で参照を取得して、それを渡す。以下のようになる。
struct Point {x: i32, y: i32}
fn func(p: &Point) -> i32 {
p.x - p.y
}
fn main() {
let a = Point{x: 100, y: 230};
let b = func(&a);
println!("{}, {}, {}", a.x, a.y, b);
}
この仕組みを借用(borrowing)と呼ぶ。借用は所有権の移動を伴わないので、func
を呼んだ後も呼び出し元はaを使える。
借用というからには返却がある。返却は、参照を借用した変数(上記の場合func
の中のp
)がスコープを出るところで自動で起こる。
借りたものを返す前に貸出元がなくなってしまうのはおかしいので、借りた側の変数のスコープが貸した側の値のスコープより後まで続くとコンパイルエラーになる(以下の例)。
struct Point {x: i32, y: i32}
fn main() {
let mut y = &0;
{
let a = Point{x: 100, y: 230};
y = &a.y; // => コンパイルエラー。aのスコープがyのスコープより早く終わってしまうため。
}
println!("{}", y);
}
上とよく似た下の例は参照ではないので問題ない。
struct Point {x: i32, y: i32}
fn main() {
let mut y = 0;
{
let a = Point{x: 100, y: 230};
y = a.y;
}
println!("{}", y);
}
可変性(mutability)
ここまで一回も、変数に別の値を再代入したり、Pointのフィールドを変更するということは全くしてこなかった。変数はデフォルトで不変であるので、変数の値や構造体のフィールドを可変にしたい場合はmut
を使う必要がある。
- 可変(mutable)な束縛
let mut
で変数に束縛することで、再代入したり、フィールドを変更したりできる。
struct Point {x: i32, y: i32}
fn main() {
let mut a = Point{x: 100, y: 230};
a.x = 80; // => どっちもOK
a = Point{x:10, y: 200}; // => どっちもOK
println!("{}, {}", a.x, a.y);
}
- 可変な参照
参照を得るときに&mut
を使うことでmutableな参照を得ることができ、それを受け取った側は値を変更できる。mutableな参照を得られるのはmutableな変数に対してのみ。
struct Point {x: i32, y: i32}
fn func(p: &mut Point) -> i32 {
p.x - p.y
}
fn main() {
let a = Point{x: 100, y: 230};
func(&mut a); // => コンパイルエラー(この行を除けばコンパイルが通る)
println!("{}, {}", a.x, a.y);
}
上の例は、最初からa
をlet mut a =
としてmutableな変数にしてやるか、あるいは以下のように一旦別のmutableな変数b
に所有権を移動してやる(そしてa
は使えなくなる)ことでコンパイルが通る。
struct Point {x: i32, y: i32}
fn func(p: &mut Point) { p.x -= 30; }
fn main() {
let a = Point{x: 100, y: 230};
let mut b = a;
func(&mut b);
println!("{}, {}", b.x, b.y);
}
なお、**可変な参照を誰かが借用している間は、それが返却されるまで不変な参照を含め他の参照は一切取得できない。**この規則によって、ある値に対して書き込み権限を持つ変数は最大1個であり、書き込み中は他の変数は読むことすらできないという、並行計算に必要な条件が確保できる。
struct Point {x: i32, y: i32}
fn func(p: &mut Point) { p.x -= 30; }
fn main() {
let mut a = Point{x: 100, y: 230};
let b = &mut a;
let c = &a; // => コンパイルエラー(この行を除けばコンパイルが通る)
func(b);
println!("{}, {}", b.x, b.y);
}
余談:コレクションでの要素の扱い
配列であるVec<T>
に関してその要素の所有権がどうなるのか見てみた。
Vec<T>::push
は以下のシグネチャを持つ。
fn push(&mut self, value: T) {...}
シグネチャから分かるように、配列(Vec<T>
)にpush
で値を追加すると、値の所有権は配列に移動する。そのため、以下の様なコードはエラーになる。
#[derive(Debug)]
struct Point {x: i32, y: i32}
fn main() {
let mut vec = Vec::new();
let a = Point{x: 2, y: 2};
vec.push(a);
println!("{:?}", vec);
println!("{}", a.x); // => use of moved value: `a.x`というコンパイルエラー(この行を除けばコンパイルが通る)
}
配列に値を追加する際には値を所有権ごと受渡して、後でその値を使う場合は配列から借用して使うイメージである。
HashMap<K,V,S>
(辞書、連想配列)も同様である。値を追加するinsert
のシグネチャは以下のようであり、HashMap中に所有権が移動される。
fn insert(&mut self, k: K, v: V) -> Option<V>
まとめ
- ある値について所有権を持つのはただ一つの変数のみ。
- 不変な参照は同時に複数作れる。
- 可変な参照は同時に一つしか作れず、可変な参照を一つ作るとそれを返却する(スコープを抜ける)までそれ以外のいかなる参照も作れない。
少しずつ言語仕様も熟してきたのか、以前学ぼうとして挫折した時よりはポインタ周りが分かり易くなっているような気がする。所有権・参照・可変性のことを理解していないとコレクションすら使えない(コンパイルが通らない)のでRustを初めて触る人には結構きつい。