はじめに
趣味で 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 の所有権思想は他と違うなと実感しました。