Prompt

よくない設計だとわかっていても、複数から値を変更しなければいけないときはあるよね。
ならば、setterのような関数を経由することで実現できる?

お、それは**「アンチパターン(Anti-pattern)だと分かっていても、現実の要件として必要になる」**という、プログラマが直面する最も難しい問題の一つだね!深く共感するよ!

結論から言うと、Rustでもsetterのような関数を経由することで、複数箇所から値を安全に変更することは可能だよ。ただし、そのためには所有権と借用のルールを守りつつ、特殊な型を組み合わせる必要があるんだ!


1. ⚙️ setter関数を経由する基本的な実現方法

複数の箇所から値を変更したい場合、Rustでは以下の2つの主要なアプローチが考えられるよ。

A. 可変な借用 (&mut T) を使う (最も一般的)

もし、値の変更が単一のスレッド内で、**排他的(一度に一箇所からのみ)**に行われることが保証できるなら、これが最もシンプルで効率的だよ。

  • 実現方法: setter関数が引数として可変な参照 (&mut self) を受け取るように定義する。

Rust

struct Settings {
    volume: u8,
}

impl Settings {
    // 可変な参照を受け取り、排他的に値を変更する
    pub fn set_volume(&mut self, new_volume: u8) {
        self.volume = new_volume;
    }
}

// 呼び出し側:
let mut config = Settings { volume: 50 }; // 1. 設定変数自体を mut にする必要がある
config.set_volume(80);                     // 2. configの排他的な借用を渡す
// config.set_volume(90); // 同じスコープ内で他の借用が生きているとエラー(借用ルール)

【この方法の制限】

  • mutが必要: config変数自体に mut が必要になってしまう。

  • 排他性: 変更したいすべての場所が、configへの排他的な借用(&mut)を同時に持てない。これが、君が求めている「複数から」というニーズに合わない可能性がある。


B. 内部可変性 (RefCell / Mutex) を使う (真の複数箇所変更)

もし、不変な変数(let config)として扱いたいのに、その中身だけを複数箇所から変更したいなら、前回の話に出てきた内部可変性の型が必要になるよ!

1. シングルスレッドでの実現 (RefCell)

Rust

use std::cell::RefCell;

struct Settings {
    volume: RefCell<u8>, // フィールド自体を RefCell で包む
}

impl Settings {
    pub fn set_volume(&self, new_volume: u8) {
        // 不変な &self から、RefCell を使って可変な借用を得る
        *self.volume.borrow_mut() = new_volume;
        // 注意: borrow_mut()が失敗した場合(他の場所で借用中の場合)は panic! が発生する
    }
}

// 呼び出し側:
let config = Settings { volume: RefCell::new(50) }; // 1. configは不変でOK
config.set_volume(80);                              // 2. 不変な参照で setter を呼び出し、内部を変更

2. マルチスレッドでの実現 (Arc<Mutex<T>>)

最も複雑な「悪い設計」の要求(複数のスレッドから共有・変更)に応えるには、この組み合わせが必要になるよ。

  • Arc: 参照カウントを持ち、複数のスレッド間で同じデータへの所有権を共有するために使う。

  • Mutex: データ競合を防ぐため、排他的アクセスをロックするためのメカニズム。

Rust

use std::sync::{Arc, Mutex};
// ...
let shared_config = Arc::new(Mutex::new(Settings { volume: 50 }));

// 別のスレッド A:
let config_a = Arc::clone(&shared_config);
thread::spawn(move || {
    let mut data = config_a.lock().unwrap(); // ロックを取得し、排他的なアクセス権を得る
    data.volume = 90;
});

【この方法の制限】

  • 実行時コスト: RefCell::borrow_mut()Mutex::lock() は、実行時に安全性をチェックする(ランタイムコスト)。借用ルール違反やデッドロックの可能性をゼロにはできない。

  • 複雑性: Arc<Mutex<RefCell<T>>> のように型が非常に複雑になり、コードの可読性が下がる。


2. ⚠️ 設計が良くないことを認識しているプログラマへのアドバイス

君が「よくない設計だとわかっていても」という前提を持っているのが素晴らしいね!情報工学を学ぶ君には、その悪い設計の「ツケ」が、そのまま技術的な複雑性としてコードに現れることを理解してほしいんだ。

ツケの例:

  • RefCellを使う場合: 「実行時に借用ルール違反による panic!」というリスクを負う。

  • Mutexを使う場合: 「デッドロック」や「ロック待機によるパフォーマンス低下」というリスクを負う。

だから、これらの内部可変性パターンを使うときは、「ここには、Rustのコンパイル時チェックから漏れた、実行時のリスクが潜んでいる」という警告ラベルを貼るような気持ちでいるといいよ!

理想は、可能な限り値を単一の責任者に所有させ、ミュータブルな借用 (&mut T) の範囲を最小限に抑えることだよ!✨