Rustにおける所有権、ポインタ、参照のまとめ (演習問題付き)
はじめに
Rustにおける所有権、ポインタ、参照についてまとめました。
併せて演習問題も用意していますので、よければRustの学習に活用してみてください。
問題が提供されている quizlet に登録すると単語(フラッシュ)カードだけでなく、問題カテゴリで学習を選択することで選択式の問題で学習することも可能です。
尚、本学習内容はプログラム言語の基本的なところは把握している前提となります。
所有権
所有権とは、メモリの安全な管理を保証するRustの重要な特徴の一つで、どの変数がメモリの特定の領域を「所有」しているかを示す。
Rustでは所有者は必ず一つと定められている。但し例外もあり、一部所有権の共有が認められている。
ここの所有とは例えば、Box.newでヒープ上に確保されたメモリへのポインタをvが所有しているという。
let v = Box.new(("hoge", 1))
変数は値(へのポインタ)を所有するが、同様に構造体はそのフィールドを、配列やタプルはその要素を所有する。
Rustにおける所有権(ownership)の概念は、スタック型変数でもヒープ型変数でも同様に適用される。スタックに格納される変数(例えば、整数や固定長の配列など)は、その変数自体がそのデータの所有者となる。
移動
Rustでは変数の代入、変数を関数の引数へ受け渡し、関数からの値の返却の際、所有権が移動される。但し、移動されるのは値そのものでヒープ上のアドレスなどは変更されない。
所有権の移動
所有権は一つの変数から別の変数に「移動」することができる。
fn main() {
let s1 = String::from("こんにちは"); // s1が所有権を持っている
let s2 = s1; // 所有権がs2に移動する
// println!("{}, world!", s1); // これはエラーになる。s1はもう有効ではない
println!("{}, world!", s2); // 正常に動作する
}
この例では、s1からs2へ所有権が移動している。その後、s1は使用できなくなる。
所有権と関数
関数に値を渡すと、その値の所有権も移動する。関数から値が返されると、所有権も戻る。
fn takes_ownership(some_string: String) { // some_stringが所有権を得る
println!("{}", some_string);
} // ここでsome_stringがスコープを抜け、`drop`が呼ばれる
fn main() {
let s = String::from("こんにちは");
takes_ownership(s); // sの所有権が関数に移動する
// println!("{}", s); // これはエラーになる。sはもう有効ではない
}
この例では、sの所有権がtakes_ownership関数に移動し、関数が終了すると所有権とともに値も破棄される。
所有権とクローン
データのクローンを作成することで、所有権を保持しつつデータのコピーを別の変数に渡すことができる。
fn main() {
let s1 = String::from("こんにちは");
let s2 = s1.clone(); // s1のデータの完全なコピーを作成し、s2に所有権を与える
println!("s1 = {}, s2 = {}", s1, s2); // s1とs2は両方とも有効
}
この例では、s1のデータをcloneメソッドを使ってコピーし、そのコピーをs2に渡している。この場合、s1とs2は独立しており、どちらも使用できる。
Copyトレイト
Copyトレイトを実装した型の場合は所有権の移動はされない。
基本データ型: 整数型(i32, u64など)、浮動小数点型(f32, f64)、ブール型(bool)、文字型(char)などの基本的なデータ型はCopyトレイトを実装している。
このような型の場合、変数の代入を行っても、元の変数はそのまま使用できる。
CopyトレイトはDropトレイトを実装している型には実装できない。
DropトレイトはスマートポインタのBox<T>
型やMutex<T>
、RwLock<T>
などこれらの同期プリミティブを利用する型に実装されている。
またCopyトレイトはシャローコピー(表面的なコピー)を提供する為、Vec<T>
やString
のようなヒープメモリを使用する型には適用できない。このような型の場合はCloneトレイトを実装する形になる。
基本的なスカラー型:Copyトレイト例
fn main() {
let x = 5; // i32はCopyトレイトを実装している
let y = x; // xの値がyにコピーされる
// 両方の値が使用できる
println!("x: {}", x); // 5
println!("y: {}", y); // 5
}
この例では、xの値がyにコピーされるが、xは引き続き使用可能。
関数引数としてのCopy型:Copyトレイト例
fn print_twice(x: i32) {
println!("x: {}", x);
println!("x: {}", x);
}
fn main() {
let a = 10;
print_twice(a); // aの値が関数にコピーされる
// aは引き続き使用可能
println!("a: {}", a);
}
この例では、aの値がprint_twice関数にコピーされるが、aはその後もメイン関数で使用できる。
Copyトレイトの実装
自分で定義した型にCopyトレイトを実装する場合は、以下のようにする。
#[derive(Debug, Copy, Clone)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = p1; // p1の値がp2にコピーされる
// p1も引き続き使用可能
println!("p1: {:?}", p1);
println!("p2: {:?}", p2);
}
この例では、Point構造体にCopyトレイトを実装している。p1をp2にコピーした後でも、p1は引き続き使用できる。
所有権の共有
Rc<T>
とArc<T>
は、Rustの標準ライブラリに含まれるスマートポインタ型で、参照カウンティングを通じて共有所有権を実現する。これらは、複数の所有者によるデータへのアクセスを許可しつつ、そのデータのライフタイムを管理する。
変数代入された場合はスマートポインタ型でも所有権の移動がされる。参照カウントがインクリメントされるのは、スマートポインタ型変数でcloneがされた場合に参照カウントが増加する。
Rc<T>
(Reference Counted)
- 参照カウント
-
Rc<T>
は、内部的に参照カウントを持っており、Rc<T>
の新しいインスタンスが作成されるたび(例えば、Rc::clone
を使って)参照カウントが増加する。Rc<T>
のインスタンスがスコープを抜けると参照カウントが減少し、カウントが0になった時点で、管理されているデータは自動的にクリーンアップ(メモリ解放など)される。
-
- スレッドアンセーフ
-
Rc<T>
はスレッドセーフではないため、複数のスレッド間で共有することはできない。
-
use std::rc::Rc;
fn main() {
// Rcで包んだ値を作成
let five = Rc::new(5);
// fiveのクローンを作成し、複数の変数で共有
let shared_five = Rc::clone(&five);
let another_shared_five = Rc::clone(&five);
// 以下の行はコンパイルエラーになります。
// *five = 6;
// *shared_five = 7;
// 参照カウントと値を表示
println!("Count after creating shared_fives = {}", Rc::strong_count(&five));
println!("Value of shared_five: {}", shared_five);
println!("Value of another_shared_five: {}", another_shared_five);
// 最初の変数をドロップ
drop(shared_five);
println!("Count after dropping shared_five = {}", Rc::strong_count(&five));
}
このRustコードでは、Rc<T>
を使って変数five(整数5)の所有権を共有し、そのクローンを複数の変数で共有している。Rc<T>
を通じての値の変更はコンパイルエラーになることが示されている。
Arc<T>
(Atomic Reference Counted)
- スレッドセーフ
-
Arc<T>
の動作はRc<T>
に似ているが、参照カウントの操作がアトミック操作で行われるため、マルチスレッド環境でも安全に動作する。
-
use std::sync::Arc;
use std::thread;
fn main() {
let data = Arc::new(5);
let thread1 = thread::spawn(move || {
let data_clone = Arc::clone(&data);
println!("Value in thread 1: {}", data_clone);
// 以下の行はコンパイルエラーになります
// *data_clone = 10;
});
thread1.join().unwrap();
}
このRustコードは、スレッド間で変数data(整数5)の所有権をArc<T>
で共有し、変更を試みるもコンパイルエラーとなることを示している。
ポインタ型
Rustでは、ポインタはメモリ上の特定の位置を指すために使用される。Rustには複数のポインタ型があり、それぞれ異なる用途と安全性のレベルを持っている。
- 参照(
&T
と&mut T
) - スマートポインタ
- rawポインタ
- インターフェース型のポインタ
参照
Rustにおける参照は変数の値ではなく、その変数のメモリアドレスを指す。Cのポインタと同様に参照がスコープから消えても、自動的に何らかの資源を解放することはない。データの所有権を持つ変数がスコープを抜ける時にのみ、そのデータは自動的に解放(ドロップ)される。Cのポインタと違って、Rustの参照は決してnullにならない。
Rustの参照には次の2種類、不変の参照(&T)と可変の参照(&mut T)がある。
-
&T
- 変更不能な共有参照。CのConst T*と同じ。
- 式
&x
はxへの参照を作るが、Rustの用語では、これを「xへの参照を借用する」という。
-
&mut T
- 排他的な可変参照。参照先の値を読み出し、変更することができる。CのT*に相当。
- ある値に対してこの種の参照が存在する間は、その値に対する他の参照は共有参照であれ可変参照であれ作ることはできない。つまり、この参照を通してしか、その値にアクセスすることはできない
式*r
で参照外しを行い、参照rが指す値を取得する。
(これはC++の&演算子と*演算子に似ている)
&T
:参照外しの例
fn main() {
let x = 10;
let r = &x; // xの不変の参照
println!("r の値: {}", r); // これは参照を表示します(例えば "0x7ffeeefbff48")
println!("*r の値: {}", *r); // これは参照が指す値を表示します("10")
}
&mut T
:可変参照と参照外し
fn main() {
let mut x = 10;
let r = &mut x; // xの可変の参照
*r += 1; // 参照外しを使用して値を変更
println!("x の値: {}", x); // 11
}
借用
一方Rustには借用という概念もある。借用は参照を通して、変数の所有権を一時的に借りることを指す。
借用は次の2つのルールを強制する。
- 不変の借用(&T): 不変の借用を通じて、データを読み取ることができるが、変更することはできない。一つのデータに対しては複数の不変の借用が可能だが、その間、可変の借用はできない。
- 但し例外もあり、
Cell<T>
は共有参照を通した変更を許す。
- 但し例外もあり、
- 可変の借用(&mut T): 可変の借用を通じて、データを変更することができる。可変の借用が存在する間、そのデータに対する他のいかなる借用も不可能。
fn main() {
let mut x = 5; // 可変変数を定義
// xの不変借用を2回行う
let z1 = &x; // xを不変借用
let z2 = &x; // xを再度不変借用
println!("x is: {}", z1); // 借用された参照を通じて値を読む
println!("x is also: {}", z2); // 他の不変借用を通じて値を読む
// 以下の行はコンパイルエラーになる
// 不変借用されている間は可変借用できない
// let y = &mut x; // xを可変借用しようとする
// 不変借用のスコープを抜ける
drop(z1);
drop(z2);
// 不変借用が終了した後、可変借用が可能
let y = &mut x; // xを可変借用
*y += 1; // 借用された参照を通じて値を変更
// 以下の行はコンパイルエラーになる
// 可変借用されている間は不変借用できない
// let z1 = &x; // xを不変借用
println!("x is now: {}", y);
}
また以下の借用規則もある。
- 有効なスコープ内でのみ借用可能: 借用されたデータは、元のデータ(所有者)がスコープを抜けて無効にならない限り有効。つまり、借用は所有者より長く存在することはできない。
スマートポインタ
スマートポインタには主に以下の種類がある、
-
Box<T>
: ヒープ上のデータを指す。所有権を持ち、スコープが終了すると自動的にデータが解放される。 -
Rc<T>
: 参照カウンティングによる共有所有権を実現。シングルスレッド環境向け。 -
Arc<T>
: アトミック参照カウンティングによる共有所有権を実現。マルチスレッド環境向け。 -
Cell<T>
: 実行時に借用規則をチェックする。内部可変性を提供。 -
RefCell<T>
: 実行時に借用規則をチェックする。内部可変性を提供。 -
Mutex<T>
: データへの排他的アクセスを提供し、スレッドセーフな変更を可能にする。 -
Arc<Mutex<T>>
:Arc<Mutex<T>>
の組み合わせを使うことで、複数のスレッドからデータへの安全な変更アクセスを許可する。
Rc<T>
とArc<T>
についてはすでに説明済みの為、説明を省略する。
Box
Box::newでヒープ上に値を確保することができる。
// シンプルな構造体を定義
struct MyStruct {
value: i32,
}
fn main() {
// Box<T>を使用してヒープ上にMyStructを確保(可変)
let mut my_box = Box::new(MyStruct { value: 5 });
// my_boxが指すデータの値を表示
println!("Value in the box: {}", my_box.value);
// my_boxが指すデータを変更
my_box.value = 10;
// 変更後の値を表示
println!("New value in the box: {}", my_box.value);
// Box<T>を使用してヒープ上にMyStructを確保(不変)
let my_box_immutable = Box::new(MyStruct { value: 20 });
// 以下の行はコンパイルエラーになる
// my_box_immutable.value = 30;
// 値を表示
println!("Value in the immutable box: {}", my_box_immutable.value);
}
この例は、Box<T>
が所有権を持つヒープ上のデータに対して、可変性に応じて変更を行うかどうかを制御する方法を示している。可変なBox<T>
ではデータを変更できるが、不変なBox<T>
ではデータを変更することはできない。
Cell<T>
とRefCell<T>
CellとRefCellは、Rustプログラミング言語における内部変更可能性(Interior Mutability)のための二つの異なるデータ構造。これらは、不変の参照を通してもデータを変更することができるように設計されている。
Cell<T>
- 用途: Cellは、Copyトレイトを実装した型に適している。Copyトレイトを実装していない型の場合はgetメソッドで値を取得(コピー)することはできない。
- 動作: Cellはsetメソッドを使用して値を更新する。また、getメソッドを用いて値を取得するが、これは値のコピーを返す。
- 制限: Cellは、その中に保存されている値の不変参照を提供することができない。したがって、Cell内のデータに対する直接的な参照を取得することはできない。
use std::cell::Cell;
fn main() {
let cell = Cell::new(5);
let cell_ref = &cell; // 不変の参照を作成
// Cellから値を取得(コピーを取得)
let original_value = cell_ref.get();
println!("元の値(コピー): {}", original_value); // 5
// 不変の参照を通してCell内の値を変更
cell_ref.set(10);
// 再びCellから値を取得(新しいコピーを取得)
let new_value = cell_ref.get();
println!("新しい値(コピー): {}", new_value); // 10
}
この例では、cell_refという不変の参照を使って、Cell内の値を取得し(getメソッドでコピー)、変更する(setメソッドで10に設定)。Cellは内部可変性を持つため、不変の参照からでも値の変更が可能。
RefCell<T>
- 用途: RefCellは、コンパイル時ではなく、ランタイム時に借用規則(borrowing rules)を確認する。これにより、非Copy型の値に対しても内部変更可能性を提供することができる。
- 動作: RefCellはborrowとborrow_mutメソッドを提供して、不変または可変の参照をそれぞれ取得する。これらのメソッドはランタイム時に借用規則をチェックし、違反があればパニックを引き起こす。
- 制限: RefCellを使用すると、コンパイル時ではなく実行時に借用規則の違反が検出される。これにより、エラーが発生した際にプログラムがクラッシュする可能性がある。
use std::cell::RefCell;
struct SomeStruct {
value: RefCell<i32>,
}
fn main() {
let example = SomeStruct {
value: RefCell::new(5),
};
let example_ref = &example; // SomeStructの不変の参照を作成
{
// 不変の参照を通してRefCellの値の不変参照を取得し、読み出す
let value_ref = example_ref.value.borrow();
println!("値: {}", *value_ref);
// value_refがスコープを抜けると、借用が終了する
}
{
// 不変の参照を通してRefCellの値の可変参照を取得し、変更する
let mut value_mut = example_ref.value.borrow_mut();
*value_mut = 10;
// value_mutがスコープを抜けると、借用が終了する
}
println!("変更後の値: {}", example_ref.value.borrow());
}
この例では、SomeStruct構造体のvalueフィールドがRefCell<i32>
型で定義されている。RefCell::newで初期値を設定し、borrowメソッドで不変の参照を取得し、borrow_mutメソッドで可変の参照を取得している。RefCellを使用することで、不変の参照&SomeStructを通しても内部の値を変更することが可能になる。
rawポインタ
Rustには*mut T
と*const T
というrawポインタ型がある。rawポインタは、Rustが全く管理してくれない。rawポインタの参照解決はunsafeブロックの中でしかできない。unsafeブロックは、Rustの高度な言語機能を用いるためのオプトイン機能で、これを用いると安全性を保つ責任はプログラマが負うことになる。
*const T
)
不変のrawポインタ(fn main() {
let x = 10;
let p = &x as *const i32; // x の不変のrawポインタを作成
unsafe {
println!("p が指す値: {}", *p);
}
}
この例では、xの不変のrawポインタpを作成し、unsafeブロック内で参照外し(dereferencing)を行っている。rawポインタの参照外しは常にunsafeブロック内で行わなければならない。
*mut T
)
可変のrawポインタ(fn main() {
let mut x = 10;
let p = &mut x as *mut i32; // x の可変raw生ポインタを作成
unsafe {
*p += 1; // rawポインタを通じて値を変更
println!("x の値: {}", x);
}
}
この例では、xの可変のrawポインタpを作成し、unsafeブロック内でxの値を変更している。rawポインタを通じたデータの変更もunsafeブロック内で行わなければならない。
ファットポインタ
ファットポインタ(fat pointer)は、通常のポインタよりも多くの情報を持っている特殊なポインタの一種。ファットポインタは通常のポインタに追加のメタ情報を持っており、これによりポインタが指すデータ構造に関する追加情報を提供する。
スライスポインタ (&[T]、&mut [T])
スライスは、配列の一部を参照するデータ構造で、ファットポインタはスライスを表現するのに使用される。スライス用のファットポインタはメタ情報として、データへのポインタとスライスの長さ(要素の数)の2つの情報を保持する。これによって、スライスが指す配列部分の開始位置と、スライスがいくつの要素を持っているかという情報を同時に持つことができる。
let arr = [1, 2, 3, 4, 5];
let slice: &[i32] = &arr[1..4]; // sliceは {pointer to arr[1], length: 3} を保持している
トレイトオブジェクト(dyn Trait)
トレイトオブジェクトは、異なる型のオブジェクトを同じトレイトを介して抽象化し、一つの型として扱う手法。ファットポインタによるトレイトオブジェクトはメタ情報として、データへのポインタと、そのデータが実装するトレイトのメソッドへのポインタ(vtable)を保持する。これによって、実際のデータ型を抽象化して、同一のトレイトを実装する異なる型を同じ型として扱うことができる。
trait MyTrait {
fn my_function(&self);
}
impl MyTrait for String {
fn my_function(&self) {
println!("String: {}", self);
}
}
let my_string: Box<dyn MyTrait> = Box::new(String::from("Hello"));
// my_stringは {pointer to String, pointer to vtable for MyTrait for String} を保持している
Discussion