はじめに
The Rust Programming Languageを頭から読み進めてみる。読みながら要点をまとめていった記事です。
4章:所有権を理解する
4.1章:所有権とは?
スタックとヒープ
- スタック
- 後入れ先出しのため、データの場所を考える必要がない
- 高速だが、サイズは既知である必要がある
- ヒープ
- 配置場所に規則はなく、空いている場所を探す
- スタックより低速だが、サイズは可変で良い
所有権規則
- Rustにおける所有権とは、メモリを安全に管理するための仕組み
- Rustの値は、保有者と呼ばれる変数と対応
- いかなる時も所有者は1つ
- 所有者がスコープから外れたら値は破棄
-
drop
関数が呼ばれ、メモリが解放
-
所有権のムーブ
fn main() {
let s1 = String::from("hello");
let s2 = s1;
println!("The Value s2 is: {}", s2);
// Error
// println!("The Value s1 is: {}", s1);
}
The Value s2 is: hello
-
s1
を定義すると、文字列の中身を保持するメモリへのポインタ、長さ、および許容量がスタックに保持される。そして、ポインタの先にある"hello"は、ヒープ上のメモリに保持される。 -
s2 = s1
で代入されると、データがコピーされるが、ポインタが指すヒープ上のデータはコピーしない -
s1
,s2
両方が生存していると、2回メモリ解放を繰り返すことになるので、s2 = s1
の時点で所有権がムーブされる(以下画像の状態) - 所有権のムーブ後は
s2
だけが有効のため、スコープを抜けるとs2
だがメモリが開放される - Rustはデータのdeep copyは設計上無いとのこと
The Rust Programming Language 日本語版 図4-3から引用
クローン
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
}
s1 = hello, s2 = hello
- ヒープデータのコピーには
clone
を使用 - コピーされた
s2
は、s1
の所有権を持たないためprintln!
可能
スタックで保持されるデータ
fn main() {
let x = 5;
let y = x;
println!("The Value y is: {}", y);
// NOT error
println!("The Value x is: {}", x);
}
The Value y is: 5
The Value x is: 5
-
String
における例と矛盾しているように見えるが、整数のような既知のサイズを持つ型はスタック上に保持される - なので、
y = x
のようにコピーするときは所有権の移動が発生しない - 値がコピーが起こることをコピーセマンティクスと呼び、これが起きる型は
Copy
トレイトが実装されている。u32
やbool
,f64
,char
などが対象(Rustドキュメント)。
所有権と関数
fn main() {
let s = String::from("hello"); // sがスコープに入る
takes_ownership(s); // sが関数にムーブ
// この時点でsは有効ではない
// println!("The value of s is: {}", s);
let x = 5; // xがスコープに入る
makes_copy(x); // xが関数にムーブされるがi32はCopy
println!("The value of x is: {}", x);
} // xもsもスコープを抜ける。ただしsは既にムーブ済み
fn takes_ownership(some_string: String) {
println!("{}", some_string);
} // some_stringがdrop
fn makes_copy(some_integer: i32) {
println!("{}", some_integer);
} // some_integerがdrop
hello
5
The value of x is: 5
- 関数に変数を渡すと、ムーブやコピーが発生する
- ムーブされる
String
は、その後に再使用することはできず、Copy
されるi32
は、再使用が可能
戻り値とスコープ
fn main() {
let s1 = gives_ownership();
println!("{}", s1); // hello
let s2 = String::from("world"); // s2がスコープに入る
println!("{}", s2); // world
let s3 = takes_and_gives_back(s2); // s2は関数に所有権がムーブし、戻り値がs3にムーブ
// println!("{}", s2); // s2はムーブしたためNG
println!("{}", s3); // world
} // s1, s3がdrop, s2もスコープ抜けるがムーブ済み
fn gives_ownership() -> String {
let some_string = String::from("hello"); // some_stringがスコープに入る
some_string // 呼び出し元関数にムーブされる
}
fn takes_and_gives_back(a_string: String) -> String {
a_string // 呼び出し元関数にムーブされる
}
hello
world
world
- 全ての関数で所有権を取られ、それをまた返さないといけないのは大変
- 作業量が多いため、参照という仕組みが存在
4-2章:参照と借用
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()
}
The length of 'hello' is 5.
-
&
(参照)を渡すことで、関数に所有権を移すこと無く参照できている -
&s1
で参照する参照を生成しているが、所有している訳ではないのでdrop
されない -
s.len()
も同様に、s
自体はスコープを外れるものの、所有権を持っている訳では無いので何も起こらない。なお、引数に参照を取ることを借用と呼ぶ
借りたら返す
fn main() {
let s = String::from("hello");
change(&s);
println!("{}", s);
}
fn change(some_string: &String) {
some_string.push_str(", world");
}
error[E0596]: cannot borrow immutable borrowed content `*some_string` as mutable
(エラー: 不変な借用をした中身`*some_string`を可変で借用できません)
--> error.rs:8:5
|
7 | fn change(some_string: &String) {
| ------- use `&mut String` here to make mutable
8 | some_string.push_str(", world");
| ^^^^^^^^^^^ cannot borrow as mutable
- 借用した文字列を変更するとどうなるか?エラーが発生する
- 要するに、借りたものは綺麗に返しましょう
可変な参照
fn main() {
let mut s = String::from("hello");
change(&mut s);
println!("{}", s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
hello, world
- 借用を変更するとエラーが発生したものが修正された
- 違いは可変な参照である
&mut s
を生成し、&mut String
で受けること
同時にはできない参照
fn main() {
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{}, {}", r1, r2);
}
$ cargo run
Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0499]: cannot borrow `s` as mutable more than once at a time
(エラー: 一度に`s`を可変として2回以上借用することはできません)
- ある特定のデータに対しては、1つしか可変な参照は持てない
- コンパイラでデータの競合を防いでくれている
可変と不変の参照の組み合わせ
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
let r3 = &mut s;
}
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as
immutable
(エラー: `s`は不変で借用されているので、可変で借用できません)
- なぜか自身の環境ではエラーが発生しなかったが、不変な参照をしている間は、可変な参照 (
&mut s
) はできない - 不変な参照している間に、値が変わることを防ぐため
ダングリングポインタ
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s // sと書くべき
}
error[E0106]: missing lifetime specifier
(エラー: ライフタイム指定子がありません)
- ダンリングポインタは、ポインタが不正なメモリを指している状態
-
dangle()
内で宣言したs
は、スコープが終わるところで解放されるが、&s
で参照を返そうとしていたため、コンパイラが止めてくれる
4.3章:スライス型
愚直に文字列の長さを数える
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s);
println!("s: {}", s);
println!("word: {}", word);
s.clear();
println!("s: {}", s);
println!("word: {}", word);
}
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
println!("bytes: {:?}", bytes);
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
bytes: [104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]
s: hello world
word: 5
s:
word: 5
-
iter
はコレクションの各要素を返し、enumerate
はiter
の結果をそのまま返して、タプルの一部として各要素を返す -
i
は添字 (1, 2, 3, ...)、&item
は各要素 (104, 101, 108, ...) -
s.clear()
すると、関連付けられていないword
は値は5のまま維持される
文字列スライス
fn main() {
let s = String::from("hello world");
println!("{}", &s[0..5]);
println!("{}", &s[6..11]);
println!("{}", &s[..]);
println!("{}", &s[2..]);
println!("{}", &s[0..s.len()]);
}
hello
world
hello world
llo world
hello world
- 形式は
[starting_index..ending_index]
- 末尾の1字を取得したい場合は、
&s[s.len()-1..s.len()]
でも通るが、&s.chars().last().unwrap()
と書けるよう
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s);
println!("s: {}", &s);
println!("word: {}", word);
// Error
// s.clear();
}
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();
println!("bytes: {:?}", bytes);
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
bytes: [104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]
s: hello world
word: hello
- 文字列スライスを使って、指定した部分まで文字列を出力
- ただし、
s.clear()
を実行することはできない- ルール1:不変な参照は幾つあってもOK
- ルール2:可変な参照は1つのみで、他の参照は許可されない
-
word
がs
不変借用している中で、clear()
を使った可変借用はできない
引数としての文字列スライス
fn main() {
// 1. String
let my_string = String::from("hello world");
let word = first_word(&my_string[..]);
println!("word: {}", word);
// 2. &str - スライス記法で指定
let my_string_literal = "hello world";
let word = first_word(&my_string_literal[..]);
println!("word: {}", word);
// 3. &str - スライス記法なし
let word = first_word(my_string_literal);
println!("word: {}", word);
}
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
println!("bytes: {:?}", bytes);
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
bytes: [104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]
word: hello
bytes: [104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]
word: hello
bytes: [104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100]
word: hello
- 復習として、
String
は変更可能で実態はVec<u8>
。一方、&str
は文字列スライスと呼び、変更不可で実態は&[u8]
。文字列リテラルで定義したもの(コメントの2番)は&strとなる - 文字列リテラルはコメントの1番のように、全要素を指定して参照を渡すことができるが、3番のように、スライス記法なしでも渡すことが可能