⚛️

Rustのスマートポインタ解説:所有権、メモリ、安全性

に公開

表紙

Rust におけるスマートポインタとは

スマートポインタ(smart pointers)は、データの所有権と追加機能を持つポインタであり、ポインタの進化版と言えます。

ポインタ(pointer)は、メモリアドレスを保持する変数の一般的な概念です。このアドレスは、他のデータを参照、または「指す」(points at)ものです。Rust では、参照は&記号で示され、参照先の値を借用します。通常の参照はデータを借用するだけで、特別な機能はありません。また、追加のオーバーヘッドもないため、Rust では非常に多用されます。

スマートポインタは、Rust における特別なデータ構造です。通常のポインタとの本質的な違いは、通常のポインタが値を借用するのに対し、スマートポインタはデータの所有権を持つことが多い点です。また、多くの追加機能を実現することができます。

Rust のスマートポインタの用途と解決する問題

スマートポインタは、メモリ管理と並行処理のための強力な抽象化を提供します。以下のような抽象化が含まれます。

  • Box<T>:ヒープ上に値を割り当てるために使用されます。
  • Rc<T>:複数の所有権を可能にする参照カウント型。
  • RefCell<T>:内部可変性を提供し、同一データに対する複数の可変参照を可能にします。

これらは標準ライブラリで定義されており、柔軟なメモリ管理を実現できます。スマートポインタの特徴の一つは、DropDerefという 2 つのトレイトを実装していることです。

  • Dropトレイトは、オブジェクトがスコープから外れたときにdropメソッドが自動的に呼ばれるようにします。
  • Derefトレイトは、自動でポインタを参照外しできるようにし、スマートポインタを手動で参照外しする手間を省きます。

Rust でよく使われるスマートポインタ

  • Box<T>:最も基本的なスマートポインタで、ヒープに値を割り当て、スコープ外に出た際に自動でメモリを解放します。
  • Rc<T>およびArc<T>:参照カウント型で、複数のポインタが同じ値を指すことを可能にします。Rc<T>はスレッドセーフではありませんが、Arc<T>はスレッドセーフです。

内部可変性型により、不変の参照であっても内部の値を変更できます。Rust にはいくつかの内部可変性型があります。

  • Cell<T>Copy型に対してのみ使用され、値のコピーによって内部可変性を実現します。
  • RefCell<T>:非Copy型にも対応し、実行時の借用チェックによって安全性を確保します。
  • UnsafeCell<T>:実行時チェックを行わない低レベルの内部可変性型で、誤用すると未定義の動作を引き起こす可能性があります。

また、Rust には**Weak<T>**という弱い参照型もあり、Rc<T>またはArc<T>と組み合わせて循環参照を防ぐのに役立ちます。Weak<T>は参照カウントを増加させないため、値の解放を妨げません。

Box<T>

Box<T>は最も基本的なスマートポインタで、ヒープ上に値を割り当て、スコープ外に出ると自動的にメモリを解放します。

Box<T>の使用シーンは以下の通りです:

  • サイズが不確定な型の場合、Box<T>でヒープにメモリを割り当てることで管理が容易になります(例:再帰型)。
  • 大規模なデータ構造をスタックではなくヒープに割り当てたい場合に使用します。これにより、スタックオーバーフローを防ぎます。
  • 値の型のみを気にし、メモリの使用方法を気にしない場合。たとえば、クロージャを関数に渡す際にBox<T>に格納して扱います。

簡単な例

fn main() {
    let b = Box::new(5);
    println!("b = {}", b);
}

この例では、変数bはヒープに割り当てられた値5を指すBoxです。このプログラムはb = 5と出力します。bmainのスコープを抜けると自動的に解放されます。

ただし、Box<T>は所有権が一つだけなので、複数の場所で同じ値を共有することはできません。たとえば、次のコードではエラーになります。

enum List {
    Cons(i32, Box<List>),
    Nil,
}

use crate::List::{Cons, Nil};

fn main() {
    let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
    let b = Cons(3, Box::new(a));
    let c = Cons(4, Box::new(a));
}

このコードは次のエラーになります。

error[E0382]: use of moved value: `a`

aの所有権はbにムーブされたため、cで再利用できません。こうした場合はRc<T>を使用します。

Rc<T> - Reference Counted(参照カウント)

Rc<T>は参照カウント型で、複数のポインタが同じ値を指すことを可能にします。最後のポインタがスコープを抜けたときに値が解放されます。Rc<T>はスレッドセーフではないため、マルチスレッド環境では使用できません。

Rc<T>の使用場面:

  • 複数の場所でデータを共有したいとき。Box<T>では所有権の移動によるエラーが発生しますが、Rc<T>はそれを解決します。
  • 循環参照を作成したいとき。Rc<T>Weak<T>を併用して実現できます。

例:Rc<T>でデータを共有

use std::rc::Rc;

fn main() {
    let data = Rc::new(vec![1, 2, 3]);
    let data1 = data.clone();
    let data2 = data.clone();

    println!("data: {:?}", data);
    println!("data1: {:?}", data1);
    println!("data2: {:?}", data2);
}

この例では、Rc::newで新しいRc<T>インスタンスを作成し、cloneメソッドを使って複数のポインタを作成しています。すべてのポインタが同じデータを指しています。Rc<T>は参照カウントを管理し、最後のポインタがスコープを抜けた時に自動的にメモリを解放します。

ただし、Rc<T>はスレッドセーフではないため、マルチスレッド環境で使用するにはArc<T>を使う必要があります。

Arc<T> - Atomically Reference Counted(アトミック参照カウント)

Arc<T>はスレッドセーフな参照カウント型で、複数のスレッド間で安全にデータを共有できます。Rc<T>と同様に、最後の参照がスコープを抜けると、値が解放されます。

Arc<T>の使用場面:

  • マルチスレッド環境でデータを共有したいとき。
  • スレッド間でデータを受け渡したいとき。

例:Arc<T>でスレッド間のデータ共有

use std::sync::Arc;
use std::thread;

fn main() {
    let data = Arc::new(vec![1, 2, 3]);
    let data1 = data.clone();
    let data2 = data.clone();

    let handle1 = thread::spawn(move || {
        println!("data1: {:?}", data1);
    });

    let handle2 = thread::spawn(move || {
        println!("data2: {:?}", data2);
    });

    handle1.join().unwrap();
    handle2.join().unwrap();
}

この例では、Arc::newで新しいArc<T>インスタンスを作成し、cloneでスレッドに渡しています。Arc<T>は内部的にアトミック操作によって参照カウントを管理しているため、スレッド間でも安全に使用できます。

Weak<T> - 弱い参照型

Weak<T>Rc<T>Arc<T>と一緒に使用することで循環参照を防ぐための弱い参照型です。Weak<T>は参照カウントを増加させないため、値の解放を妨げません。

Weak<T>の使用場面:

  • 値を監視したいが、所有権は持ちたくないとき。
  • 循環参照を回避したいとき。

例:Rc<T>Weak<T>で循環参照を解決

use std::rc::{Rc, Weak};

struct Node {
    value: i32,
    next: Option<Rc<Node>>,
    prev: Option<Weak<Node>>,
}

fn main() {
    let first = Rc::new(Node { value: 1, next: None, prev: None });
    let second = Rc::new(Node { value: 2, next: None, prev: Some(Rc::downgrade(&first)) });
    first.next = Some(second.clone());
}

この例では、Node構造体にnextprevを持たせ、prevには弱い参照Weak<T>を使用しています。Rc::downgradeで弱い参照を作成し、循環参照によるメモリリークを防ぎます。

注意Weak<T>は値が解放されても安全ですが、値を使用する際にはupgradeメソッドで強い参照Rc<T>に変換する必要があります。もし値が解放されていれば、Noneが返ります。

UnsafeCell<T>

UnsafeCell<T>は、最も低レベルな内部可変性型で、不変の参照からでも内部の値を変更できます。ただし、Cell<T>RefCell<T>のように実行時の安全性チェックは行わないため、誤用すると未定義の動作を引き起こします。

例:UnsafeCell<T>を使用して値を変更

use std::cell::UnsafeCell;

fn main() {
    let x = UnsafeCell::new(1);
    let y = &x;
    let z = &x;
    unsafe {
        *x.get() = 2;
        *y.get() = 3;
        *z.get() = 4;
    }
    println!("x: {}", unsafe { *x.get() });
}

この例では、UnsafeCell::newでインスタンスを作成し、getメソッドで裸ポインタを取得しています。その後、unsafeブロック内で値を変更しています。

注意UnsafeCell<T>は実行時のチェックを行わないため、安全性を確保するのは開発者の責任です。通常はCell<T>RefCell<T>など、より安全な型を使用することが推奨されます。

Cell<T>

Cell<T>は内部可変性型で、不変の参照であっても内部の値を変更できます。Cell<T>Copy型の値に対してのみ使用可能で、値のコピーによって内部可変性を実現します。

Cell<T>の使用場面:

  • 不変の参照でありながら内部の値を変更したいとき。
  • 構造体の中に可変なフィールドを持ちたいとき。

例:Cell<T>で内部値を変更する

use std::cell::Cell;

fn main() {
    let x = Cell::new(1);
    let y = &x;
    let z = &x;
    x.set(2);
    y.set(3);
    z.set(4);
    println!("x: {}", x.get());
}

この例では、Cell::newで新しいCell<T>インスタンスを作成し、setメソッドを使って内部の値を順番に変更しています。最終的にx.get()で値を取得し、4が出力されます。

注意

  • Cell<T>Copy型である必要があります。
  • Cell<T>は値をコピーして返すため、参照での借用はできません。非Copy型の値に対してはRefCell<T>を使います。

RefCell<T>

RefCell<T>は内部可変性型で、不変の参照であっても内部の値を変更できます。Cell<T>とは異なり、RefCell<T>Copy型に限らず、あらゆる型に対応しています。実行時に借用のチェックを行い、安全性を確保します。

RefCell<T>の使用場面:

  • 不変の参照でありながら内部の値を変更したいとき。
  • Copy型の値を内部で可変にしたいとき。
  • 構造体の中に可変なフィールドを持ちたいとき。

例:RefCell<T>で内部値を変更する

use std::cell::RefCell;

fn main() {
    let x = RefCell::new(vec![1, 2, 3]);
    let y = &x;
    let z = &x;

    x.borrow_mut().push(4);
    y.borrow_mut().push(5);
    z.borrow_mut().push(6);

    println!("x: {:?}", x.borrow());
}

この例では、RefCell::newでベクターを格納し、borrow_mutで可変の借用を取得して値を追加しています。最終的にx.borrow()で値を借用し、[1, 2, 3, 4, 5, 6]が出力されます。

注意

  • RefCell<T>は実行時に借用チェックを行います。
  • 同時に複数の可変借用を行おうとすると、パニックが発生します。
  • 例えば以下のコードはパニックを引き起こします。
let mut first_borrow = x.borrow_mut();
let second_borrow = x.borrow_mut(); // 実行時エラー

このようにRefCell<T>は、実行時の安全性を確保しながら内部可変性を提供します。

まとめ

Rust のスマートポインタは、所有権とメモリ管理を効率的かつ安全に行うための強力なツールです。以下に主要なスマートポインタとその特徴をまとめます:

  • Box<T>:ヒープメモリに値を格納し、スコープを抜けると自動で解放。
  • Rc<T>:複数の所有権を可能にする参照カウント型。ただし、スレッドセーフではない。
  • Arc<T>:スレッドセーフな参照カウント型で、マルチスレッド環境でも使用可能。
  • Weak<T>:弱い参照型で、循環参照を防ぐために使用される。
  • Cell<T>Copy型の値に対する内部可変性を提供。
  • RefCell<T>:非Copy型にも対応し、実行時の借用チェックで安全性を確保。
  • UnsafeCell<T>:最も低レベルな内部可変性型で、安全性は開発者の責任。

これらのスマートポインタを適切に選択し、利用することで、Rust における安全で効率的なプログラミングが実現できます。


私たちはLeapcell、Rustプロジェクトのホスティングの最適解です。

Leapcell

Leapcellは、Webホスティング、非同期タスク、Redis向けの次世代サーバーレスプラットフォームです:

複数言語サポート

  • Node.js、Python、Go、Rustで開発できます。

無制限のプロジェクトデプロイ

  • 使用量に応じて料金を支払い、リクエストがなければ料金は発生しません。

比類のないコスト効率

  • 使用量に応じた支払い、アイドル時間は課金されません。
  • 例: $25で6.94Mリクエスト、平均応答時間60ms。

洗練された開発者体験

  • 直感的なUIで簡単に設定できます。
  • 完全自動化されたCI/CDパイプラインとGitOps統合。
  • 実行可能なインサイトのためのリアルタイムのメトリクスとログ。

簡単なスケーラビリティと高パフォーマンス

  • 高い同時実行性を容易に処理するためのオートスケーリング。
  • ゼロ運用オーバーヘッド — 構築に集中できます。

ドキュメントで詳細を確認!

Try Leapcell

Xでフォローする:@LeapcellHQ


ブログでこの記事を読む

Discussion