こんにちは、rustが好きでたまらない初心者のせいです。
最近業務でrustを書くチャンスを頂いて日々楽しくrustコンパイラーと戦っています。
まだ初心者ですが、The bookを読みながらrustの一番特徴なメモリ管理(所有権、参照&借用)について
自分の理解でまとめてみました。間違ったところがあったら温かいコメントをいただければと思います。
実行環境:
- rustc --version
- rustc 1.35.0-nightly (fbd34efb3 2019-03-26)
- edition: 2018
rustにハマる巻01
1. rustはどんな言語?
高速かつ安全、多くの設計&実装ミスなど潜在的なバグはコンパイル時点で検知してくれる言語です。
2. 色々なメモリの管理仕組み
- 手動でallocate/free (C、、、他は全然思い付かない)
- バグ無しで実装することはスーパーエンジニアしかできない
- freeし忘れたら、memory leak
- freeが早かったら、予期せぬエラー
- freeがダブったら、予期せぬエラー
- バグ無しで実装することはスーパーエンジニアしかできない
- garbage collection (Jvm系、Haskellなど)
- 手抜き、めっちゃ楽
- Full GCが発生したら再起動しかない
- 自動的にスコープから外れたら直ちに解放 (rust、C++ RAII patterns)
3. rustのメモリ管理
メモリは用途によって2種類に分類:
\ | Stack | Heap |
---|---|---|
アクセス方法 | LIFO | 一定な領域が確保できる任意な場所 |
格納対象 | サイズ固定なデータ | サイズ変動可能なデータ |
-------- | - 全ての整数型(i32, u32など) | - サイズ固定以外のデータ(Stringなど) |
-------- | - 全ての小数型(f64など) | -------- |
-------- | - bool, char | -------- |
-------- | - 上記タイプのみ含むTuple:(i32, bool) | -------- |
-------- | - 上記タイプのみ含むstruct | -------- |
-------- | - 所有権 | -------- |
4. 所有権
{ // スコープ開始
let s1 = String::from("hello"); // 「hello」という文字列の所有権はs1に与えている
let s2 = String::from("world"); // 「world」という文字列の所有権はs2に与えている
// s1, s2 は使える
} // スコープ終了、s2->s1という順番で解放する
実際メモリに格納したイメージ:
- メモリ解放: Stackにある所有権に従ってHeapも一緒にfreeされる
Stack Heap
↓ ↓
4-1. 所有権の移譲: 代入
fn main() {
let s1: String = String::from("hello");
{
let s2: String = s1; // 所有権はs1からs2に移譲した
}
println!("{}, world!", s1); // s1は無効なのでコンパイルエラーになる
}
コンパイルエラー:
error[E0382]: use of moved value: `s1`
--> src/main.rs:22:28
|
20 | let s2 = s1;
| -- value moved here
21 | }
22 | println!("{}, world!", s1);
| ^^ value used here after move
|
= note: move occurs because `s1` has type `std::string::String`, which does not implement the `Copy` trait
実際メモリに格納したイメージ:
- rustの思想はheapまでも解放する鍵として常に1つしかないようにコントロールしないといけないことです。
- 仮にこの例のs1とs2が両方共所有権があるとしたら、s2がスコープ(奥の中括弧)から離れたとき、s1はheapが解放済みの所有権になるので、実行時に予期せぬエラーになります。
4-2. 所有権の移譲: function経由
fn takes_ownership(s: String) {
}
fn main() {
let s1: String = String::from("hello");
takes_ownership(s1); // 所有権はs1からfunction:takes_ownershipのsに移譲した
println!("{}, world!", s1); // s1は無効なのでコンパイルエラーになる
}
functionを呼び出した後でもs1を使いたい場合はどうすればいいの?
- 一つ方法は所有権をreturn経由で返す
fn takes_ownership_and_give_back(s: String) -> (String, usize) {
let length = s.len();
(s, length) // sで受け取った所有権をそのまま返す
}
fn main() {
let s1: String = String::from("Oh my god ...");
let (s1, length) = takes_ownership_and_give_back(s1); // 分配束縛で文字列の所有権が再び与えられた、s1は同名変数の再宣言
println!("{} : size({})", s1, length);
}
これはやってらんねーな。。。
4-3. 所有権の移譲: structの属性
#[derive(Debug)]
struct Data2 {
pub value1: String,
pub value2: String,
}
fn main() {
let data2 = Data2 { value1: "value1".to_string(), value2: "value2".to_string() };
let data2_value1 = data2.value1; // 所有権は data2.value1 から移譲した
println!("{:?}", data2.value1); // data2.value1 の所有権は失ったため、コンパイルエラーになる
println!("{:?}", data2.value2); // 問題なく data2.value2 はアクセスできる
println!("{:?}", data2); // data2.value1 の所有権は失ったため、コンパイルエラーになる
}
5. 所有権を移譲しない: Copy trait と Clone trait
5-1. Stackコピー: Copy trait
- サイズ固定なデータ(Stackのみ使う)は、所有権の移譲とデータの複製は実質同じ処理になるため、所有権は移譲しない。
fn main() {
let i1: i32 = 100;
{
let i2: i32 = i1; // 所有権の移譲は発生せず、i1のcopyをi2に与えた
} // i2のみ解放される
println!("number: {}", i1); // i1はそのまま使える
// これも同じく通る
let t1: (i32, bool) = (1, true);
{
let t2: (i32, bool) = t1;
}
println!("{}, {}", t1.0, t1.1)
}
- サイズ固定な属性しか持ってないstructも同じ
#[derive(Copy, Clone)] // サイズ固定な属性しか持ってないstructもCopy traitを実現することができる
struct Data3 {
pub value1: i32,
pub value2: char,
}
fn main() {
let d1: Data3 = Data3 { value1: 0, value2: 'a' };
{
let d2: Data3 = d1; // 所有権の移譲は発生せず、d1のcopyをd2に与えた
} // d2のみ解放される
println!("{}, {}", d1.value1, d1.value2); // d1はそのまま使える
}
5-2. 完全コピー: Clone trait
fn main() {
let s1: String = String::from("hello");
{
let s2: String = s1.clone(); // 所有権の移譲は発生せず、s1のcloneをs2に与えた
} // s2のみ解放される
println!("{}, world!", s1); // s1はそのまま使える
}
実際メモリに格納したイメージ:
Clone trait の実現:
#[derive(Clone)]
struct Data {
pub value: String
}
fn main() {
let d1: Data = Data { value: String::from("hello, data") };
{
let d2: Data = d1.clone();
}
println!("{}", d1.value);
}
6. 所有権を移譲しない: 参照と借用
fn main() {
let s1: String = String::from("hello");
let s: &String = &s1;
let s: &String = &&&s1; // コンパイルOK: s1の参照の参照の参照(やめましょう)
}
実際メモリに格納したイメージ:
- 4-2の問題を参照と借用で解決する
fn does_not_takes_ownership(s: &String) -> usize {
s.len()
}
fn main() {
let s1: String = String::from("hello");
let length = does_not_takes_ownership(&s1);
println!("{} : size({})", s1, length);
}
6-1. Mutableな参照
fn main() {
let s1: String = String::from("hello");
let s: &mut String = &mut s1;
}
コンパイルエラー:
error[E0596]: cannot borrow immutable local variable `s1` as mutable
--> src/main.rs:75:31
|
74 | let s1: String = String::from("hello");
| -- help: make this binding mutable: `mut s1`
75 | let s: &mut String = &mut s1;
| ^^ cannot borrow mutably
- Mutableな参照はMutableな所有権からしか借用できない
fn main() {
let mut s1: String = String::from("hello"); // Mutableな所有権
println!("{}", s1);
{
let s: &mut String = &mut s1; // Mutableな参照
s.push_str(", mut ref"); // Mutableな参照を使って文字列をappendした
}
println!("{}", s1);
}
6-2. 参照の制限
- 参照の有効スコープは所有権のを超えてはいけない
fn main() {
let s;
{
let s1: String = String::from("hello");
s = &s1;
}
println!("{}", s);
}
コンパイルエラー:
error[E0597]: `s1` does not live long enough
--> src/main.rs:104:14
|
104 | s = &s1;
| ^^ borrowed value does not live long enough
105 | }
| - `s1` dropped here while still borrowed
106 | println!("{}", s);
107 | }
| - borrowed value needs to live until here
-
参照とMutableな参照は同時に混在できない
- edition-2018のNon-lexical lifetimesの場合、ライフタイムが広い方が使われるまでの間に、他の参照またはmut参照が宣言されたらコンパイルエラーになる
- edition-2018の場合、下記の例だと、s2とs3の宣言順番が逆にしたらコンパイルエラーにはならない
fn main() {
let mut s1: String = String::from("hello");
let s2: &String = &s1;
let s3: &mut String = &mut s1;
println!("{}", s2);
}
コンパイルエラー:
error[E0502]: cannot borrow `s1` as mutable because it is also borrowed as immutable
--> src/main.rs:95:27
|
94 | let s2: &String = &s1;
| --- immutable borrow occurs here
95 | let s3: &mut String = &mut s1;
| ^^^^^^^ mutable borrow occurs here
96 | println!("{}", s2);
| -- immutable borrow later used here
-
Mutableな参照は同時に複数存在できない
- edition-2018のNon-lexical lifetimesの場合、ライフタイムが広い方が使われるまでの間に、他のmut参照が宣言されたらコンパイルエラーになる
- edition-2018の場合、下記の例だと、s2とs3の宣言順番が逆にしたらコンパイルエラーにはならない
fn main() {
let mut s1: String = String::from("hello");
let s2: &mut String = &mut s1;
let s3: &mut String = &mut s1;
s2.push_str(", error occur here");
}
コンパイルエラー:
error[E0499]: cannot borrow `s1` as mutable more than once at a time
--> src/main.rs:100:27
|
99 | let s2: &mut String = &mut s1;
| ------- first mutable borrow occurs here
100 | let s3: &mut String = &mut s1;
| ^^^^^^^ second mutable borrow occurs here
101 | s2.push_str(", error occur here");
| -- first borrow later used here
7. 特別な参照: Slice
Sliceを使用すると、コレクション全体だけではなく、コレクション内の一部連続した要素も参照できる。
SliceはStackに格納されるため、コレクションのサイズは固定になる。
7-1. 文字列Slice: &str
fn main() {
let s: &str = "hello"; // 文字列リテラルは&strである
let s1: String = String::from("hello");
let ss1: &str = s1.as_str(); // => 「hello」の参照
let ss1: &str = &s1[..]; // => 「hello」の参照
let ss1: &str = &s1[2..4]; // => 「hello」の「ll」部分の参照
let ss1: &str = &s1[2..=4]; // => 「hello」の「llo」部分の参照
}
7-2. &str vs &String
自動キャスト | 結果 |
---|---|
&String -> &str | OK |
&str -> &String | NG |
fn takes_ref_str(s: &str) {}
fn takes_ref_string(s: &String) {}
fn main() {
takes_ref_str(&s1);
takes_ref_str(s1.as_str());
takes_ref_string(&s1);
takes_ref_string(s1.as_str()); // ここだけコンパイルエラーになる
}
コンパイルエラー:
error[E0308]: mismatched types
--> src/main.rs:132:22
|
132 | takes_ref_string(s1.as_str());
| ^^^^^^^^^^^ expected struct `std::string::String`, found str
|
= note: expected type `&std::string::String`
found type `&str`
7-3. その他のSlice
fn main() {
let s_i32: &[i32; 3] = &[1, 2, 3];
}
8. インスタンスメソッド
#[derive(Debug)]
struct Object {
pub data: String,
}
impl Object {
fn takes_self(self) {}
fn takes_ref_self(&self) {}
fn takes_ref_mut_self(&mut self) {
self.data.push_str(", &mut self")
}
}
fn main() {
let o1 = Object { data: "hello".to_string() };
o1.takes_self();
println!("{:?}", o1); // ここだけコンパイルエラーになる
let o2 = Object { data: "hello".to_string() };
o2.takes_ref_self();
println!("{:?}", o2);
let mut o3 = Object { data: "hello".to_string() };
o3.takes_ref_mut_self();
println!("{:?}", o3);
}
コンパイルエラー:
error[E0382]: use of moved value: `o1`
--> src/main.rs:146:22
|
145 | o1.takes_self();
| -- value moved here
146 | println!("{:?}", o1);
| ^^ value used here after move
|
= note: move occurs because `o1` has type `Object`, which does not implement the `Copy` trait