日記(日記とは言っていない)

https://zenn.dev/23prime に移行しました。

<Rust 備忘録> 所有権と参照・借用

Rust のキー概念である所有権と参照・借用についてのメモ.

(メモリのモデル図とかはそのうち作れたらいいなぁ…)

所有権 (ownership) と移動 (move)

基本的なこと

  • String 型 や Vec<T> 型等の参照型では,ポインタ・容量・長さを stack で,実際の値を heap で確保します.
  • この heap で確保された値を所有 (own) しているという意味で,所有権 (ownership) という概念があります.
  • なお,数値型や char 型等の基本型は,値を stack で確保します.

所有権

String 型で見てみます.

次のように変数宣言した段階で,heap 上に確保された値の所有権は, a にあります.

let a = "hello".to_string();

所有権の移動

ba の値を代入すると,値の所有権は a から b へ移動 (move) します.

let a = "hello".to_string();
let b = a;
assert_eq!("hello", b);

ここで,変数 a を使おうとすると,次のようにコンパイルエラーになります.

let a = "hello".to_string();
let b = a;
assert_eq!("hello", a);
error[E0382]: use of moved value: `a`
 --> src/main.rs:7:23
  |
6 |   let b = a;
  |       - value moved here
7 |   assert_eq!("hello", a);
  |                       ^ value used here after move
  |
  = note: move occurs because `a` has type `std::string::String`, which does not implement the `Copy` trait

Clone

Rust ではデフォルトで上のように「値の所有権の移動」が行われますが,例外的にメモリ上に「値をコピー(クローン)」することもできます.

次のように書くことで, a の値とは別の領域に値のコピー(すなわち b の値)が作られます.

let a: String = "hello".to_string();
let b = a.clone();
assert_eq!("hello", a);
assert_eq!("hello", b);

この .clone() メソッドにより,多くの所有権関連のコンパイルエラーは回避できます.しかし,闇雲に使うと無駄にメモリを消費していくので,次節にて述べる参照を上手く用いながら,できるだけ Clone しないような実装ができればベターかもしれません.

Copy Trait

Copy Trait を実装している型では,デフォルトで値はコピーされます.

例えば, i32 型は Copy Trait を実装しており,次のような記述をしてもエラーになりません.

let a: i32 = 0;
let b = a;
assert_eq!(0, b);

参照 (reference) と借用 (borrowing)

  • 参照は,所有権を持たないポインタ型です.
  • 値に対する参照を作ることを,借用といいます.
  • 参照には,デフォルトの初期値(NULL のような)は存在しません.
  • 「参照の参照」みたいなこともできます.

2種類の参照(借用)

種類 共有参照 可変参照
借用 immutable mutable
読み出し
変更 ×
複数参照 ×
記法 &a &mut a
  • 共有参照(immutable な借用)の例
let a = 0;
let b: &i32 = &a;  // 借用!
assert_eq!(0, *b);

ここでは *b で明示的に参照解決(つまりポインタ型から参照先の値を取り出す)しています.

immutable な借用は,何回でも可能です.

let a = 0;
let b: &i32 = &a;
let c: &i32 = &a;
assert_eq!(0, *b);
assert_eq!(0, *c);
  • 可変参照(mutable な借用)の例
let mut a = 0;                  // 参照元の a も mutable である必要がある
{
  let mut b: &mut i32 = &mut a; // 借用!
  *b += 1;                      // 参照解決して値を変更
  assert_eq!(1, *b);
}                               // b がスコープから外れる
assert_eq!(1, a);               // a の値も変更されていることがわかる

mutable な借用は,原則1回のみ許されます.次の2つのコードは通りません.

let mut a = 0;
let mut b: &mut i32 = &mut a;
let mut c: &mut i32 = &mut a; // 2回目の借用をしようとすると怒られる
let mut a = 0;
let mut b: &mut i32 = &mut a;
let c : &i32 = &a;            // immutable な借用と併用できない

ただし,次のコードでは, b が解法された後に c で借用をしているため,コンパイルエラーになりません.

let mut a = 0;
{
  let mut b: &mut i32 = &mut a;
  assert_eq!(0, *b);
}
let mut c: &mut i32 = &mut a;
assert_eq!(0, *c);

参照の代入

let a = 0;
let b = 1;
let mut c = &a;
assert_eq!(0, *c);

c = &b;            // 参照 c へ参照を再代入
assert_eq!(1, *c);
assert_eq!(0, a); // 「借用しなおす」ため, a の値は変更されない

ライフタイム (lifetime)

  • ライフタイム:参照を(安全に)利用できる範囲のこと

次のコードでは, assert_eq!(*a, 1) の段階で既に b がスコープから外れています. これを放っておくとダングリングポインタが生じてしまいますが, Rust の場合はコンパイラがこれを阻止します.

let mut a: &i32 = &0; // a を `&i32` 型として定義
{
  let b = 0;
  a = &b;             // ここで a の値を変更する
}
assert_eq!(*a, 0);    // a は b への参照を持つが, b がスコープから外れているためエラー

↓ 実行結果.

error[E0597]: `b` does not live long enough
  --> src/main.rs:10:10
   |
10 |     a = &b;
   |          ^ borrowed value does not live long enough
11 |   }
   |   - `b` dropped here while still borrowed
12 |   assert_eq!(*a, 0);
13 | }
   | - borrowed value needs to live until here

ただし,次のコードは通ります.

let mut a: &i32 = &0;
{
  let b = 0;
  let a = &b;      // ここで a を新たに宣言している
}                  // ↑この新たな宣言ごとここで吹っ飛ぶ
assert_eq!(*a, 0); // ここでは,1行目で宣言した a の値が生きている