ここにはいくつかの良い例がありますが、不変性が非常に役立ついくつかの個人的な例に飛び込みたいと思いました。私の場合、主に重複する読み取りおよび書き込みと並行してコードを自信を持って実行でき、競合状態を心配することなく、不変の並行データ構造の設計を開始しました。ジョン・カーマックがそのようなアイデアについて話したところ、ジョン・カーマックが私にそれをやる気にさせた話がありました。これは非常に基本的な構造であり、次のように実装するのは非常に簡単です。
もちろん、一定の時間内に要素を削除し、回収可能な穴を残して、特定の不変のインスタンスに対してブロックが空になり、潜在的に解放されるとブロックの参照を取り消すことができるように、いくつかの追加機能があります。ただし、基本的に構造を変更するには、「一時的な」バージョンを変更し、変更を原子的にコミットして、古いバージョンに影響を与えない新しい不変のコピーを取得します。新しいバージョンでは、浅くコピーし、他をカウントしながら参照を一意にする必要があります。
しかし、私はそれを見つけられませんでしたマルチスレッドの目的に役立ちます。結局のところ、たとえば、物理システムが物理を同時に適用しているときに、プレイヤーが世界の中で要素を動かそうとしているという概念上の問題があります。変換されたデータのどの不変のコピーを使いますか、プレイヤーが変換したものか、物理システムが変換したものですか?そのため、この基本的な概念的な問題に対する素敵で簡単な解決策は、スマートな方法でロックし、スレッドのストールを回避するためにバッファーの同じセクションへの読み取りと書き込みの重複を防ぐ可変データ構造を持つことを除いて、本当に見つかりませんでした。それはジョン・カーマックが彼のゲームでどのように解決するかをおそらく考え出したようです。少なくとも彼は、ワームの車を開けることなくほとんど解決策を見ることができるようにそれについて話します。私は彼に関してはその点に関しては得ていません。不変物の周りのすべてを並列化しようとした場合、私が見ることができるのは無限の設計質問です。私の努力の大部分は彼が投げ出したアイデアから始まったので、私は彼の脳を選ぶことに一日を費やすことができたらいいのにと思います。
それにもかかわらず、私は他の分野でこの不変のデータ構造の莫大な価値を発見しました。私は今でもそれを使って本当に奇妙な画像を保存し、ランダムアクセスにさらに命令を必要とします(右シフトとand
ポインタ間接化の層とともにビット単位)が、以下の利点をカバーします。
元に戻すシステム
これにより恩恵を受けることがわかった最も近い場所の1つは、取り消しシステムです。元に戻すシステムコードは、私の地域(ビジュアルFX業界)で最もエラーが発生しやすいものの1つであり、作業した製品だけでなく、競合する製品(元に戻すシステムも不安定でした)正しく元に戻したりやり直したりすることを心配するデータの種類(プロパティシステム、メッシュデータの変更、相互に交換するようなプロパティベースではないシェーダーの変更、子の親の変更などのシーン階層の変更、画像/テクスチャの変更、などなど)。
そのため、必要な取り消しコードの量は膨大で、多くの場合、取り消しシステムが状態の変更を記録する必要があるシステムを実装するコードの量に匹敵します。このデータ構造に頼ることで、元に戻すシステムを次のようにすることができました。
on user operation:
copy entire application state to undo entry
perform operation
on undo/redo:
swap application state with undo entry
通常、上記のコードは、シーンデータがギガバイトにまたがって全体をコピーする場合、非常に非効率的です。しかし、このデータ構造は、変更されていないものを浅くコピーするだけであり、実際には、アプリケーション全体の状態の不変のコピーを保存するのに十分なほど安価になりました。したがって、上記のコードと同じくらい簡単に取り消しシステムを実装し、この不変のデータ構造を使用して、アプリケーション状態の変更されていない部分のコピーをますます安くすることができます。このデータ構造の使用を開始してから、すべての個人プロジェクトには、この単純なパターンを使用するだけの取り消しシステムがあります。
ここにまだいくらかのオーバーヘッドがあります。前回の測定では、アプリケーションの状態を変更せずに浅くコピーするために約10キロバイトでした(シーンは階層に配置されているため、シーンの複雑さに依存しません。子に降りることなく浅くコピーされます)。これは、デルタのみを保存するアンドゥシステムに必要な0バイトからはほど遠いものです。ただし、操作ごとに10キロバイトの取り消しオーバーヘッドがありますが、これは100ユーザー操作あたりわずか1メガバイトです。さらに、必要に応じて、将来的にそれをさらに押しつぶす可能性があります。
例外安全性
複雑なアプリケーションでの例外安全性は簡単なことではありません。ただし、アプリケーションの状態が不変で、一時的なオブジェクトのみを使用してアトミック変更トランザクションをコミットしようとすると、コードの一部がスローされた場合、新しい不変のコピーを与える前に一時的なオブジェクトが破棄されるため、本質的に例外に対して安全です。そのため、複雑なC ++コードベースで正しく動作することが常にわかっている最も難しいことの1つを単純化します。
多くの場合、C ++でRAII準拠のリソースを使用しているだけであり、例外に対して安全であると考えています。多くの場合、そうではありません。関数は一般に、そのスコープに対してローカルな状態以外の状態に副作用を引き起こす可能性があるためです。これらの場合、通常、スコープガードと洗練されたロールバックロジックの処理を開始する必要があります。このデータ構造がそれを作ったので、関数が副作用を引き起こさないので、私はしばしばそれを気にする必要はありません。アプリケーションの状態を変換する代わりに、アプリケーションの状態の変換された不変のコピーを返します。
非破壊編集
非破壊編集とは、基本的に、元のユーザーのデータに触れることなく操作を階層化/スタック/接続することです(入力に触れることなく入力データと出力データのみを入力します)。通常、Photoshopのような単純な画像アプリケーションで実装するのは簡単ですが、多くの操作では画像全体のすべてのピクセルを変換したいだけなので、このデータ構造の恩恵はあまりありません。
ただし、たとえば、非破壊的なメッシュ編集では、多くの操作でメッシュの一部のみを変換したいことがよくあります。1つの操作で、ここにいくつかの頂点を移動するだけの場合があります。別の人は、そこにいくつかのポリゴンを再分割したいだけかもしれません。ここで、不変のデータ構造は、メッシュの一部を変更した新しいバージョンを返すためにメッシュ全体のコピー全体を作成する必要性を回避するのに役立ちます。
副作用の最小化
これらの構造が手元にあれば、パフォーマンスを大幅に低下させることなく、副作用を最小限に抑える関数を簡単に作成できます。少し無駄に思えても、副作用を引き起こすことなく、不変のデータ構造全体を値で返す関数をますます多く書いていることに気づきました。
たとえば、通常、多数の位置を変換する誘惑は、マトリックスとオブジェクトのリストを受け入れ、それらを可変的な方法で変換することです。最近では、オブジェクトの新しいリストを返すだけです。
副作用を引き起こさないこのような関数がシステムにある場合、その正確性についての推論や正確性のテストが間違いなく容易になります。
安価なコピーの利点
とにかく、これらは不変のデータ構造(または永続的なデータ構造)の中で最も使用が多い領域です。また、最初は少し熱心になり、不変のツリーと不変のリンクリストと不変のハッシュテーブルを作成しましたが、時間が経つにつれてそれらの用途がめったに見つかりませんでした。私は主に、上の図で、チャンキーで不変の配列のようなコンテナの使用が最も多いことを発見しました。
私はまだミュータブルで動作する多くのコードも持っています(少なくとも低レベルのコードでは実際に必要です)が、主なアプリケーションの状態は不変の階層であり、不変のシーンから内部の不変のコンポーネントにドリルダウンします。一部の安価なコンポーネントはまだ完全にコピーされますが、メッシュや画像などの最も高価なコンポーネントは不変の構造を使用して、変換が必要な部分のみの部分的に安価なコピーを許可します。
ConcurrentModificationException
通常foreach
、同じコレクションのループの本体で、同じスレッドのコレクションを変更する同じスレッドによって引き起こされる適切な名前を見てください。