はじめての Rust 入門 Part3 ~所有権について学ぶまで~

はじめての Rust 入門 Part3 ~所有権について学ぶまで~

link です。

高速でセキュリティ的にも安全な言語として Rust が注目を集めています。

今回はそんな Rust の勉強をしていきます。

本記事は Part2 の続きになっています。

想定環境

  • Windows 11
  • Rust 1.72

所有権とは

所有権は Rust の中心といってもいい機能です。

すべてのプログラムは、実行中に何らかの形でメモリーを管理する必要があります。

メモリーを管理する方法としては、以下の 2 種類が挙げられます。

  • ガベージコレクション
  • 明示的なメモリーの確保とメモリーの解放

Rust ではどちらも使いません。

メモリーは、コンパイラがチェックする文法に基づいた、所有権システムを通じて管理されています。

所有権のルール

先に所有権のルールについて述べておきます。

  • Rust の各値は所有者と呼ばれる変数と対応している
  • いかなる時も値の所有者は 1 つだけである
  • 所有者がスコープから外れたら、値は破棄される

これらを踏まえたうえで、さまざまな所有権の例を見ていきます。

変数のスコープ

分かりやすい例としてまず、変数のスコープについてみていきます。

変数のスコープ
{
    let s = "scope"; // ここから有効
    // s で作業
} // スコープ終了後、s が解放

変数のスコープでの所有権については、以下の 2 点が挙げられます。

  • 変数がスコープ内で宣言されると有効になる
  • スコープを抜けると解放される

スコープを抜けると変数が解放されるのは他のプログラミング言語と一緒です。

Rust におけるメモリーの確保

変数のスコープで紹介したものはすべてスタック領域に保管され、スコープが終わるとスタック領域から取り除かれます。

では、スタック領域ではなくヒープ領域に保存されるデータは Rust ではどのように扱われるのでしょうか。

例として String 型の変数を見ていきます。 String 型では、可変かつ伸長可能なテキストを扱うために、コンパイル時には不明なサイズのメモリーをヒープ領域に確保します。

そのため、 String 型は使用し終わったら、メモリーを解放する必要があります。

メモリーを解放する手段はプログラミング言語によって異なります。

C や C++ では free() でメモリー解放を宣言し、 Java や C# ではガベージコレクションによって自動的に使用されなくなったメモリーを解放しています。

Rust ではスコープを抜けると drop() という特殊な関数を自動で呼び出し、メモリーを解放するようになっています。

ムーブとコピー

先ほど、Rust ではスコープを抜けると drop() という特殊な関数を自動で呼び出し、メモリーを解放するようになっていると説明しました。

では、以下のコードのように 2 つの変数の片方がもう片方のポインターを参照しているとどうなるでしょうか。

ムーブ
let s1 = String::from("hello");
let s2 = s1;

println!("{}, world!", s2);

s1s2 は同じメモリーを参照しているため、二重解放が発生してしまうように見えます。

ですが Rust では変数ポインターを別の変数ポインターに代入すると代入に利用した変数は無効になります。これをムーブと言います。

つまり、上のコード例では s2s1 を代入した段階で s1 は無効な変数として扱われるようになります。

なお、ムーブではなく変数の値をディープコピーする場合は clone() メソッドを利用すれば可能です。

コピー
let s1 = String::from("hello");
let s2 = s1.clone();

println!("s1 = {}, s2 = {}", s1, s2);

また、ヒープ領域ではなくスタック領域に保存される変数は別変数に代入しても元の変数が無効化されることはありません。

スタック型の変数のコピー
let x = 5;
let y = x;

println!("x = {}, y = {}", x, y);

Rust では以下の変数型がスタック領域に保存されます。

  • i32 などのあらゆる整数型
  • 論理値型の bool
  • f64 などのあらゆる浮動小数点型
  • 文字型の char
  • スタック領域に保存される変数型のみを含むタプル型 (例: (i32, i32))

所有権と関数

関数に値を渡すことでも所有権は移動します。

以下のコードでは takes_ownership() に渡された文字列はそのまま所有権がムーブし、関数の終了とともに解放されています。 そして、 makes_copy() に渡された変数はそのまま値が関数内にコピーされるため、関数が終了してもそのまま変数を利用し続けることができます。

引数の所有権
fn main() {
    let s = String::from("hello"); // 文字列型
    takes_ownership(s); // s の値が関数にムーブされ、 s はこの後使えなくなる

    let x = 5; // i32 型
    makes_copy(x); // i32 はコピーされるため、この後でも x は使える
}

fn takes_ownership(some_string: String) {
    println!("{}", some_string);
} // ここで some_string がスコープを抜け、 drop() によってメモリが解放される

fn makes_copy(some_integer: i32) {
    println!("{}", some_integer);
}

戻り値とスコープ

値を返すことでも所有権は移動します。

以下のコードでは gives_ownership() では戻り値の所有権をそのまま代入先の s1 にムーブしています。 また takes_and_gives_back() に渡された s2 の所有権は関数に渡りますが、関数から返された値の所有権がそのまま s3 に渡されています。

戻り値の所有権
fn main() {
    let s1 = gives_ownership(); // gives_ownership は、戻り値を s1 にムーブする
    let s2 = String::from("hello");
    let s3 = takes_and_gives_back(s2); // s2 は takes_and_gives_back にムーブされ戻り値も s3 にムーブされる
}

fn gives_ownership() -> String { // gives_ownership は戻り値を呼び出した関数にムーブする
    let some_string = String::from("hello"); // some_string がスコープに入る

    some_string // some_string が返され、呼び出し元関数にムーブされる
}
fn takes_and_gives_back(a_string: String) -> String {
    a_string  // a_string が返され、呼び出し元関数にムーブされる
}

参照と借用

関数に値を渡すと所有権が移動しうることを説明しましたが、値の所有権ではなく、代わりに引数としてオブジェクトへの参照を渡す借用について説明します。

以下のコードでは通常の変数の代わりに $ がついた参照型の変数を引数に指定しています。

変数の借用
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()
}

ただし、この書き方では参照型の変数の値の変更はできません。

参照型の変数の値を変更する必要がある場合は以下のコードのように &mut をつける必要があります。

可変な参照型
fn main() {
    let mut s = String::from("hello");

    change(&mut s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

この可変な参照型変数は一つしか持てないことには注意してください。

この制約はデータ更新が競合する事象の発生をコンパイル時点で防いでいます。

スライス型

Rust ではスライスという可変サイズのコレクションの一部分のみに参照を取る手段が用意されています。

たとえば、 String 型なら以下のコードのように指定した範囲の文字にのみ参照を取ることができます。

文字列スライス
let s = String::from("hello world");

let hello = &s[0..5]; // hello
let world = &s[6..11]; // world

&s[..6]&s[1..] のように参照開始点や参照終了点のみの指定もできます。

配列も同様にスライス型の参照を取ることができます。

配列のスライス
let a = [1, 2, 3, 4, 5];

let slice = &a[1..3];

参考サイト

まとめ

今回は Rust の所有権について勉強しました。

次回は Rust の構造体について勉強していきます。

それではまた、別の記事でお会いしましょう。

linkohta