イミュータブル

出典: フリー百科事典『ウィキペディア(Wikipedia)』
移動: 案内検索

オブジェクト指向プログラミングにおいて、イミュータブル(immutable)なオブジェクトとは、作成後にその状態を変えることのできないオブジェクトのことである。対義語はミュータブル(mutable)なオブジェクトで、作成後も状態を変えることができる。

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

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

背景[編集]

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

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

オブジェクトがミュータブルであると、参照コピーのテクニックはずっと困難になる。なぜなら、ミュータブルなオブジェクトの参照を保持する者が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オブジェクトを保持し続けることで、フィールドが書き換え可能だからである。以下のImmutableCartクラスは部分的にイミュータブルになるように書き換えた例である。

class ImmutableCart<T> {
  private final List<T> items;
 
  public ImmutableCart(List<T> items) {
    this.items = Arrays.asList(items.toArray());
  }
 
  public List<T> getItems() {
    return Collections.unmodifiableList(items);
  }
  public int total() { /* return sum of the prices */ }
}

次にRubyでのほぼ同じ例を載せる。

class Cart
  def initialize(items)
    @items = items.dup.freeze
  end
 
  def items
    @items.clone
  end
 
  def total
    # sum of the prices
  end
end

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

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

template<typename T>
class Cart {
 private:
  std::vector<T> items;
 
 public:
  Cart(std::vector<T> v): items(v) { }
 
  std::vector<T>& getItems() { return items; }
  const std::vector<T>& getItems() const { return items; }
  int total() const { /* return sum of the prices */ }
};

前述のJavaの例ではコンストラクタ引数を参照として保持していたが、C++ではメンバ変数に格納する時点で複製を行なっている。これはC++に自動ガベージコレクションがないための定石によるものである。こうしてメンバ変数がすでに複製であることから、getItems() constの内容はJavaと違って戻り値の型にconstを加えるだけでよい。

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

前述のJavaのコードがイミュータブルではない理由は他にもある。クラスが継承可能であるということである。サブクラスで勝手にitemsを書き換えるsetterメソッドを実装される恐れがある。そこでclassにfinal修飾子を付加する。念のためメソッド引数にもfinalをつけておく。さらに、「防御的コピー」という手法を用いて、getItems()で取り出したListを変更されても、ImmutableCartクラスが持っているitemsフィールドの内容まで変更されないよう、フィールドをコピーしておく。上のほうでは、Listを一旦配列に変換してから戻すという手法が使われたが、ここでは替わりにObject.clone(Object)メソッドを用いて解決する。引数のオブジェクトを一旦clone()でコピーしてからフィールドに代入することで、引数に渡す前のListオブジェクトとの参照を切り離すことができ、引数に渡す前のListオブジェクトを変更してもImmutableCartにあるListオブジェクトitemsにまで変更が及ばないようにすることができる。これは、getItems()メソッドでも同様に行う必要がある。Collections.checkedList(List<T>,Class<T>) は、itemsが他のメソッドに渡されたときにリストにTとは異なる型が代入されるのを防ぐためにある。パラメタライズされていない非汎用のListでしか動作しないメソッドに渡すと、そのメソッドはそのListに対して型Tではない要素を不正に追加する恐れがある。Collections.unmodifiableList(List<T>)はリストの要素を変更できないようfinalにするためにある。

final class ImmutableCart<T> {
  private final List<T> items;
 
  public ImmutableCart(final List<T> items) {
    //防御的コピーを行った後、リストの要素の実行時タイプチェックを行い、
    //リストの要素をfinalにする。
    this.items = Collections.unmodifiableList(
                   Collections.checkedList(
                     (List<T>) items.clone(), T.class
                   )
                 );
  }
 
  public List<T> getItems() {
    return (List<T>) items.clone(); //内部フィールドの防御的コピー
  }
  public int total() { /* return sum of the prices */ }
}

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

関連項目[編集]

参考資料[編集]

外部リンク[編集]

英語