TL; DR
Rustの用語集【備忘録】でWIPにしていた下記を調べた備忘録です。
- 所有権
- 借用
- ライフタイム
自分用のメモを流用しただけなので、わかりにくい部分あると思いますがご了承ください。
共通する概念
Rustは思想として安全性とスピードを最重要視しており、それらをゼロコスト抽象化を用いて実現可能にしている。具体的にはコンパイル時に型ごとに関数をあらかじめ解決するよう静的ディスパッチを使用するため、実際の実行時にはオーバーヘッドなく関数を呼び出すことができる。
※動的ディスパッチができないわけではない。
use std::fmt::Debug;
fn main() {
fn static_dispatch<T: Debug>(x: T) {
println! {"{:?}", x};
}
static_dispatch(2020);
static_dispatch("thursday");
#[derive(Debug)]
struct Point {
x: i32, // xは32ビット符号あり数値として指定
y: String, // yは文字列として指定
}
let points = Point { x: 2020, y: "thursday".to_string() };
static_dispatch(points);
}
========================result========================
$ ./main
2020
"thursday"
Point { x: 2020, y: "thursday" }
所有権
変数束縛において、Rustは所有権という特性を持つ。
fn ownership() {
let origin = vec![1, 2, 3];
}
上記のような変数origin
があった場合、新しくVec<T>
が作られる。変数origin
がスコープに入った場合各要素のためにヒープにメモリが割り当てられる。しかし、このスコープを抜けるとRustは割り当てたリソース自動的に解放してくれる。その際注意しなければいけないのは別のリソースから参照されている場合、参照元が先にスコープを抜けてしまうと参照することができなくなってしまう。必ず有効期限が参照元 > 参照
が成り立つように設計しなければ、意図せぬ動作が発生してしまう。
ムーブセマンティクス
変数束縛はリソースに対して常に対になることを保証する。
fn ownership() {
let origin = vec![1, 2, 3];
let duplicate1 = origin;
println!("origin[0] first index is {}", origin[0]);
}
======================result======================
$ rustc main.rs
error[E0382]: borrow of moved value: `origin`
--> main.rs:5:45
|
2 | let origin = vec![1, 2, 3];
| ------ move occurs because `origin` has type `std::vec::Vec<i32>`, which does not implement the `Copy` trait
3 | let duplicate1 = origin;
| ------ value moved here
4 |
5 | println!("origin[0] first index is {}", origin[0]);
| ^^^^^^ value borrowed here after move
error: aborting due to previous error
この場合は既にduplicate1
のリソースとしてorigin
が束縛されているため、後続のprintln
メソッドにリソースとして割り当てることができないことがわかる。
Rustのデフォルトの動作として、ベクタを別の変数に束縛した場合そのデータへのポインタをコピーするが、ポインタが2つあることによるデータの競合を防ぐためにコピー元(上の例だとorigin
)へのポインタの使用を制限する。
Copy型
指定した型のトレイトにCopyがある場合は上記に当てはまらず、ポインタのみでなくデータごとコピーするため元の束縛された変数で呼び出すことが可能。
下記の例の場合、boolean型はcopyトレイトを持っているためprintln!("{}", origin);
で呼び出すことを可能にしている。
fn main() {
let origin = false;
let duplicate1 = full_copy(origin);
println!("{}", origin);
}
fn full_copy(x: bool) -> bool {
!x
}
その他所有権のパターン
// スコープ外での書き込み
fn out_of_scope() {
let mut origin = vec![1, 2, 3];
let mut duplicate1 = &mut v1;
{
let duplicate2 = &duplicate1;
}
*duplicate1 = vec![4, 5, 6];
}
// 複数階層の参照
fn multi_ref() {
let origin = vec![1, 2, 3];
let duplicate1 = &origin;
let duplicate2 = &duplicate1;
println!("Hello, {}, {}, {} !", origin[0], duplicate1[1], duplicate2[2]);
}
// 複数の参照先
fn mut_single() {
let mut origin = vec![1, 2, 3];
let mut duplicate1 = &mut origin;
let mut duplicate2 = &mut origin;
}
// 既に参照先がある場合(同上)
fn impossible_refs() {
let mut origin = vec![1, 2, 3];
let mut duplicate1 = &mut origin;
let duplicate2 = &origin;
}
// ミュータブルで参照しているベクタへの書き込み
fn mut_readonly() {
let mut origin = vec![1, 2, 3];
let mut duplicate1 = &mut v1;
let duplicate2 = &duplicate1;
*duplicate1 = vec![4, 5, 6];
}
借用
&T
型で所有権の借用が可能になる。
借用された変数&T
の束縛はスコープから外れてもリソース解放されないため、後続で値を変更することができない。
fn borrowed_func(v: &Vec<i32>) {
v.push(5);
}
let v = vec![]; // このベクタへの変更は無効になる
borrowed_func(&v); // エラーが吐かれる
借用を利用する際は、以下の借用についてのルール4点を意識する必要がある。
- 借用は全て所有者のスコープより長く存続することはできない。
- 上述した
&T
と後述する&mut
については同時に持つことはできない。 -
&T
は書き込みが発生しない場合は何回でも利用できる。 -
&mut
は同時に複数持つことはできない。
&mut参照
変数を指定する際にmut
を置くことでミュータブルな変数束縛であることを宣言できたが、参照する場合も同様に&mut
と宣言しなければミュータブルなものとして変数を束縛することができない。またミュータブルに参照している変数を利用する場合は*T
として記述しなければミュータブルな変数束縛として扱うことができない。
// OKパターン
fn main() {
let mut x = 5;
{
let y = &mut x;
*y += 1;
}
println!("{}", x); // 6が返ってくる
}
// NGパターン
fn main() {
let mut x = 5;
{
let mut y = x;
y += 1;
}
println!("{}", x); // 5が返ってくる
}
スコープの考え方
スコープの範囲は{}
でくくられた範囲に限定される。言い換えればその中では変数束縛が継続されリソースが解放されることがないということ。
&T
もしくは&mut
の一方しか持たない場合は問題ないが、両方持つ場合はスコープを意識する必要がある。
fn main() {
let mut x = 5;
{ // &mutとしてxを借用開始
let y = &mut x;
*y += 1;
} // &mutとしてのxを借用終了
println!("{}", x); // xを借用
}
ライフタイム
参照における有効期限(ライフタイム)は、すべての参照に対し発生するがコンパイラがよしなにしてくれるため省略することが可能。
反対に明示的にライフタイムは'a
を用いて記述することも可能であり、指定することができる。
下記の例は記述は異なるが意味は同じなる。(ライフタイムの省略をしているか否かの違い)
fn foo(x: &i32) {}
= fn bar<'a>(x: &'a i32) {}
上記の関数のあとの<>
はジェネリックパラメータを意味し、ライフタイムはその一種である。
static
'static
がついたライフタイムには2通りあり、いずれも特別な扱いがされる。
文字列リテラルは&'static str
型をもち、常に参照が有効になる。
もう一つはグローバルに参照される変数(≒ 定数)に利用され、その場合型を明示する必要がある。
// 文字列
let x: &'static str = "Hello, world.";
// グローバル
static FOO: i32 = 5;
let x: &'static i32 = &FOO;
ユースケース的なやつ
struct の中
struct Hoge<'a> { // structに対してライフタイムを明示する
x: &'a i32, // 変数xがそのライフタイムを利用するように明示する
}
fn main() {
let y = &2;
let f = hoge { x: y }; // structを参照する際はi32型として読みとる
println!("{}", f.x);
}
impl ブロック
struct Hoge<'a> {
x: &'a i32,
}
impl<'a> Hoge<'a> { // メソッドに対するライフタイムを明示 & Hogeがそのライフタイムを利用する
fn x(&self) -> &'a i32 { self.x }
}
fn main() {
let y = &2;
let f = Hoge { x: y };
println!("x is: {}", f.x());
}
複数のライフタイム
fn x_or_y<'a>(x: &'a str, y: &'a str) -> &'a str { // xもyもライフタイムaを使うことができる
}
fn x_or_y<'a, 'b>(x: &'a str, y: &'b str) -> &'a str { // xはライフタイムaでyはライフタイムbと使い分けることができる
}