記事について
次のTour of Rustを用いて学んだ内容のメモになります。
所有権
型のインスタンスを生成して、変数に束縛するとメモリリソースが作成され、そのすべてのライフタイムに渡ってRustコンパイラが検証する。
束縛された変数はリソースの所有者と呼ばれる。
struct Foo {
x: i32,
}
fn main() {
// 構造体をインスタンス化し、変数に束縛してメモリリソースを作成
let foo = Foo { x: 42 };
// foo は所有者
}
スコープベースのリソース管理
Rustでは、スコープの終わりをリソースのデストラクトと解放の場として使用する。
このデストラクトと解放のことをドロップと呼ぶ。
struct Foo {
x: i32,
}
fn main() {
let foo_a = Foo { x: 42 };
let foo_b = Foo { x: 13 };
println!("{}", foo_a.x);
println!("{}", foo_b.x);
// foo_b はここでドロップ
// foo_a はここでドロップ
}
メモリについて
- Rustにはガベージコレクションがない。
- C++ではResourse Acquisition Is Initialization(RAII)「リソース取得は初期化である」とも呼ばれる。
ドロップは階層的
構造体がドロップされると、まず構造体自体がドロップされ、その次に子の要素が個別に削除される。
struct Bar {
x: i32,
}
struct Foo {
bar: Bar,
}
fn main() {
let foo = Foo { bar: Bar { x: 42 } };
println!("{}", foo.bar.x);
// foo が最初にドロップ
// 次に foo.bar がドロップ
}
メモリについて
- メモリを自動的に解放することで、メモリリークを軽減する。
- メモリリソースのドロップは一度しかできない。
所有権の移動
所有者が関数の実引数として渡されると、所有権は関数の仮引数に移動(move)する。
移動後は、元の関数内の変数は使用できなくなる。
struct Foo {
x: i32,
}
fn do_something(f: Foo) {
println!("{}", f.x);
// f はここでドロップ
}
fn main() {
let foo = Foo { x: 42 };
// foo の所有権は do_something に移動
do_something(foo);
// foo は使えなくなる
}
メモリについて
移動している間、所有者の値のスタックメモリは、関数呼び出しパラメータのスタックメモリにコピーされる。
所有権を返す
所有権を関数から返すこともできる。
struct Foo {
x: i32,
}
fn do_something() -> Foo {
Foo { x: 42 }
// 所有権は外に移動
}
fn main() {
let foo = do_something();
// foo は所有者になる
// 関数のスコープの終端により、foo はドロップ
}
参照による所有権の借用
参照は、&演算子を使ってリソースへのアクセスを借用できるようにしてくれます。
参照も他のリソースと同様にドロップされる。
struct Foo {
x: i32,
}
fn main() {
let foo = Foo { x: 42 };
let f = &foo;
println!("{}", f.x);
// f はここでドロップ
// foo はここでドロップ
}
参照による所有権の可変な借用
&mut演算子を使うと、リソースへの変更可能なアクセスを借用することができる。
リソースの所有者は、可変な借用の間は移動や変更ができない。
struct Foo {
x: i32,
}
fn do_something(f: Foo) {
println!("{}", f.x);
// f はここでドロップ
}
fn main() {
let mut foo = Foo { x: 42 };
let f = &mut foo;
// 失敗: do_something(foo) はここでエラー
// foo は可変に借用されており移動できないため
// 失敗: foo.x = 13; はここでエラー
// foo は可変に借用されている間は変更できないため
f.x = 13;
// f はここから先では使用されないため、ここでドロップ
println!("{}", foo.x);
// 可変な借用はドロップされているため変更可能
foo.x = 7;
// foo の所有権を関数に移動
do_something(foo);
}
メモリについて
- データ競合を防止するため、Rustでは同時に2つの変数から値を変更することはできない。
参照外し
&mutによる参照は、*演算子によって参照を外す(dereference)することで、所有者の値を
設定できる。
*演算子によって所有者の値のコピーを取得することもできる。
fn main() {
let mut foo = 42;
let f = &mut foo;
let bar = *f; // 所有者の値を取得
*f = 13; // 参照の所有者の値を設定
println!("{}", bar);
println!("{}", foo);
}
借用したデータの受け渡し
Rustの参照には次のルールがある。
- 可変な参照が1つだけか、不変な参照が複数かのどちらかが許可される。両方を同時に使用できない。
- 参照は所有者よりも長く存在してはならない。
これは関数への参照を渡す際に問題となることはない。
struct Foo {
x: i32,
}
fn do_something(f: &mut Foo) {
f.x += 1;
// f への可変な参照はここでドロップ
}
fn main() {
let mut foo = Foo { x: 42 };
do_something(&mut foo);
// 関数 do_something で可変な参照はドロップされるため、
// 別の参照を作ることが可能
do_something(&mut foo);
// foo はここでドロップ
}
メモリについて
- 参照の最初のルールはデータ競合を防ぐものである。
- 参照の2番目のルールは、存在しないデータへの参照(C言語ではダングリングポインタと呼ばれる)による誤動作を防ぐものである。
参照の参照
参照の一部を参照することができる。
次の例では、構造体の参照から構造体内の変数の参照扱っている。
struct Foo {
x: i32,
}
fn do_something(a: &Foo) -> &i32 {
return &a.x;
}
fn main() {
let mut foo = Foo { x: 42 };
let x = &mut foo.x;
*x = 13;
// x はここでドロップされるため、不変な参照が作成可能
let y = do_something(&foo);
println!("{}", y);
// y はここでドロップ
// foo はここでドロップ
}
明示的なライフタイム
ライフタイムとは、その参照が有効となるスコープのことである。
Rustでは、常にコードに表されるわけではないが、コンパイラはすべての変数のライフタイムを管理しており、その参照がその所有者よりも長く存在しないことを検証する。
関数は、どの引数と戻り値とがライフタイムを共有しているかを、識別のための指定子で明示的に指定できる。
ライフタイム指定子は常に'で始まる。(例:'a,'b,'b)
struct Foo {
x: i32,
}
// 引数 foo と戻り値はライフタイムを共有
fn do_something<'a>(foo: &'a Foo) -> &'a i32 {
return &foo.x;
}
fn main() {
let mut foo = Foo { x: 42 };
let x = &mut foo.x;
*x = 13;
// x はここでドロップされるため、不変な参照が作成可能
let y = do_something(&foo);
println!("{}", y);
// y はここでドロップ
// foo はここでドロップ
}
複数のライフタイム
ライフタイム指定子は、関数の引数や戻り値のライフライムをコンパイラが解決できない場合に、明示的に指定することができる。
struct Foo {
x: i32,
}
// foo_b と戻り値はライフタイムを共有
// foo_a のライフタイムは別
fn do_something<'a, 'b>(foo_a: &'a Foo, foo_b: &'b Foo) -> &'b i32 {
println!("{}", foo_a.x);
println!("{}", foo_b.x);
return &foo_b.x;
}
fn main() {
let foo_a = Foo { x: 42 };
let foo_b = Foo { x: 12 };
let x = do_something(&foo_a, &foo_b);
// ここから先は foo_b のライフタイムしか存在しないため、
// foo_a はここでドロップ
println!("{}", x);
// x はここでドロップ
// foo_b はここでドロップ
}
スタティックライフタイム
スタティック変数は、コンパイル時に生成され、プログラムの開始から終了まで存在するメモリリソースである。
これらの変数の型は明示的に指定しなくてはならない。
スタティックライフタイムは、プログラムの終了まで無期限に持続するメモリリソースである。この定義では、スタティックライフタイムを持つリソースは実行時にも作成できることに注意する必要がある。
スタティックライフタイムを持つリソースには、特別なライフタイム指定子'staticがある。'staticリソースは決してドロップしない。
スタティックライフタイムを持つリソースが参照を含む場合、それらはすべて'staticでなくてはならない。(そうでなければ、参照はプログラム終了前にドロップしてしまう可能性があるから)
static PI: f64 = 3.1415;
fn main() {
// スタティック変数は関数スコープでも定義可能
static mut SECRET: &'static str = "swordfish";
// 文字列リテラルは 'static ライフタイム
let msg: &'static str = "Hello World!";
let p: &'static f64 = &PI;
println!("{} {}", msg, p);
// ルールを破ることはできますが、それを明示する必要があります。
unsafe {
// 文字列リテラルは 'static なので SECRET に代入可能
SECRET = "abracadabra";
println!("{}", SECRET);
}
}
メモリついて
- スタティック変数は誰でもグローバルにアクセスして読み取るため、変更することは危険である。
- コンパイラがメモリを保証できない操作を実行するには
unsafeブロックを使用する。(例では、スタティック変数を書き換えている)
データ型のライフタイム
関数と同様に、データ型はメンバのライフタイムを指定できる。
Rustは、参照を含む構造体が、その参照を指す所有者よりも長く存在しないことを検証する。
構造体には、なにもないところを指している参照を含めることはできない。
struct Foo<'a> {
i:&'a i32
}
fn main() {
let x = 42;
let foo = Foo {
i: &x
};
println!("{}",foo.i);
}
まとめ
システムプログラミングに共通する次のような課題を解決するため、Rustでは所有権・ライフタイムが厳密に管理されている。
- リソースの意図しない変更
- リソースの解放漏れ
- リソースを誤って複数回解放
- データ競合(同リソースに対して同時に行われる読み書き)
- コンパイラが保証できない部分の明確化