Rustについての勉強ログ。
所有権と借用
RustはC/C++並みの高速化をもちながら、メモリ安全性を担保するように設計されたプログラミング言語です。C/C++ではユーザーがメモリ確保と解放のタイミングについて責任を持つことでガベージコレクションを行う必要がなく、これが高速動作の一助となっています。Rustでは記法に強く制約を課すことでメモリ参照と解放のタイミングに安全性を与えます。反面、その記法の理解が大変なのですが基本的なルールはそれほど多くないと考えています。
ルール
- 一つのオブジェクトに対して、その所有権を持つ変数が一つだけ存在する
- 所有権を持つ変数を通してのみ、そのオブジェクトへの参照や変更ができる
- 所有権は他の変数に譲渡(move)もしくは貸し出す(borrow)ことができる
- 譲渡した場合、元の変数は所有権を完全に失う
- 貸し出した場合、その間は所有権を失うが、いつか返ってくる
- 変数が自身のスコープを抜ける(=ライフタイムを終える)とき
- もし所有権を借用している場合は元の所有者にオブジェクトの所有権が返される
- そうでない場合、オブジェクトが解放される
例
これは関数を呼び出す際にも同様で、関数に引数を渡すときにも所有権の譲渡(move)が発生します。
fn main() { let v = vec![1, 2, 3]; f(v); // ここでvの所有権がfに渡される println!("{:?}", v); // エラー!! // fにvecの所有権を渡した後なのでvは使えない // vec自体はfのスコープを抜けた後に解放される(fの変数xがvecの所有者なので) } fn f(x: Vec<i32>) { // xがvecの所有権を受け取る }
note: 整数型などCopyトレイトが実装されている型については所有権の譲渡(ムーブセマンティクス)は生じず、複製(コピーセマンティクス)が行われます。Vec型の場合はCopyトレイトが実装されていないため、所有権の譲渡が行われます。
ルールに従いfの関数の引数であるxがvec![1, 2, 3]
の所有権を譲渡され、関数fの終了時、xがスコープを抜けるとともにvectorも開放されます。
関数を呼び出した後でも値を参照したり変更したい場合は、(1) 借用を行うか、(2) 複製を行うかのどちらかで対応する必要があります。
借用
借用する場合は次のようにします(&マークに着目)。関数先に所有権を貸し出して、その関数を抜けた後に元の変数に所有権が戻ります。
fn main() { let v = vec![1, 2, 3]; f(&v); // ここでfにvの所有権が借用される // 関数を抜けた後にvにvecの所有権が戻る println!("{:?}", v); // OK } fn f(x: &Vec<i32>) { // xがvecの所有権を借用する }
借用先で変数を変更する場合は、可変な借用を行います。
fn main() { let mut v = vec![1, 2, 3]; f(&mut v); // ここでvの可変な借用がfに渡される // 関数を抜けた後にvにvecの所有権が戻る println!("{:?}", v); // OK } fn f(x: &mut Vec<i32>) { // xがvecを可変な所有権として借用する x.push(4); // OK }
基本はこれだけです。意外と簡単ですよね。
コピー
次にコピーを行う場合は次のようにします。
fn main() { let v = vec![1, 2, 3]; f(v.clone()); // ここでvのコピーがfに渡される // vecの所有権はvのまま println!("{:?}", v); // OK } fn f(x: Vec<i32>) { // xがvecのコピーを受け取る }
この場合はmainとfでそれぞれの異なるvecをもち、所有権も別々になります。しかしコピーは借用に比べて、メモリのコピーが発生するため、メモリ効率が悪くなるのに加えて、時間もかかります。そのため、整数値などのCopyトレイトを持つ型以外は、借用を使うことが推奨されます。
OptionとResult型
Option型
pythonでいうNoneを表すための列挙型(enum)。ただpythonと違ってRustでのenumはその列挙子に値を持たせることができます。
enum Option<T> { None, Some(T), }
ここで<T>
はC++でいうところの型パラメータになります。Rustでは多くの場合、これは推論されるためユーザーが意識することはありません。enum型は通常(型名)::(field名)
で参照できますが、OptionとResultの場合は型名を省略できます。
let x = None; // is equal to let x = Option::None; let y = Some(4); // is equal to let y = Option::Some(4);
let y = Some(4)のように4のような値を列挙子が持てるのがミソです。例えば、次のpythonコードを見てみましょう。div関数ではx/yを計算しますが、y=0の場合ではNoneを返すとします。次のコードは、divの返り値がNoneでない場合のみ何か処理を行う、というものです。
def div(x, y): if y == 0: return None else: return x / y z = div(3, 0) if z is not None: z += 2
これをRustで実装すると次のようになります。
fn f(x: i32, y: i32) -> Option<i32> { if y == 0 { None } else { Some(x/y) // Someの中に関数が意図する値を入れて返す } } fn main() { // pattern 1 // pythonに似た形。is_none()でzがNoneであるかを判定する。 // unwrap()でSomeの中身を取り出すことができる。 // !z.is_none() = z.is_some() let z = f(3, 0); let mut zz; if !z.is_none() { zz = z.unwrap() + 2; } // pattern 2 // matchを用いて、zがNoneかSomeかで処理を変える。 // matchの中のSome(value)でvalue変数にzのSomeの中の値を束縛できる。 let z = f(3, 0); let mut zz; match z { None => {/*do nothing*/}, Some(value) => {zz = value + 2}, } // pattern 3 // もしzがNoneであればpanicを起こして終了する let mut zz = z.unwrap(); zz += 2; // pattern 4 // unwarpと同様だがエラーメッセージを指定できる let mut zz = z.expect("zero division was occurred!"); zz += 2; }
Result型
エラーハンドリング用の型です。Result<T, E>でTは成功した場合にOkの中身の型、Eは失敗した場合のErrの中身の型です。
enum Result<T, E> { Ok(T), Err(E), }
これも例を示しましょう。次のコードは0除算の場合Exceptionを返すPythonコードです。
def div(x, y): if y == 0: raise Exception() else: return x / y # raise Exception z = div(3, 0) # (例外処理, y = 0の場合は0を代入) try: z = div(3, 0) except: z = 0
これをRustで実装すると次のようになります。ここでResult<T, E>でTは成功した場合にOkの中身の型、Eは失敗した場合のErrの中身の型です。
fn div(x: i32, y: i32) -> Result<i32, String> { if y == 0 { Err(String::from("div by zero")) } else { Ok(x / y) } } fn main() { // pattern 1 もしErrの場合 panicked を起こす let z = div(3, 0).unwrap(); // pattern 2 (例外処理, y = 0の場合は0を束縛) let z = match div(3, 0) { Err(value) => {0}, Ok(value) => {value}, }; // pattern 3 (例外処理, y = 0の場合は0を束縛) // もしerrorの場合は、引数を変数に束縛する let z = div(3, 0).unwrap_or(0); }