イミュータブル
コンピュータプログラミングにおいて、イミュータブル (英: immutable) なオブジェクトとは、作成後にその状態を変えることのできないオブジェクトのことである。対義語はミュータブル (英: mutable) なオブジェクトで、作成後も状態を変えることができる。mutableは「変更可能な」、immutableは「変更不可能な、不変の」という意味を持つ形容詞である。
あるオブジェクト全体がイミュータブルなこともあるし、C++でconst
データメンバを使う場合など、一部の属性のみがイミュータブルなこともある。場合によっては、内部で使われている属性が変化しても、外部からオブジェクトの状態が変化していないように見えるならば、オブジェクトをイミュータブルとみなすことがある。例えば、コストの高い計算の結果をキャッシュするためにメモ化を利用していても、そのオブジェクトは依然イミュータブルとみなせる。イミュータブルなオブジェクトの初期状態は大抵は生成時に設定されるが、オブジェクトが実際に使用されるまで遅らせることもある。
イミュータブルなオブジェクトを使うと、複製や比較のための操作を省けるため、コードが単純になり、また性能の改善にもつながる。しかしオブジェクトが変更可能なデータを多く持つ場合には、イミュータブル化は不適切となることが多い。このため、多くのプログラミング言語ではイミュータブルかミュータブルか選択できるようにしている。
背景
[編集]ほとんどのオブジェクト指向言語では、オブジェクトは参照の形でやり取りされる。Java・C++・Perl・Python・Rubyなどがその例である。この場合、オブジェクトが参照を通じて共有されていると、その状態が変更される可能性が問題となる。
もしオブジェクトがイミュータブルであったなら、オブジェクトの複製はオブジェクト全体の複製ではなく、単に参照の複製で済む。参照は通常オブジェクト自体よりもずっと小さいので(典型的にはポインタのサイズのみ)、メモリが節約でき、プログラムの実行速度もよくなる。
参照コピーのテクニックは、ミュータブルなオブジェクトに対して使用するのはさらに難しくなる。なぜならば、ミュータブルなオブジェクトの参照を保持する者が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());
}
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> 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 */ }
};
前述のJavaのコードがイミュータブルではない理由は他にもある。クラスが継承可能であるということである。サブクラスで勝手にitems
を書き換えるsetterメソッドを実装される恐れがある。そこでclassにfinal
修飾子を付加する。念のためメソッド引数にもfinal
をつけておく。さらに、「防衛的コピー」という手法を用いて、getItems()
で取り出したList
を変更されても、ImmutableCart
クラスが持っているitems
フィールドの内容まで変更されないよう、フィールドをコピーしておく。上の例では、List
を一旦配列に変換してから戻すという手法が使われたが、ここでは替わりにObject.clone()
メソッドを用いて解決する。引数のオブジェクトを一旦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というツールを使うという手がある。これはバグの温床になるコードを自動検出してくれて、不変クラスを作るときにも貢献する。
関連項目
[編集]脚注
[編集]参考資料
[編集]外部リンク
[編集]英語
- Pattern: Immutable Object by Nat Pryce
- Java theory and practice: To mutate or not to mutate? by Brian Goetz
- Java Practices: Immutable objects
- Immutable Object - Portland Pattern Repository