Help us understand the problem. What is going on with this article?

Rust Tutorial[2版] めも 4章 4.1 所有権とは

More than 1 year has passed since last update.

はじめに

はじめまして、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を束縛したのちに、yxの値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では以下のことが書かれていました。

  • 所有権とは何なのか?

一言で言えば簡単ですが、慣れないうちは手こずるかもしれません。

今回は関数内での所有権の振る舞いについて色々とテストしている内にかなり時間を使ってしまったので、参照と借用は次にやろうと思います。

task4233
競プロ(AtCoder:task4233) | CTF | VuePress(Vue.js) | Golang | FE | AP | DB | seccamp2019 A | GitHub: https://github.com/task4233
https://task4233.hatenablog.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした