目次
4.0 概要
- 所有権とは
- 参照
- 借用
- スライス
- コンパイラはデータをメモリにどう配置するか
4.1 所有権とは
所有権は「ヒープ領域にメモリ領域を確保したり、それを解放する機能を言語側で管理するための概念」
所有権により以下が可能になる
- どの部分のコードがどのヒープ上のデータを使用するかの把握
- ヒープ上のデータの重複の最小化
- メモリ不足の防止
コラム: 「スタックとヒープ」
スタック
- コンパイル時に必要メモリサイズが分かっているデータを保持
- データは固定長
- 得た順にデータを並べ、逆順で取り除く
ヒープ
- コンパイル時には必要メモリサイズが不明なデータを保持
- OS が管理
- OS に、ある大きさのメモリ領域をヒープ領域に確保するよう依頼すると、OS はメモリ領域を確保したのちに、そのメモリ領域のベースアドレスを返す
- そのポインタを指定してオブジェクトの破棄をOSに依頼すると、メモリ領域が解放され、貸出可能状態に戻る
所有権の規則
- Rust の各値は、所有者と呼ばれる変数と対応している。
- いかなる時も所有者は一つである。
- 所有者がスコープから外れたら、値は破棄される。
例1: str 型
{
// *1
let s = "hello"; // *2
// *3
} // *4
- 上記のコード内で変数
sにはプログラムにハードコードされた"hello"という文字列への参照が束縛される - この文字列への参照は、
sが宣言された行 (*2 の時点) からsがスコープから抜けるまで (*3 まで) はメモリ上に存在するが、それ以外の領域では存在しない
例2:ヒープに値を保持する場合(String型)
{
// *1
let s = String::from("hello"); // *2
// *3
} // *4
上記のコードでは
- 変数
sの宣言時に OS に(ヒープ領域内での)メモリ領域の確保が依頼され - 確保された領域のベースアドレスなどの情報( pointer, size, capacity )が
sに束縛される -
sに束縛された値は *2 ~ *3 の範囲ではメモリ上に存在するが、それ以外の領域では存在しない - また、ヒープ領域に確保されたメモリ領域は
sがスコープを抜けると同時に解放される- より厳密には、以下のような仕組みになっている:
- Rust では、閉じ括弧
}が自動的に(該当スコープで有効なすべての(Dropトレイトが有効な)変数の)drop関数を呼び出す -
dropはその変数と結びついたヒープ領域内のメモリ領域を解放するよう定義されている
- Rust では、閉じ括弧
- より厳密には、以下のような仕組みになっている:
二重解放エラーを防ぐ仕組み(ムーブセマンティクス)
- Rust では2つ以上の異なる変数に、同じヒープ領域へのポインタが束縛されることはない
- ヒープ領域と結びついたある変数
aをほかの変数bに代入let b = aすると、aはそれ以降無効化される
- ヒープ領域と結びついたある変数
例:String型の変数のムーブの挙動
let s1 = String::from("hello");
によって
- ヒープ領域内に
b'h',b'e',b'l',b'l',b'o'の一連のデータが保持され - このメモリ領域へのポインタがスタック領域に保持されたとする
- この時、このポインタは変数
s1に束縛される
ここでさらに
let s2 = s1;
とすると
- 変数
s2にヒープ領域へのポインタが束縛され - 変数
s1は無効化される
- 無効化された変数を利用しようとするとコンパイルエラーが発生する
- たとえば以下のコードコンパイルエラーを起こす
let s1 = String::from("hello"); let s2 = s1; // この時点で s1 は無効化される println!("s1: {}, s2: {}", s1, s2);
ヒープ領域内のデータを複製する方法(クローン)
- スタック上のデータだけではなくヒープ領域内のデータの deep copy が必要なら
cloneメソッドを用いる - たとえば以下のコードは問題なく動く
let s1 = String::from("hello"); let s2 = s1.clone(); // clone メソッドを使うことでヒープ領域上のデータごと複製されるため、s1 は破棄されない println!("s1: {}, s2: {}", s1, s2);
スタックのみのデータの複製(コピー)
- スタック上で完結するデータ型には
Copyトレイトが実装されていることが期待される -
Copyトレイトが実装された型の値はムーブされずにコピーされる -
Dropトレイトを実装した型、もしくはDropトレイトを実装した型を一部に含む型にはCopyトレイトの実装ができない(コンパイルエラーを起こす) - 以下は自動的に
Copy- スカラー型
-
Copyの型だけを含むタプル
所有権と関数
- 関数に変数を渡すと、その変数のもつ所有権が関数内に移動する
- 変数を別の変数に代入した場合と同様に、あるヒープ領域と結びついた変数
aを関数fに渡す( つまりf(a)のようにする) と、aはそれ以降無効化される
- 変数を別の変数に代入した場合と同様に、あるヒープ領域と結びついた変数
例:所有権と関数
fn main() {
let s = String::from("hello"); // sがスコープに入る
takes_ownership(s); // sの値が関数にムーブされ...
// ...ここではもう有効ではない
// ここで s を呼び出そうとすると、コンパイラは、コンパイルエラーを投げる
let x = 5; // xがスコープに入る
makes_copy(x); // xも関数にムーブされるが、
// i32はCopyなので、この後にxを使っても大丈夫
} // ここでxがスコープを抜け、sもスコープを抜ける。
// ただし、sの値はムーブされているので、何も特別なことは起こらない。
fn takes_ownership(some_string: String) { // some_stringがスコープに入る。
println!("{}", some_string);
} // ここでsome_stringがスコープを抜け、`drop`が呼ばれる。後ろ盾してたメモリが解放される。
fn makes_copy(some_integer: i32) { // some_integerがスコープに入る
println!("{}", some_integer);
} // ここでsome_integerがスコープを抜ける。何も特別なことはない。
戻り値とスコープ
- 関数が値を返すことでも、所有権は移動する
例:戻り値とスコープ
fn main() {
let s1 = gives_ownership(); // gives_ownership は、戻り値をs1にムーブする
let s2 = String::from("hello"); // s2がスコープに入る
let s3 = takes_and_gives_back(s2); // s2は takes_and_gives_back にムーブされ
// 戻り値もs3にムーブされる
} // ここで、s3はスコープを抜け、ドロップされる。
// s2もスコープを抜けるが、ムーブされているので、
// 何も起きない。s1もスコープを抜け、ドロップされる。
fn gives_ownership() -> String { // gives_ownership は、戻り値を呼び出した関数にムーブする
let some_string = String::from("hello"); // some_string がスコープに入る
some_string // some_string が返され、呼び出し元関数にムーブされる
}
// takes_and_gives_back は、Stringを一つ受け取り、返す。
fn takes_and_gives_back(a_string: String) -> String { // a_stringがスコープに入る。
a_string // a_stringが返され、呼び出し元関数にムーブされる
}
4.2 参照と借用
参照
-
変数
hogeの参照は&hogeで取得できる -
型
Hogeの変数の参照の型は&Hogeとなる -
&hogeにはhogeへのポインタ(=「hogeに束縛された(ヒープデータへの)ポインタ」へのポインタ)が束縛される -
&hogeがスコープを抜けてもhogeに結びついたヒープデータはドロップされない -
&hogeがスコープを抜けてもhogeに束縛された(ヒープデータへの)ポインタはドロップされない -
&hogeがスコープを抜けると&hoge束縛されたhogeへのポインタが消去される -
参照外し演算子
*で参照を外せる -
例:以下のコードに登場する
sとその参照s = &s1の関係は下図の通り;fn main() { let s1 = String::from("hello"); let len = calculate_length(&s1); println!("The length of '{}' is {}.", s1, len); } fn calculate_length(s: &String) -> usize { s.len() }
参照と関数
- 関数の引数に参照を取ることを借用と呼ぶ
可変参照
- 参照はデフォルトではimmutable(不変)
- 可変変数
hogeの可変参照は&mut hogeで取得できる - 例:
fn main() { let mut s = String::from("hello"); change(&mut s); } fn change(some_string: &mut String) { some_string.push_str(", world"); }
参照についてのルール
「参照は、一つの可変参照か複数の不変参照」
「ダングリング参照は許されない」
1. 一つのスコープ内では、一つのデータに対して、一つしか可変な参照を作れない
- このルールにより、複数のポインタが同じデータに同時にアクセスすることで発生する問題などを防げる
- 例1:以下のコードはコンパイルエラーを起こす
let mut s = String::from("hello"); let r1 = &mut s; let r2 = &mut s; println!("{}, {}", r1, r2); - 例2:以下のコードは問題なく動く
let mut s = String::from("hello"); { let r1 = &mut s; } // r1はここでスコープを抜けるので、問題なく新しい参照を作ることができる let r2 = &mut s;
2. 不変参照と可変参照は同一スコープ内で共存できない
3. 不変参照は複数共存可能
- 以下のコードはコンパイルエラーを起こす
let mut s = String::from("hello"); let r1 = &s; // 問題なし let r2 = &s; // 問題なし let r3 = &mut s; // 問題あり!
4. ダングリング参照はコンパイラエラーで阻止される
- 以下のコードはコンパイルエラーを起こす
fn main() { let reference_to_nothing = dangle(); } fn dangle() -> &String { let s = String::from("hello"); &s // ここで s はドロップされる // --> ドロップ済みの値への参照をこの関数の外側にムーブしようとしていることになる // --> 無効な参照を作る不法な操作なのでコンパイラに怒られる }
4.3 スライス型
-
スライスにより、コレクション全体ではなく、その内の一連の要素を参照することができる
-
スライスは、
&hoge[1..3]のような形で取得する -
スライスは、元のコレクションオブジェクトの不変参照である
-
スライスは、通常の参照と異なり、スタック上のポインタへのポインタではなく、ヒープデータへのポインタを保持する
-
このような成り立ちのために、スライスのスライスはスライスである(型は変わらない)
- ただし、元のコレクションオブジェクトとは型が異なる
- たとえば、
String型に対するスライスは&str,&strのスライスは&str - たとえば、
[i32; 10]型に対するスライスは&[i32],&[i32]のスライスは&[i32]
- たとえば、
- ただし、元のコレクションオブジェクトとは型が異なる
-
一方、
Hoge型の変数への参照は&Hogeであり、その参照への参照は&&Hogeであるので注意// `String` のスライスは `&str` // `&str` のスライスは `&str` fn main() { let s: String = String::from("hello"); let slice: &str = &s[1..4]; // ell let slice: &str = &slice[1..]; // ll let slice: &str = &slice[..1]; // l println!("slice: {}", slice); }// `[i32; 10]` のスライスは `&[i32]` // `&[i32]` のスライスは `&[i32]` fn main() { let a:[i32; 10] = [1, 2, 3, 4, 5, 1, 2, 3, 4, 5]; let slice: &[i32] = &a[1..9]; // [2, 3, 4, 5, 1, 2, 3, 4] let slice: &[i32] = &slice[..7]; // [2, 3, 4, 5, 1, 2, 3] let slice: &[i32] = &slice[3..]; // [5, 1, 2, 3] println!("slice: {:?}", slice); }// `String` の参照は `&String`, // `&String` の参照は `&&String` fn main() { let s: String = String::from("hello"); let s: &String = &s; let s: &&String = &s; } -
また、
&&..&Hogeのスライスを取るのと、Hogeのスライスを取るのは同等らしい(Rust 側で参照外しをやってくれるっぽい?)fn main() { let s: String = String::from("hello"); let s: &String = &s; let s: &&String = &s; let s: &&&String = &s; let slice: &str = &s[1..4]; // ell println!("slice: {}", slice); }