自己紹介
出田 守と申します。
しがないPythonプログラマです。
情報セキュリティに興味があり現在勉強中です。CTFやバグバウンティなどで腕を磨いています。主に低レイヤの技術が好きで、そっちばかり目が行きがちです。
Rustを勉強していくうえで、読んで学び、手を動かし、記録し、楽しく学んでいけたらと思います。
環境
新しい言語を学ぶということで、普段使わないWindowsとVimという新しい開発環境で行っています。
OS: Windows10 Home 64bit 1903
CPU: Intel Core i5-3470 @ 3.20GHz
Rust: 1.38.0
RAM: 8.00GB
Editor: Vim 8.1.1
Terminal: PowerShell
前回
前回は文字列型について学びました。
Rust勉強中 - その10
次に進む前に
前々回ベクタを生成する際にあらかじめサイズが分かっている場合はwith_capacityを使った方がわずかに効率が良いと書きました。気になったので速度に差が出るのか試してみました。
以下のようなコードです。
const N: usize = 100000000;
fn main() {
let mut s = String::new();
// let mut s = String::with_capacity(N);
for _i in 0..N {
s.push('X');
}
}
Stringのバッファに1億回'X'をpushします。
まずは、String::new()
で時間を計測してみます。
※ PowerShellのMeasure-Commandで計測しています。
回数 | 実行時間(最適化なし) | 実行時間(最適化あり) |
---|---|---|
1回目 | 8.1796408 | 0.2835464 |
2回目 | 8.4658226 | 0.2719309 |
3回目 | 8.2872695 | 0.2760315 |
4回目 | 8.2663929 | 0.2737734 |
5回目 | 8.2630283 | 0.2734954 |
平均 | 8.29243082 | 0.27575552 |
次に、String::with_capacity(N)
で時間を計測してみます。
回数 | 実行時間(最適化なし) | 実行時間(最適化あり) |
---|---|---|
1回目 | 8.0972565 | 0.2094571 |
2回目 | 8.3242392 | 0.2092475 |
3回目 | 8.30591 | 0.2086652 |
4回目 | 8.2027424 | 0.206948 |
5回目 | 8.2062353 | 0.2131908 |
平均 | 8.22727668 | 0.20950172 |
結果から最適化なしの実行にはそれほど差はないですね。一方で、最適化ありだと差があるようです。やはり、with_capacityありだと速くなっているようですね。
ちなみに、StringではなくVecで同じように試すと、やはりこちらもwith_capacityの方が約0.2秒速くなっていました。
あと、全然関係ないのですが、最適化なしとありとでの差がすごいなって思いました。こんな速なるんやって感じですよね。
所有権
Rustにおける所有権とは、所有者(変数)が、値を所有する権利をいいます。そして、値の所有者は常に一つです。値の所有者が解放(Rustではdrop(ドロップ)といいます)されれば、所有している値もドロップされます。
{
let mut owner = vec![0, 0, 0];
}
上記のコードは、あるスコープ内で変数ownerは、ベクタVec<i32>という値の所有権を持っています。ownerはこのスコープから外れると、ドロップされ、所有している値Vec<i32>もドロップされます。
なぜ所有権があるのか
所有権は解放されたメモリ領域への誤った参照を防ぐためにあります。所有権によって、現在の値に対する所有者や変数、保持している値の生存期間を明確にし、コンパイラで管理できます。コンパイラによってそれらはチェックされ、誤った参照があればエラーになります。
では所有権がない場合を考えてみます。
ある値Aを、所有者(owner1)は所有しているとします。またその値Aの一部を、別の所有者(owner2)が所有しているとします。
所有者 | 所有 |
---|---|
owner1 | 値A全体 |
owner2 | 値Aの一部 |
次にowner1が解放され、所有している値のメモリ領域も解放されたとします。
所有者 | 所有 |
---|---|
owner2 | (解放済みの)値Aの一部 |
このときowner2が値を参照した場合、解放済みのメモリ領域を参照することになり予期しないエラーになります。
所有権とツリー構造
所有者と所有されている値の関係はツリー構造となっているそうです。つまり、所有者が親で、所有されている値は子となります。
{
let mut owner = vec![vec![0, 0, 0]; 3];
}
上記のコードで変数ownerはツリー構造でいうrootです。ownerはVec<Vec<i32>>を子として持ちます。また、値Vec<Vec<i32>>のそれぞれの要素にもVec<i32>という子を持ちます。さらに、Vecのそれぞれの要素にも値0を持ちます。
owner -> Vec<Vec<i32>> -> Vec<i32> -> 0
-> 0
-> 0
-> Vec<i32> -> ...
-> Vec<i32> -> ...
この状況で、ownerがドロップされればツリー全体がドロップされます。owner[0]をドロップすればowner[0]より下の子はドロップされます。
fn main() {
{
let mut owner = vec![vec![0, 0, 0]; 3];
println!("owner = {:?}", owner);
owner.remove(0);
println!("owner = {:?}", owner);
}
}
owner = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
owner = [[0, 0, 0], [0, 0, 0]]
移動
所有権をほかの所有者(変数)へ移動させることができます。移動後は、所有権は移動先の所有者に移ります。そして、値の元所有者は、未初期化状態となります。つまり、参照ができません。
fn move1() -> String {
let s = String::from("String1");
s
}
fn move2(s: String) {
}
fn main() {
let mut s = String::from("String0"); // move0
s = move1(); // move1
move2(s); // move2
}
上記のコードには3種類の移動があります。
最初の移動(move0)は、String::from("String0")
で生成された値を、変数sに代入するときに所有権を移動させています。
次の移動(move1)では、move0とほとんど同じ意味で、move1関数内で生成された値String1を返す際に所有権を呼び出し元へ移動させています。この時、値String0はドロップされます。
最後の移動(move2)では、変数sの所有権を関数への引数に移動させています。
例えば、move2の前で変数sをほかの変数へ代入する式を入れてみます。
...
fn main() {
let mut s = String::from("String0"); // move0
s = move1(); // move1
let t = s;
move2(s); // move2
}
すると以下のようなエラーが発生しました。
error[E0382]: use of moved value: `s`
--> .\src\main.rs:17:11
|
14 | let mut s = String::from("String0"); // move0
| ----- move occurs because `s` has type `std::string::String`, which does not implement the `Copy` trait
15 | s = move1(); // move1
16 | let t = s;
| - value moved here
17 | move2(s); // move2
| ^ value used here after move
つまり、「変数tに変数sの所有権を移動させたので、move2関数の引数に変数sの所有権を移動させることはできませんでー」って言ってるんですね。
move1の時には、値String0がドロップされます。これをドロップしないようにするには、他の値を代入される前に、別の変数へ所有権を移動させます。
fn main() {
let mut s = String::from("String0"); // move0
let t = s;
s = move1(); // move1
println!("t = {}, s = {}", t, s);
move2(s); // move2
}
t = String0, s = String1
これら値の移動は、値そのものを移動するのではなく、fat pointerのみです。
制御フロー
fn main() {
...
let s = String::from("String");
if true {
f0(s); // move
} else {
f1(s); // move
}
f2(s); // error
}
上記を実行すると、
error[E0382]: use of moved value: `s`
--> .\src\main.rs:38:8
|
32 | let s = String::from("String");
| - move occurs because `s` has type `std::string::String`, which does not implement the `Copy` trait
33 | if true {
34 | f0(s); // move
| - value moved here
35 | } else {
36 | f1(s); // move
| - value moved here
37 | }
38 | f2(s); // error
| ^ value used here after move
if構文内ではf0関数かf1関数どちらか一方に変数sの値の所有権が移動させられるのでエラーは出ません。その後、f2関数ではすでに変数sの所有権はf0関数に移されているので、f2関数のところでエラーになります。
また、ループでは、以下のようなコードを実行した場合にエラーが出ます。
fn main() {
let s = String::from("String");
loop {
f0(s);
}
}
error[E0382]: use of moved value: `s`
--> .\src\main.rs:42:12
|
40 | let s = String::from("String");
| - move occurs because `s` has type `std::string::String`, which does not implement the `Copy` trait
41 | loop {
42 | f0(s);
| ^ value moved here, in previous iteration of loop
一週目のループでf0関数の引数に、変数sの所有権が移動されるため、二週目のループで、変数sは未初期化状態となります。その結果エラーになります。
インデックスされる値
インデックスされる値の所有権にも注意が必要なようです。
fn move_indexing() {
let mut v = vec!["abc".to_string(); 3];
let t = v[0];
}
上記のコードを実行すると、
error[E0507]: cannot move out of index of `std::vec::Vec<std::string::String>`
--> .\src\main.rs:52:13
|
52 | let t = v[0];
| ^^^^
| |
| move occurs because value has type `std::string::String`, which does not implement the `Copy` trait
| help: consider borrowing here: `&v[0]`
error: aborting due to previous error
これは、ベクタ内のどの要素が未初期化状態なのか管理しなければならなくなるため、Rustではこのような場合エラーを出します。
これを実現したい場合はいくつか方法があるようです。
- popやremoveメソッドで削除しながら取り出す
- std::mem::replaceで、新しい値と入れ替える
fn move_indexing() {
let mut v = vec!["abc".to_string(); 3];
// let t = v[0]; // error
let t = v.pop();
println!("v.pop(); v = {:?}, t = {:?}", v, t);
let t = std::mem::replace(&mut v[1], "def".to_string());
println!("std::mem::replace(&mut v[1], \"def\".to_string(); v = {:?}, t = {}", v, t);
}
v.pop(); v = ["abc", "abc"], t = Some("abc")
std::mem::replace(&mut v[1], "def".to_string(); v = ["abc", "def"], t = abc
コピー型
移動ルールが適用されない型もあるようです。
fn move_except() {
let i = 0;
let j = i;
let k = i;
println!("i = {}, j = {}, k = {}", i, j, k);
}
実行すると、
i = 0, j = 0, k = 0
コンパイルエラーにはなりませんでした。実はこの場合の値i32型にはCopyトレイトが実装されているため、代入した場合値のコピーとなります。こういった型は他にも、浮動小数点数やchar型、bool型などがあります。
こういった型は、値をコピーしても何ら影響がないためです。
一方でString型やベクタは、コピー型を持ちません。おそらくこの理由は、バッファが大きくなった場合にすべてをコピーして保持していると、それだけリソースコストや計算時間がかかるためだと思っています。
どのような型がコピー型を持つべきで、あるいは持つべきではないのかはやっていくうちに分かってくるのかなーと今のところ思っています。
所有権の共有
参照カウントを使って、ある値の所有権を複数の所有者が共有することができるそうです。それを実現するのが、RcとArcです。Rcはシングルスレッド用で、Arcはマルチスレッド用です。
fn move_Rc() {
let f = std::rc::Rc::new(vec![0.01, 0.02, 0.03]);
let g = f.clone();
let h = std::rc::Rc::clone(&f);
println!("f = {:p}, g = {:p}, h = {:p}", f, g, h);
println!("std::rc::Rc::strong_count(&f) = {}", std::rc::Rc::strong_count(&f));
println!("std::rc::Rc::strong_count(&g) = {}", std::rc::Rc::strong_count(&g));
println!("std::rc::Rc::strong_count(&h) = {}", std::rc::Rc::strong_count(&h));
}
f = 0x1cbd4759400, g = 0x1cbd4759400, h = 0x1cbd4759400
std::rc::Rc::strong_count(&f) = 3
std::rc::Rc::strong_count(&g) = 3
std::rc::Rc::strong_count(&h) = 3
変数f, g, hはcloneしたにもかかわらず、どれも同じアドレスを指しています。つまり、所有権を共有することができています。また、参照カウントも3となっています。
ただし、これらの値はイミュータブルです。
fn move_Rc() {
let f = std::rc::Rc::new(vec![0.01, 0.02, 0.03]);
let g = f.clone();
let h = std::rc::Rc::clone(&f);
f[0] = 0.05;
println!("f = {:p}, g = {:p}, h = {:p}", f, g, h);
println!("std::rc::Rc::strong_count(&f) = {}", std::rc::Rc::strong_count(&f));
println!("std::rc::Rc::strong_count(&g) = {}", std::rc::Rc::strong_count(&g));
println!("std::rc::Rc::strong_count(&h) = {}", std::rc::Rc::strong_count(&h));
}
error[E0596]: cannot borrow data in an `Rc` as mutable
--> .\src\main.rs:70:5
|
70 | f[0] = 0.05;
| ^ cannot borrow as mutable
|
= help: trait `DerefMut` is required to modify through a dereference, but it is not implemented for `std::rc::Rc<std
:vec::Vec<f64>>`
ソース
#![allow(unused)]
fn get_type<T>(_: T) -> &'static str {
std::any::type_name::<T>()
}
fn move1() -> String {
let s = String::from("String1");
s
}
fn move2(s: String) {
}
fn f0(s: String) {
println!("f0: s = {}", s);
}
fn f1(s: String) {
println!("f1: s = {}", s);
}
fn f2(s: String) {
println!("f2: s = {}", s);
}
fn move_examples() {
let mut s = String::from("String0"); // move0
let t = s;
s = move1(); // move1
println!("t = {}, s = {}", t, s);
move2(s); // move2
}
fn move_control() {
let s = String::from("String");
if true {
f0(s); // move
} else {
f1(s); // move
}
// f2(s); // error
// let s = String::from("String");
// loop {
// f0(s);
// }
}
fn move_indexing() {
let mut v = vec!["abc".to_string(); 3];
// let t = v[0]; // error
let t = v.pop();
println!("v.pop(); v = {:?}, t = {:?}", v, t);
let t = std::mem::replace(&mut v[1], "def".to_string());
println!("std::mem::replace(&mut v[1], \"def\".to_string(); v = {:?}, t = {}", v, t);
}
fn move_except() {
let i = 0;
let j = i;
let k = i;
println!("i = {}, j = {}, k = {}", i, j, k);
}
fn move_Rc() {
let f = std::rc::Rc::new(vec![0.01, 0.02, 0.03]);
let g = f.clone();
let h = std::rc::Rc::clone(&f);
// f[0] = 0.05; // error
println!("f = {:p}, g = {:p}, h = {:p}", f, g, h);
println!("std::rc::Rc::strong_count(&f) = {}", std::rc::Rc::strong_count(&f));
println!("std::rc::Rc::strong_count(&g) = {}", std::rc::Rc::strong_count(&g));
println!("std::rc::Rc::strong_count(&h) = {}", std::rc::Rc::strong_count(&h));
}
fn main() {
move_examples();
move_control();
move_indexing();
move_except();
move_Rc();
}
t = String0, s = String1
f0: s = String
v.pop(); v = ["abc", "abc"], t = Some("abc")
std::mem::replace(&mut v[1], "def".to_string(); v = ["abc", "def"], t = abc
i = 0, j = 0, k = 0
f = 0x27f741194c0, g = 0x27f741194c0, h = 0x27f741194c0
std::rc::Rc::strong_count(&f) = 3
std::rc::Rc::strong_count(&g) = 3
std::rc::Rc::strong_count(&h) = 3
今回はここまでー!
Rustの目玉商品である所有権と移動について学習しました!
なぜそうなったのかを学ぶとRustだけではなく、ほかの言語の事情なども一緒に学べるのでRustの学習はやってて楽しいですよね!