コンピュータプログラミングにおいて、イミュータブル (: immutable) なオブジェクトとは、作成後にその状態を変えることのできないオブジェクトのことである。対義語はミュータブル (: mutable) なオブジェクトで、作成後も状態を変えることができる。mutableは「変更可能な」、immutableは「変更不可能な、不変の」という意味を持つ形容詞である。

あるオブジェクト全体がイミュータブルなこともあるし、C++constデータメンバを使う場合など、一部の属性のみがイミュータブルなこともある。場合によっては、内部で使われている属性が変化しても、外部からオブジェクトの状態が変化していないように見えるならば、オブジェクトをイミュータブルとみなすことがある。例えば、コストの高い計算の結果をキャッシュするためにメモ化を利用していても、そのオブジェクトは依然イミュータブルとみなせる。イミュータブルなオブジェクトの初期状態は大抵は生成時に設定されるが、オブジェクトが実際に使用されるまで遅らせることもある。

イミュータブルなオブジェクトを使うと、複製や比較のための操作を省けるため、コードが単純になり、また性能の改善にもつながる。しかしオブジェクトが変更可能なデータを多く持つ場合には、イミュータブル化は不適切となることが多い。このため、多くのプログラミング言語ではイミュータブルかミュータブルか選択できるようにしている。

背景

編集

ほとんどのオブジェクト指向言語では、オブジェクトは参照の形でやり取りされる。JavaC++PerlPythonRubyなどがその例である。この場合、オブジェクトが参照を通じて共有されていると、その状態が変更される可能性が問題となる。

もしオブジェクトがイミュータブルであったなら、オブジェクトの複製はオブジェクト全体の複製ではなく、単に参照の複製で済む。参照は通常オブジェクト自体よりもずっと小さいので(典型的にはポインタのサイズのみ)、メモリが節約でき、プログラムの実行速度もよくなる。

参照コピーのテクニックは、ミュータブルなオブジェクトに対して使用するのはさらに難しくなる。なぜならば、ミュータブルなオブジェクトの参照を保持する者が1人でもオブジェクトに変更を加えると、参照を共有する者全員がその影響を受けるからである。これが意図した作用でないならば、その他の参照の保持者に正しく対処してもらうよう通知するのは難しくなる可能性がある。このような場合、参照ではなくオブジェクト全体の防衛的(防御的)コピー英語版 (defensive copy) [1]を作成するのが一般的な解決法だが、これは簡単ではあるもののコストがかかる。他にはObserver パターンがミュータブルなオブジェクトへの変更に対処するのに利用できる。

イミュータブルなオブジェクトはマルチスレッドプログラミングにおいても有用となる。データがイミュータブルなオブジェクトで表現されていると、複数のスレッドが他のスレッドにデータを変更される心配なくデータにアクセスできる。つまり排他制御の必要がない。よってイミュータブルなオブジェクトのほうがミュータブルなものよりスレッドセーフであると考えられる。

等価なオブジェクトのコピーを作成する代わりに常に参照を複製するというテクニックはインターン英語版として知られる。インターンが使われているならば、2つのオブジェクトが等しいとみなされるのは、2つの参照が等しい場合でありかつその場合に限られる。いくつかの言語では自動的にインターンが行なわれる。例えばPythonでは文字列を自動的にインターンする。インターンを実装したアルゴリズムで、可能な場合は常にインターンすることが保証されているならば、オブジェクトの等価性の比較はそのポインタの比較に帰着し、多くのアプリケーションで高速化が達成できる。またアルゴリズムがそのような保証をしない場合でも、オブジェクトの比較の平均的なコストを下げることができる。一般的に、インターンの価値があるのはオブジェクトがイミュータブルなときだけである。

実装

編集

イミュータブルとは即ちオブジェクトがコンピュータのメモリ中で書き込み不可能であるという意味ではない。むしろイミュータブルはコンパイル時の問題であり、プログラマが「何をすべきか」であって、必ずしも「何ができるか」ではない。例えばCやC++で型システムの裏をかくのを禁止するためのものではない。

ミュータブルとイミュータブルの利点をいいとこ取りする、モダンなハードウェアがサポートしているテクニックはコピーオンライトである。このテクニックでは、利用者がシステムにオブジェクトを複製するように命じた際に、代わりに同一のオブジェクトを指す参照を作る。利用者がある参照を通してそのオブジェクトに変更を加えると、直ちに本物の複製を作ってそれを指すように参照を書き換える。これによって他の利用者は影響されない。なぜなら、依然オリジナルのオブジェクトを参照しているからである。したがって、コピーオンライト環境ではすべての利用者はオブジェクトをミュータブルなものとして持っているように見えるが、利用者がそのオブジェクトを書き換えない限り、イミュータブルなオブジェクトの実行効率も獲得できる。コピーオンライトは仮想記憶システムでよく利用され、プログラムが他のプログラムにメモリを書き換えられる心配なしにメモリを節約することができる。

イミュータブルなオブジェクトの古典的な例はJavaのStringクラスのインスタンスである。

String str = "ABC";
str.toLowerCase();

メソッドtoLowerCase()は変数strの値"ABC"を書き換えない。代わりに新しいStringオブジェクトがインスタンス化され、生成時に"abc"という値が与えられる。このStringオブジェクトへの参照はtoLowerCase()が返す。変数strに値"abc"を持たせたいのなら、

str = str.toLowerCase();

とする必要がある。Stringクラスのメソッドはインスタンスの持つデータを書き換えることがない。

オブジェクトがイミュータブルであるためには、フィールドがミュータブルであるかどうかとは別に、そのフィールドを書き換える方法があってはならず、またミュータブルなフィールドを読み書きする方法があってはならない。Javaでミュータブルなオブジェクトの例を示す。

class Cart<T> {
  private final List<T> items;

  public Cart(List<T> items) { this.items = items; }

  public List<T> getItems() { return items; }
  public int total() { /* return sum of the prices */ }
}

このクラスのインスタンスはイミュータブルではない。なぜなら、getItems()を呼んでitemsフィールドの参照コピーを得たり、インスタンス化の際に渡したListオブジェクトの参照を保持し続けたりすることで、itemsフィールドが指しているListオブジェクトの内容を書き換えることが可能だからである。以下のImmutableCartクラスは部分的にイミュータブルになるように書き換えた例である。

class ImmutableCart<T> {
  private final List<T> items;

  public ImmutableCart(List<T> items) {
    //this.items = Arrays.asList(items.toArray()); // ジェネリクスでは使えない。
    //this.items = List.copyOf(items); // Java 10 以降で使えるものの、要素として null を含んでいた場合は NullPointerException がスローされてしまう。
    this.items = new ArrayList<T>();
    this.items.addAll(items);
  }

  public List<T> getItems() {
    return Collections.unmodifiableList(items);
  }
  public int total() { /* return sum of the prices */ }
}

java.util.Collections.unmodifiableList(List<T>)は、引数で指定されたリストの変更不可能なビューを返す。つまり、読み取り専用のアクセスのみを許可する(返却されたリストを書き換えようとした場合はUnsupportedOperationExceptionがスローされる)。

したがって、もはやitemsを書き換えることはできない。しかし、リストitemsの要素もイミュータブルであるという保証はない。解決法の一つとしてはDecorator パターンでリストの各要素をラップしてしまうというものがある。


C++ではCartをconst-correctな実装にすることで、インスタンスをイミュータブル (const) としてもミュータブルとしても好きなように宣言できる。つまり、2つの異なるgetItems()を提供するのである。

template<typename T>
class Cart {
private:
  std::vector<T> m_items;

public:
  explicit Cart(const std::vector<T>& items) : m_items(items) { }

  std::vector<T>& getItems() { return m_items; }
  const std::vector<T>& getItems() const { return m_items; }
  int total() const { /* return sum of the prices */ }
};

前述のJavaの例ではコンストラクタ引数の参照コピーとしてフィールドに保持していたが、C++ではメンバー変数に格納する時点で複製を行なっている。これはC++にガベージコレクションがないための定石によるものである。メンバー変数m_itemsをポインタ型または参照型にすることもできるが、そうするとコンストラクタ呼び出し元で渡した変数の寿命が尽きた場合に無効なアドレスを指すダングリングポインタまたはダングリング参照となってしまい、管理が煩雑となる。

上記の例では、ミュータブル用のgetItems()はメンバー変数m_itemsへの参照を直接返しており、この参照経由でオブジェクトの状態を変更することができる。メンバー変数m_itemsがすでに複製であることから、イミュータブル用のgetItems() constの内容は戻り値の型にconstを加えるだけでよい。const修飾されたオブジェクトからはconst修飾されたメンバーにしかアクセスできないため、破壊的な操作はできなくなる。ただし、内部状態への参照を返すと、Cart型変数が破棄されたときにその参照も無効となってしまうため、扱いに注意が必要である。安全のため、イミュータブル用のgetItems() constの戻り値の型はstd::vector<T>として、ディープコピーを返す実装とすることもある(防衛的コピー)。

このC++の例はイミュータブル/ミュータブル兼用として作成されたものだが、コンストラクタについて2つのバージョンを用意する必要はないし、実際にできない。Cart型の変数を宣言するときにconstを付けるかどうかを決めるだけである。

なお、C++でも共有ポインタ(参照カウント方式のスマートポインタ)を使えば、Javaのようにコンストラクタ呼び出し元と状態を共有する設計にすることもできる。共有ポインタのコピーは、配列全体のコピーよりもコストがずっと小さいため、場合によってはあえてこちらのミュータブルな設計を選択することもある。

template<typename T>
class Cart {
private:
  std::shared_ptr<std::vector<T>> m_items;

public:
  explicit Cart(std::shared_ptr<std::vector<T>> items) : m_items(items) { }

  std::shared_ptr<std::vector<T>> getItems() { return m_items; }
  std::shared_ptr<const std::vector<T>> getItems() const { return m_items; }
  int total() const { /* return sum of the prices */ }
};

また、クラスが不変であるかどうかを確認する方法の一つとしてFindBugsというツールを使うという手がある。これはバグの温床になるコードを自動検出してくれて、不変クラスを作るときにも貢献する。

関連項目

編集

脚注

編集

参考資料

編集

外部リンク

編集

英語