0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rust の所有権チョットワカル

0
Posted at

はじめに

趣味で Tauri でアプリを作っており、Rust に初めて触れました。

Rust といえば、所有権という独特なシステムがあります。

この所有権について、少し分かってきたので、メモを残したいと思います。

基本

Rust チュートリアルから、変数の可変性です。

変数には、ミュータブル(可変)とイミュータブル(不変)の 2 つがあります。

珍しいことに、Rust はデフォルトでは不変です。

fn main() {
    let x = 1;
    println!("x={x}");

    // ここで x = 2; と書くとコンパイルエラーになる
}

実際にコンパイルエラーになる様子

error[E0384]: cannot assign twice to immutable variable `x`                                                                                                                                                                    
 --> src\main.rs:5:5
  |
2 |     let x = 1;
  |         - first assignment to `x`
...
5 |     x = 2;
  |     ^^^^^ cannot assign twice to immutable variable
  |
help: consider making this binding mutable
  |
2 |     let mut x = 1;
  |         +++

このため可変にするには、mut 修飾子を書きます。

fn main() {
    let mut x = 1;
    println!("x={x}");

    x = 2;
    println!("x={x}");
}

不変変数の所有権

所有権というのは、メモリ上にある実データを参照するポインタの管理というのが、今のところしっくりきます。

公式のチュートリアルだと、ヒープメモリを使った文字列型で説明されています。

所有権には、2つの考え方があり、移動と借用です。

ここではまず、不変の変数における所有権をまとめます。

移動

まず、移動です。他のプログラミング言語と同じ感覚で書くと、この「移動」になります。

fn main() {
    let s1 = String::from("word");
    // s1 のアドレスを表示
    println!("s1={:p}", &s1);
    // s1 が示す word という文字列が格納されたメモリのアドレスを表示
    println!("str={:p}", s1.as_str());

    // ここが所有権の移動
    let s2 = s1;
    println!("s2={:p}", &s2);
    println!("str={:p}", s2.as_str());

    // ここで、println!("s1={:p}", &s1); を書くとコンパイルエラーが起きる

    // 実行結果
    // s1=0x713f91f6d0
    // str=Pointer { addr: 0x18944a8a220, metadata: 4 }
    // s2=0x713f91f740
    // str=Pointer { addr: 0x18944a8a220, metadata: 4 }
}

コンパイルエラー内容

error[E0382]: borrow of moved value: `s1`                                                                                                                                                                                      
  --> src\main.rs:10:25
   |
 2 |     let s1 = String::from("word");
   |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
...
 6 |     let s2 = s1;
   |              -- value moved here
...
10 |     println!("s1={:p}", &s1);
   |                         ^^^ value borrowed here after move
   |

10 行目で、「move したあとに借用しようとしてます」と怒られました。

また、実行結果から、格納先アドレスは変わらないですが、格納元のアドレスが変わっていることがわかると思います。

これが、移動ですね。

移動は、1度しかできません。

fn main() {
    let s1 = String::from("word");
    println!("s1={:p}", &s1);
    println!("str={:p}", s1.as_str());

    let s2 = s1;
    println!("s2={:p}", &s2);
    println!("str={:p}", s2.as_str());

    let s3 = s1;
    println!("s3={:p}", &s3);
}

再度移動している 10 行目でコンパイルエラーです。移動をした場合に、すでに s1 に所有権がなくなるため、再度の移動は許されないようです。

error[E0382]: use of moved value: `s1`                                                                                                                                                                                         
  --> src\main.rs:10:14
   |
 2 |     let s1 = String::from("word");
   |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
...
 6 |     let s2 = s1;
   |              -- value moved here
...
10 |     let s3 = s1;
   |              ^^ value used here after move
   |

借用

s1 も引き続き参照し続けたい場合において、借用を行います。

& アンパサンドを付ければ、借用が行えます。

fn main() {
    let s1 = String::from("word");
    println!("s1={:p}", &s1);
    println!("str={:p}", s1.as_str());

    // ここで借用
    let s2 = &s1;
    println!("s2={:p}", &s2);
    println!("str={:p}", s2.as_str());

    println!("s1={:p}", &s1);

    // 実行結果
    // s1=0x555b5ff620
    // str=Pointer { addr: 0x244890aa220, metadata: 4 }
    // s2=0x555b5ff690
    // str=Pointer { addr: 0x244890aa220, metadata: 4 }
    // s1=0x555b5ff620
}

借用は、何度でも行えます。

fn main() {
    let s1 = String::from("word");
    println!("s1={:p}", &s1);
    println!("str={:p}", s1.as_str());

    let s2 = &s1;
    println!("s2={:p}", &s2);
    println!("str={:p}", s2.as_str());

    let s3 = &s1;
    println!("s3={:p}", &s3);
    println!("str={:p}", s3.as_str());

    // 実行結果
    // s1=0xb15a2ff730
    // str=Pointer { addr: 0x2a9a43ea220, metadata: 4 }
    // s2=0xb15a2ff7a0
    // str=Pointer { addr: 0x2a9a43ea220, metadata: 4 }
    // s3=0xb15a2ff800
    // str=Pointer { addr: 0x2a9a43ea220, metadata: 4 }
}

借用と移動の混合

当然ですが、移動のあとの借用は認められますが、借用のあとの移動は認められません。

fn main() {
    let s1 = String::from("word");
    println!("s1={:p}", &s1);
    println!("str={:p}", s1.as_str());

    let s2 = &s1;
    println!("s2={:p}", &s2);
    println!("str={:p}", s2.as_str());

    let s3 = s1;
    println!("s3={:p}", &s3);
    println!("str={:p}", s3.as_str());

    let s4 = &s1;
    println!("s4={:p}", &s4);
    println!("str={:p}", s4.as_str());
}

エラー内容。s4 に対して借用しようとして、怒られましたね。

error[E0382]: borrow of moved value: `s1`                                                                                                                                                                                      
  --> src\main.rs:14:14
   |
 2 |     let s1 = String::from("word");
   |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
...
10 |     let s3 = s1;
   |              -- value moved here
...
14 |     let s4 = &s1;
   |              ^^^ value borrowed here after move
   |

可変変数の所有権

可変の変数に対する所有権は、基本的に不変と同じです。

しかし、借用と代入が混ざる場合には、注意が必要です。

借用と代入

以下のサンプルのように、借用後に元の値を変更するとエラーになります。

これは、s1 で管理している情報が s2 と異なることから、s2 のデータが信用できなくなるためだと思います。

fn main() {
    let mut s1 = String::from("word");
    println!("s1={:p}", &s1);
    println!("str={:p}", s1.as_str());

    // 借用
    let s2 = &s1;
    println!("s2={:p}", &s2);
    println!("str={:p}", s2.as_str());

    // ここで変更する
    s1 = String::from("hello");
    println!("s1={:p}", &s1);
    println!("str={:p}", s1.as_str());
    
    // 借用したものを参照しようとするとエラー
    // println!("s2={:p}", &s2);
    // println!("str={:p}", s2.as_str());

    // 実行結果
    // s1=0x9079b7f7f0
    // str=Pointer { addr: 0x22cada2a240, metadata: 4 }
    // s2=0x9079b7f860
    // str=Pointer { addr: 0x22cada2a240, metadata: 4 }
    // s1=0x9079b7f7f0
    // str=Pointer { addr: 0x22cada2cc90, metadata: 5 }
}

エラー内容は、以下の通りです。エラー内容は、s1 はすでに借用してるから、変更しないでくれ、と言われています。

error[E0506]: cannot assign to `s1` because it is borrowed                                                                                                                                                                     
  --> src\main.rs:10:5
   |
 6 |     let s2 = &s1;
   |              --- `s1` is borrowed here
...
10 |     s1 = String::from("hello");
   |     ^^ `s1` is assigned to here but it was already borrowed
...
14 |     println!("s2={:p}", &s2);
   |                         --- borrow later used here

コーディングする側からしてみると、mut 宣言しているのだから変更できると思うのですが、コンパイラからしたら、s1 の指すメモリが変わっているので、借用している s2 の指す値は管理外メモリとなり、管理に困るのでしょう。

所有権のための思想(ハッシュマップの操作)

Rust はこの思想のもと、他の言語と所有権操作のため関数の考え方が少し異なります。

「逆に所有権というのは手間なのでは?」という思いが一瞬沸きますが、厳格なメモリ管理を行うためには必要なので、少し私の体験から説明させてください。

私は、HashMap 構造体でそれを実感しました。

例えば、ハッシュマップの内容を仕分けして、別のハッシュマップに移動させる場合です。C# であれば、単純にキーと値(参照)を移動先に渡すだけでよいですが、Rust の場合は元のハッシュマップに残すことはできません。そのために drain() という関数が用意されています。

use std::collections::HashMap;

struct Item {
    _id: u32,
}

fn main() {
    let mut hash: HashMap<u32, Item> = HashMap::from([
        (1, Item{ _id: 1 }),
        (2, Item{ _id: 2 }),
        (3, Item{ _id: 3 }),
    ]);

    let mut odd: HashMap<u32, Item> = HashMap::new();
    let mut even: HashMap<u32, Item> = HashMap::new();

    for (key, value) in hash.drain() {
        if key % 2 == 0 {
            even.insert(key, value);
        } else {
            odd.insert(key, value);
        }
    }

    println!("{}, {}, {}", hash.len(), even.len(), odd.len());
    // 結果
    // 0, 1, 2
}

これは、remove() を使っても同じです。JavaScript でいうと、配列を pop() する感覚に近いです。

fn main() {
    let mut hash: HashMap<u32, Item> = HashMap::from([
        (1, Item{ _id: 1 }),
        (2, Item{ _id: 2 }),
        (3, Item{ _id: 3 }),
    ]);

    let mut odd: HashMap<u32, Item> = HashMap::new();
    let mut even: HashMap<u32, Item> = HashMap::new();

    let keys: Vec<u32> = hash.keys().cloned().collect();
    for num in keys {
        let item = hash.remove(&num).unwrap();
        if num % 2 == 0 {
            even.insert(num, item);
        } else {
            odd.insert(num, item);
        }
    }

    println!("{}, {}, {}", hash.len(), even.len(), odd.len());
    // 結果
    // 0, 1, 2
}

主題からは逸れますが hash.keys().cloned().collect() も所有権を意識した関数になります。キーの一覧の所有権と keys が紐づかないようにするための方法です。

このように、Rust はデータをどう操作するかという関数設計ではなく、データに伴う所有権をどう操作するのか、という観点で設計されています。remove()drain() の関数仕様を理解して、私は Rust の所有権思想は他と違うなと実感しました。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?