はじめに
はじめまして、task4233です。
これはあくまでメモなので、基本的にググってわかるようなことは詳しく書きません。
よろしくお願いします。
なお、以下のプログラムでは面倒なのでfn main() {}
を省略することがあります。
ご承知おきください。
おさらい
前回は1版の4章の4.5-4.6に目を通して、以下のことを学びました。
- if-else, else ifの使い方とその仕様
- loop, while, forの3種類のループとそのオプション
以下のリンクで参照できます。
https://qiita.com/task4233/items/57e7a2abed504831a245
この記事の目的
Rust Tutorialの各章ごとでメモを残しておくことにより、一通り目を通した後に見返せるようにすることを目的としています。
そのため、各章の最初に何を目的とするか、最後に小さなまとめをメモしています。
なお、Rust Tutorial2版のリンクはこちらです。
(https://doc.rust-jp.rs/book/second-edition/foreword.html)
4. 所有権を理解する
所有権はRustの最もユニークな機能であり、これのおかげでガベージコレクタなしで安全性担保を行うことができるのです。 故に、Rustにおいて、所有権がどう動作するのかを理解するのは重要です。
だそうです。
4.1 所有権とは?
所有権の概念を理解すること。
どのような場合にムーブされ、どのような場合にコピーされるのかを理解すること。
所有権とは
所有権規則
Rustの所有権について、以下のルールがあります。
- Rustの各値は、
owner
(以下、所有者とする)と呼ばれる変数と対応している。 - 1度に1つの所有者のみを持つ。
- 所有者がスコープから外れた時に値は破棄される。
変数スコープ
スコープは以前触れましたので、参考コードのみ書いておきます。
{ // sは、ここでは有効ではない。まだ宣言されていない
let s = "hello"; // sは、ここから有効になる
// sで作業をする
} // このスコープは終わり。もうsは有効ではない
String
型
String
型は文字列型で、以下のように生成できます。
let s = String::from("hello"); // fromは文字列リテラルからString型を生成する
さらにこの文字列は以下のように可変です。
let mut s = String::from("hello"); // mut s: String
s.push_str(", world!"); // push_str()関数でリテラルをStringに付け加える
println!("{}", s); // hello, world!を出力
メモリと確保
Rustではメモリを所有している変数がスコープを抜けた瞬間にメモリが自動的に返還される。
なぜなら、Rustは閉じ括弧で自動的にdrop
関数を呼び出すから。
変数とデータの相互作用法: ムーブ
ただし、ヒープ上に確保されたデータを複数の変数に使用させるような場面では、コードの振る舞いは非常に複雑になる。
以下のコードではx
に5を束縛したのちに、y
にx
の値5を束縛します。
ここで、5という値は既知の固定サイズの単純な値であり、2つの5という数字はスタックに積まれるのでエラーは発生しません。
let x = 5; // x: i32
let y = x; // y: i32
println!("x: {}", x);
// [output]
// x: 5
しかし、先ほどのString
型ではそうは行きません。
String
型は既知の固定サイズの単純な値ではなく、以下の3要素でできています。
- ポインタ(ptr)
- 長さ(len)
- 容量(capacity)
このString
について他の変数に対して束縛する場合、所有権がその変数に移り、元々の変数束縛は無効化されます。
以下のように、他の変数に対して束縛をした後に、元々の変数にを参照しようとするとエラーが表示される訳です。
let s1 = String::from("hello");
let s2 = s1;
println!("{}", s1); // s1の所有権はs2に移っているためコンパイルエラー
println!("{}", s2); // 「hello」と出力される
ただし、このコードで改めて所有権をs2からs1に移せばs1の参照が可能になります(その場合s2の参照は不可能になります)。
let mut s1 = String::from("hello");
let s2 = s1;
s1 = s2;
println!("{}", s1); // 「hello」と出力される
println!("{}", s2); // s2の所有権はs1に移っているためコンパイルエラー
このように所有権を移したり無効化したりするのは、メモリ安全のためです。
仮に、所有権を譲渡せずに値をそのままディープコピーするとどうなるでしょうか?
先ほど、Rustでは閉じ括弧でdrop
関数を呼び出してヒープメモリを解放すると言いました。
仮に値をそのままコピーしていた場合、s1に対して解放を行ってからs2に対する解放を行うと二重解放エラーを引き起こします。
このエラーはメモリの退廃やセキュリティ上の脆弱性を生む恐れがあります。
したがって、所有権を譲渡する仕組みはRust全体の安全に貢献しているのです。
変数とデータの相互作用法: クローン
ただ単に変数束縛を行うとdeep copyは出来ませんが、以下のようにclone
メソッドで行うことが出来ます。
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1: {}, s2: {}", s1, s2);
// [output]
// s1: hello, s2: hello
clone
メソッドの呼び出しを見かけたら、そのコードは実行コストが高く、何か違うことが起こっていることが一目でわかります。
スタックのみのデータ: コピー
先ほど既知の固定サイズの単純な値ならばスタックに積まれるので、以下のようなコードはclone
を使用せずともコピーに関するエラーが発生していませんでした。
let x = 5; // x: i32
let y = x; // y: i32
println!("x: {}", x);
// [output]
// x: 5
このようにclone
を使用せずともコピーに関するエラーが発生しない型は以下の型です。
- あらゆる整数型(
u8〜u64
,i8〜i64
,usize
,isize
) - 論理値型(
bool
,true
,false
) - 浮動小数点型(
f32
,f64
) - 文字型(
char
) - 以上の型のみを含むタプル((
i32
,String
)は不可)
所有権と関数
関数に変数を渡すときも所有権が譲渡されます。
したがって、String
型を利用する以下のコードではmain関数内のprintln!
ではコンパイルエラーが発生します。
fn main() {
let s = String::from("hello"); // sがスコープに入る
takes_ownership(s); // sの所有権が関数内のnew_sに移る
println!("{}", s); // sの所有権は移った後なので、コンパイルエラー
}
fn takes_ownership(new_s: String) { // new_sがスコープに入る
println!("{}", new_s);
} // ここでnew_sがスコープを抜けてdropが呼ばれ、メモリが解放される
それに対して、整数型のi32
を利用する場合はclone
を使用せずともコピーに関するエラーは発生しないので、main関数内のprintln!
ではコンパイルエラーが発生しなくなります。
fn main() {
let n: i32 = 5; // nがスコープに入る
takes_ownership(n); // nが関数内のnew_sにコピーされる
println!("{}", n); // sはコピーされただけなので、エラーは発生しない
}
fn takes_ownership(new_n: i32) { // new_sがスコープに入る
println!("{}", new_n);
} // ここでnew_sがスコープを抜けてdropが呼ばれ、new_sのメモリが解放される
戻り値とスコープ
値を返すときにも所有権は移動します。
先ほどのString
でエラーになっていたsも、値を返すことでコンパイルエラーが発生しなくなります。
fn main() {
let mut s = String::from("hello"); // sがスコープに入る
takes_ownership(s); // sの所有権が関数内のnew_sに移る
s = give_ownership(); // some_sの所有感がsに移る
println!("{}", s);
}
fn takes_ownership(new_s: String) { // new_sがスコープに入る
println!("{}", new_s);
} // ここでnew_sがスコープを抜けてdropが呼ばれ、メモリが解放される
fn give_ownership() -> String {
let some_s = String::from("hello"); // some_sがスコープに入る
some_s // some_sが返され、呼び出し元にムーブされる
}
複数の戻り値を返す
以下のように、
タプルを用いることで複数の値を返すことが可能です。
fn main() {
let s1 = String::from("hello");
let (s2, len) = calculate_length(s1);
println!("The length of '{}' is {}.", s2, len);
}
fn calculate_length(s: String) -> (String, usize) {
let length = s.len();
(s, length)
}
// [output]
// The length of 'hello' is 5.
所有権は束縛により移ることがあり、これが安全性を高めることに繋がる。
ムーブされず、コピーされるのは以下の型である。
あらゆる整数型(u8〜u64, i8〜i64, usize, isize)
論理値型(bool, true, false)
浮動小数点型(f32, f64)
文字型(char)
以上の型のみを含むタプル((i32, String)は不可)
これら以外の型でdeep copyを行いたい場合はcloneを利用する。
おわりに
2版4章4.1では以下のことが書かれていました。
- 所有権とは何なのか?
一言で言えば簡単ですが、慣れないうちは手こずるかもしれません。
今回は関数内での所有権の振る舞いについて色々とテストしている内にかなり時間を使ってしまったので、参照と借用は次にやろうと思います。