では、どの時点でクラスが複雑すぎて不変にならないのでしょうか?
私の意見では、あなたが示しているような言語で小さなクラスを不変にするのは面倒です。私が使用している小さなここではなく、複雑なあなたがそのクラスに10フィールドを追加し、それが彼らに本当に派手な操作をした場合でも、私はキロバイトだけではメガバイトだけではギガバイトを聞かせて聞かせて取るために起こって疑うので、任意の関数は、あなたのインスタンスを使用して、 classは、オブジェクト全体の安価なコピーを作成して、外部の副作用の発生を回避したい場合に元のオブジェクトの変更を回避できます。
永続的なデータ構造
不変性の個人的な用途は、表示するクラスのインスタンス(100万を格納するものなど)のような小さなデータの集まりを集約する大きな中央のデータ構造ですNamedThings
。不変で永続的なデータ構造に属し、読み取り専用アクセスのみを許可するインターフェースの背後にあることにより、コンテナに属する要素は、要素クラス(NamedThing
)が処理しなくても不変になります。
安いコピー
永続的なデータ構造により、その領域を変換して一意にすることができ、データ構造全体をコピーすることなく元の変更を回避できます。それが本当の美しさです。ギガバイトのメモリを取り、メガバイトのメモリだけを変更するデータ構造を入力する副作用を回避する関数を単純に作成したい場合は、入力に触れずに新しいものを返すために、おかしなもの全体をコピーする必要があります出力。そのシナリオでは、ギガバイトをコピーして副作用を回避するか、副作用を引き起こすため、2つの不快な選択肢から選択する必要があります。
永続的なデータ構造を使用すると、このような関数を記述し、データ構造全体のコピーを作成せずに済みます。関数がメガバイトのメモリを変換するだけの場合、出力に約1メガバイトの追加メモリが必要になります。
重荷
負担については、少なくとも私の場合はすぐに負担があります。人々が話している「一時的」なビルダーを必要としているのは、大規模なデータ構造に手を加えずに効果的に変換を表現できるようにするためです。このようなコード:
void transform_stuff(MutList<Stuff>& stuff, int first, int last)
{
// Transform stuff in the range, [first, last).
for (; first != last; ++first)
transform(stuff[first]);
}
...そして、このように書かなければなりません:
ImmList<Stuff> transform_stuff(ImmList<Stuff> stuff, int first, int last)
{
// Grab a "transient" (builder) list we can modify:
TransientList<Stuff> transient(stuff);
// Transform stuff in the range, [first, last)
// for the transient list.
for (; first != last; ++first)
transform(transient[first]);
// Commit the modifications to get and return a new
// immutable list.
return stuff.commit(transient);
}
しかし、これらの2行のコードと引き換えに、関数は同じ元のリストを持つスレッド間で安全に呼び出すことができ、副作用などを引き起こしません。また、この操作を取り消し可能なユーザーアクションにすることも非常に簡単になります。 undoは、古いリストの安価な浅いコピーを保存するだけです。
例外安全またはエラー回復
このようなコンテキストで永続的なデータ構造から得られるほど誰もが恩恵を受けるとは限りません(VFXドメインの中心概念である取り消しシステムや非破壊編集で非常に有用であることがわかりました)。考慮する必要があるのは、例外安全またはエラー回復です。
元の変更関数を例外に対して安全にしたい場合、ロールバックロジックが必要です。そのためには、最も単純な実装ではリスト全体をコピーする必要があります。
void transform_stuff(MutList<Stuff>& stuff, int first, int last)
{
// Make a copy of the whole massive gigabyte-sized list
// in case we encounter an exception and need to rollback
// changes.
MutList<Stuff> old_stuff = stuff;
try
{
// Transform stuff in the range, [first, last).
for (; first != last; ++first)
transform(stuff[first]);
}
catch (...)
{
// If the operation failed and ran into an exception,
// swap the original list with the one we modified
// to "undo" our changes.
stuff.swap(old_stuff);
throw;
}
}
この時点で、例外セーフな可変バージョンは、「ビルダー」を使用した不変バージョンよりも計算コストが高く、間違いなく正しく記述するのがさらに困難です。そして、多くのC ++開発者は例外安全性を無視しており、おそらくそれは彼らのドメインにとっては問題ありませんが、私の場合は、例外が発生した場合でもコードが正しく機能することを確認したいです(例外をテストするために意図的に例外をスローするテストを書く安全性)、それが原因で、何かがスローされた場合、関数が関数の途中で引き起こす副作用をロールバックできるようにする必要があります。
アプリケーションをクラッシュさせたり焼き付けたりせずに例外から安全にエラーを回復したい場合は、エラー/例外が発生した場合に関数が引き起こす可能性のある副作用を元に戻す/元に戻す必要があります。そして、ビルダーは実際には、計算時間とともにコストよりもプログラマーの時間を節約できます。
副作用を引き起こさない関数で副作用をロールバックすることを心配する必要はありません!
基本的な質問に戻りましょう。
不変クラスはどの時点で負担になりますか?
それらは常に不変性よりも可変性を中心に展開する言語の負担です。そのため、コストよりも利益のほうが大きい場合に使用すべきだと思います。しかし、十分に大きいデータ構造に十分なレベルで、私はそれが価値のあるトレードオフになる多くの場合があると信じています。
私の場合も、不変のデータ型は数個しかなく、それらはすべて膨大な数の要素(画像/テクスチャのピクセル、ECSのエンティティとコンポーネント、および頂点/エッジ/ポリゴンを格納するための巨大なデータ構造です。メッシュ)。