Rust の所有権や借用に関する学習メモ。
Rust のバージョンは1.52.1
、Edition は2018
で動作確認している。
所有権
変数を値に束縛すると、変数はその値の所有者であり、その値の所有権を持っていると表現される。
以下のコードでは、_x
は"foo"
の所有者であり、"foo"
の所有権を持っている。
fn main() { let _x = "foo".to_string(); }
変数がスコープから外れたら、その変数が所有している値は破棄される。言い換えれば、メモリが解放される。
Rust では{}
でスコープを作れるため、以下のような挙動になる。
fn main() { { let _x = "foo".to_string(); // メモリに "foo" が格納される } // _x がスコープから外れ、その際に "foo" がメモリから破棄される }
このような方法でメモリを管理することで、Rust ではガベージコレクタが不要になっている。
そして、ある値の所有者はひとつしか存在しないようにすることで、安全にメモリを管理できるようにしている。
=
を使うと、右オペランドの変数が持っている所有権が、左オペランドに移る。
以下の例では、_x
が持っている所有権が_y
に移っている。そのため、_y
がスコープから外れる際に値を解放すればよく、_x
がスコープから外れるときは何もしないでよいことになる。
この仕組みによって、メモリの二重解放が起こらないようになっている。
fn main() { { let _x = "foo".to_string(); // "foo" の所有権を _x が持つ let _y = _x; // "foo" の所有権が _y に移る } // _y がスコープから外れ、その際に "foo" がメモリから破棄される }
所有権を失った変数を使おうとすると、コンパイルエラーになる。
fn main() { { let _x = "foo".to_string(); let _y = _x; let _z = _x; // use of moved value: `_x` } }
関数の引数として変数を渡したときも、その変数は所有権を失う。代わりに関数のパラメータが所有権を持つ。
fn main() { let x = "foo".to_string(); some_method(x); // x は所有権を失う } fn some_method(arg: String) { println!("{}", arg); // この時点で arg が "foo" の所有権を持っている } // arg がスコープから外れるので、"foo" がメモリから破棄される
所有権の移転は、関数が値を返すときにも発生する。
fn main() { let _x = some_method(); // "foo" の所有権を _x が持つ let _y = some_method(); // また別の "foo" の所有権を _y が持つ } // _x と _y がスコープから外れるので、それぞれが持っている "foo" がメモリから破棄される fn some_method() -> String { let return_value = "foo".to_string(); return_value // "foo" の所有権を呼び出し元に移す }
所有権の移転ではなく値のコピーを行いたい場合は、clone
メソッドを使う。
以下の例では_x
と_y
がそれぞれに"foo"
を所有している。
そのため_x
から_z
に所有権を移すコードを書いても、コンパイルエラーにはならない。この時点では_x
も所有権を持っているため。
fn main() { { let _x = "foo".to_string(); let _y = _x.clone(); let _z = _x; // OK } }
Copy
トレイトを実装している型(例えばi32
)の場合、=
を使ったり関数に変数を渡したりしても、所有権の移転ではなく値のコピーが行われる。
fn main() { { let _x = 1; let _y = _x; let _z = _x; // OK } }
fn main() { let x = 1; some_method(x); let _y = x; // OK } fn some_method(arg: i32) { println!("{}", arg); }
借用
所有権を保持したまま、値を参照する権利だけを他の変数に貸すことができる。これを借用という。
値を参照するには&
という記法を用いる。
x
の値を参照するには&x
と書く。
fn main() { let x = "foo".to_string(); let y = &x; // y は x が持っている "foo" を参照しているだけなので、"foo" の所有権は引き続き x が持っている let z = x; // ここで、所有権の移転が起こる }
fn main() { let x = "foo".to_string(); let length = get_length(&x); // get_length に渡しているのは参照なので、"foo" の所有権は引き続き x が持っている println!("The length of {} is {}.", x, length); // x は所有権を失っていないので、この位置で println! に渡してもコンパイルエラーにならない } fn get_length(arg: &String) -> usize { arg.len() }
ミュータブルな参照とイミュータブルな参照
参照はデフォルトではイミュータブルであり、変更しようとするとコンパイルエラーになる。
fn main() { let x = "hello".to_string(); let y = &x; y.push_str(", world"); // cannot borrow `*y` as mutable, as it is behind a `&` reference }
&mut
と書けばミュータブルな参照になり、変更することができる。
fn main() { let mut x = "hello".to_string(); let y = &mut x; y.push_str(", world"); // OK }
値と参照は同じものを指しているので、変更は連動する。
fn main() { let mut x = "hello".to_string(); let y = &mut x; y.push_str(", world"); println!("{}", x); // hello, world }
イミュータブルな値に対してミュータブルな参照を作ろうとすると、コンパイルエラーになる。
fn main() { let x = "hello".to_string(); let y = &mut x; // cannot borrow `x` as mutable, as it is not declared as mutable }
逆に、ミュータブルな値に対してイミュータブルな参照を作ることはできる。
fn main() { let mut x = "hello".to_string(); let y = &x; // OK }
参照は無制限に作れるわけではない。
イミュータブルな参照なら、いくつでも作れる。
fn main() { let x = "foo".to_string(); let y = &x; let z = &x; println!("{}, {}", y, z); // foo, foo }
しかしミュータブルな参照は、ひとつしか作ることができない。
fn main() { let mut x = "foo".to_string(); let y = &mut x; let z = &mut x; // cannot borrow `x` as mutable more than once at a time println!("{}, {}", y, z); }
また、イミュータブルな参照とミュータブルな参照を混在させることもできない。
fn main() { let mut x = "foo".to_string(); let y = &x; let z = &mut x; // cannot borrow `x` as mutable because it is also borrowed as immutable println!("{}, {}", y, z); }
しかし以下のコードはどちらも、コンパイルエラーにならない。
fn main() { let mut x = "foo".to_string(); let y = &mut x; let z = &mut x; }
fn main() { let mut x = "foo".to_string(); let y = &x; let z = &mut x; }
これには、ライフタイムという概念が関係している。
ライフタイム
全ての参照はライフタイムを持っている。ライフタイムとは、その参照が有効な期間のこと。
参照が使われている期間がそのまま、その参照のライフタイムになる。
fn main() { let x = "foo".to_string(); let y = &x; // ┐ y のライフタイム開始 println!("{}", 1); // | println!("{}", 2); // │ println!("{}", y); // ┘ y のライフタイム終了 println!("{}", 3); }
参照を宣言したが使われることはなかった場合、宣言した行でライフタイムは終了する。
fn main() { let x = "foo".to_string(); let y = &x; // y のライフタイムが開始されると同時に終了する println!("{}", 1); println!("{}", 2); println!("{}", 3); }
以下のコードでは、イミュータブルな参照であるy
のライフタイムは、宣言されたその行で終了している。
そのため、その次の行でミュータブルな参照を作っても、「イミュータブルな参照とミュータブルな参照が混在している」という状態にはならない。
fn main() { let mut x = "foo".to_string(); let y = &x; let z = &mut x; }
以下のコードだと、y
のライフタイムがprintln!
の行まで延びてしまっているので、z
を宣言した瞬間に混在が発生し、コンパイルエラーになる。
fn main() { let mut x = "foo".to_string(); let y = &x; let z = &mut x; // cannot borrow `x` as mutable because it is also borrowed as immutable println!("{}, {}", y, z); }
ライフタイムという概念があることで、参照を安全に利用できる。
具体的には、参照元の値の有効期間よりもライフタイムが長くなっている場合は、コンパイルエラーになる。
この仕組みによって、既にメモリから破棄された値を参照してしまう、ということがなくなる。そのようなコードを書いた場合はそもそもコンパイルが通らなくなる。
以下のコードでは、println!
の行までがy
のライフタイムだが、それよりも先にx
がスコープから外れ、その値を破棄してしまう。
そのため、コンパイルエラーになる。
fn main() { let y; { let x = "foo".to_string(); y = &x; // `x` does not live long enough } // ここで x がスコープから外れるため、"foo" を破棄してしまう println!("{}", y); // 既に破棄された値を参照しようとしてしまっている }
以下のような関数を定義しても、コンパイルエラーになる。
s
の参照を返そうとしているが、この関数のスコープが終わった時点でs
は破棄されてしまう。
そのため、&s
は既に破棄された値の参照になってしまうので、このようなコードはコンパイルできないようになっている。
fn some_method() -> &String { let s = "foo".to_string(); &s // s の参照を呼び出し元に返そうとしているが…… } // s がスコープから外れるため、この時点でその値は破棄されてしまう
以下のように、引数として渡された参照を返すのは問題ない。
この関数には値は何も渡されていないのだから、スコープが終わっても何かが破棄されることはない。
fn some_method(arg: &String) -> &String { arg }
しかし引数を複数にすると、コンパイルエラーになってしまう。
fn some_method(arg1: &String, arg2: &String) -> &String { // missing lifetime specifier arg1 }
このコンパイルエラーは、コンパイラがライフタイムを推論できないために発生した。
引数がひとつのときはコンパイラが推論できるため、エラーにならなかった。
このエラーを修正するためには、ライフタイムに関する注釈を記述する必要がある。型を明示する必要がある際に型注釈をつけるのと似ている。
ライフタイム注釈は、'a
のようにアポストロフィーで始まる。
以下が、先程のsome_method
にライフタイム注釈をつけた例。
これでコンパイルが通るようになる。
fn some_method<'a>(arg1: &'a String, arg2: &String) -> &'a String { arg1 }
このように書くと、some_method
が返す参照のライフタイムは、arg1
として渡された参照のライフタイムと同じと見做されるようになる。
そのため、以下のコードはコンパイルが通る。
z
のライフタイムは&x
と同じなので、println!
の行までが&x
のライフタイムとなる。
このコードには何の問題もない。
// OK fn main() { let z; let x = "foo".to_string(); { let y = "bar".to_string(); z = some_method(&x, &y); } println!("{}", z); } fn some_method<'a>(arg1: &'a String, arg2: &String) -> &'a String { arg1 }
だがsome_method
に渡す引数を逆にすると、つまりarg1
として&y
を渡すと、コンパイルエラーになる。
z
のライフタイムは&y
と同じはずなのだが、そうなると、println!
の行までが&y
のライフタイムになる。
しかしその前にy
がスコープから外れているため、参照元の値よりも参照のライフタイムが長生きしてしまっている。
そのためコンパイラはこのコードを許可しない。
// コンパイルエラー fn main() { let z; let x = "foo".to_string(); { let y = "bar".to_string(); z = some_method(&y, &x); // `y` does not live long enough } // y がスコープから外れるため、"bar" がメモリから破棄される println!("{}", z); } fn some_method<'a>(arg1: &'a String, arg2: &String) -> &'a String { arg1 }
以下のように、複数の引数に同じライフタイム注釈をつけることもできる。
fn some_method<'a>(arg1: &'a String, arg2: &'a String) -> &'a String { arg1 }
この場合、これらの引数のライフタイムのうち短いほうが、ライフタイム注釈のライフタイムになる。
そのため、arg2
のライフタイムのほうがarg1
より短い場合、この関数の返り値のライフタイムはarg2
と同じと見做される。
返している参照自体は、arg1
にも関わらず。
以下はまさにそのような例であり、そしてその結果コンパイルエラーになっている。
z
が参照している値はx
が所有している"foo"
なのだが、z
のライフタイムと&y
のライフタイムが、同じであると見做される。そうなると、y
がスコープから外れたあとも&y
が生きていることになり、コンパイルエラーとなる。
fn main() { let z; let x = "foo".to_string(); { let y = "bar".to_string(); z = some_method(&x, &y); // `y` does not live long enough } println!("{}", z); } fn some_method<'a>(arg1: &'a String, arg2: &'a String) -> &'a String { arg1 }