並行性がない場合、不変性は非常に価値がありますか?


53

不変の型、特にコレクションを使用する主な利点として、スレッドセーフが常に/しばしば言及されているようです。

メソッドが文字列の辞書(C#では不変)を変更しないようにしたい状況があります。できるだけ物事を制約したいと思います。

ただし、新しいパッケージ(Microsoft Immutable Collections)に依存関係を追加する価値があるかどうかはわかりません。パフォーマンスも大きな問題ではありません。

したがって、私の質問は、ハードパフォーマンス要件がなく、スレッドセーフの問題がない場合に、不変コレクションを強く推奨するかどうかだと思いますか?値のセマンティクス(私の例のように)も要件である場合とそうでない場合があることを考慮してください。


1
同時変更はスレッドを意味する必要はありません。ConcurrentModificationException通常foreach、同じコレクションのループの本体で、同じスレッドのコレクションを変更する同じスレッドによって引き起こされる適切な名前を見てください。

1
あなたは間違っていませんが、それはOPが求めているものとは異なります。列挙中の変更が許可されていないため、この例外がスローされます。たとえば、ConcurrentDictionaryを使用すると、このエラーが引き続き発生します。
edthethird 14

13
また、逆の質問を自問する必要があるかもしれません:可変性はいつ価値がありますか?
ジョルジオ14

2
Javaでは、可変性が影響を受けhashCode()たりequals(Object)、変更される結果になると、使用時にエラーが発生する可能性がありますCollections(たとえば、HashSetオブジェクトが「バケット」に格納されており、変更後は別のオブジェクトに移動する必要があります)。
SJuan76 14

2
@DavorŽdralo高レベル言語の抽象化に関する限り、広範に渡る不変性はかなり抑えられています。これは、「一時的な値」を作成し、静かに破棄するという非常に一般的な抽象化(Cにも存在する)の自然な拡張です。おそらく、それはCPUを使用するのに非効率的な方法だと言うつもりですが、その引数にも欠点があります:可変性に満足しているが動的な言語は、不変なだけではあるが静的な言語よりもパフォーマンスが悪い場合があります。不変データをジャグリングするプログラムを最適化するコツ:線形型、森林破壊など

回答:


101

不変性は、後でコードを読むときに精神的に追跡する必要がある情報の量を簡素化します。可変変数、特に可変クラスメンバの場合、デバッガでコードを実行することなく、読んでいる特定の行でそれらがどのような状態になるかを知ることは非常に困難です。不変データは簡単に推論できます-常に同じです。変更したい場合は、新しい値を作成する必要があります。

正直に言って、デフォルトでは不変にすることを好み、それが必要であることが証明されている場合、パフォーマンスが必要であることを意味するのか、不変では意味のないアルゴリズムであるのかを変更可能にします。


23
+1同時実行性は同時突然変異ですが、時間とともに広がる突然変異は、
推論

20
これを拡張するには、可変変数に依存する関数は、可変変数の現在の値である追加の隠された引数を取ると考えることができます。同様に、可変変数を変更する関数は、余分な戻り値、つまり可変状態の新しい値を生成すると考えることができます。コードの一部を見るとき、それが可変状態に依存するか、または変更可能な状態を変更するかどうか分からないため、精神的に変更を見つけて追跡する必要があります。また、これにより、可変状態を共有する2つのコードの間にカップリングが導入され、カップリングが不良になります。
ドーバル14

29
@Mehrdad Peopleは、何十年にもわたって大規模なアセンブリプログラムを実施しました。その後、Cを数十年行いました。
Doval 14

11
@Mehrdadオブジェクトが大きい場合、オブジェクト全体をコピーすることは適切なオプションではありません。なぜ改善に関係する規模が重要なのかわかりません。3桁の改善ではなかったという理由だけで、生産性の20%の改善(注:任意の数)を拒否しますか?不変性は正常なデフォルトです。あなたそれから外れることはできますが、あなたには理由が必要です。
ドーバル14

9
@Giorgio Scalaを使用すると、値を変更可能にする必要がある頻度が非常に低いことに気付きました。その言語を使用するときはいつでも、すべてをaにしますがval、非常にまれな場合にのみ、何かをaに変更する必要があることに気付きvarます。特定の言語で定義する「変数」の多くは、何らかの計算の結果を格納する値を保持するためだけのものであり、更新する必要はありません。
KChaloux

22

あなたのコードはあなたの意図を表現すべきです。作成後にオブジェクトを変更したくない場合は、変更できないようにします。

不変性にはいくつかの利点があります。

  • 原作者の意図がよりよく表現されています。

    次のコードで名前を変更すると、アプリケーションが後で例外を生成することをどのように知ることができますか?

    public class Product
    {
        public string Name { get; set; }
    
        ...
    }
    
  • オブジェクトが無効な状態で表示されないようにする方が簡単です。

    これはコンストラクター内でのみ制御する必要があります。一方、オブジェクトを変更する多数のセッターとメソッドがある場合、特に、たとえばオブジェクトを有効にするために2つのフィールドを同時に変更する必要がある場合、そのようなコントロールは特に難しくなる可能性があります。

    アドレスがない場合にはたとえば、オブジェクトが有効であるnull か、 GPS座標はありませんnullが、アドレスとGPS座標の両方が指定されている場合、それは無効です。アドレスとGPS座標の両方にセッターがある場合、または両方が変更可能な場合、これを検証するために地獄を想像できますか?

  • 並行性。

ところで、あなたの場合、サードパーティのパッケージは必要ありません。.NET Frameworkには既にReadOnlyDictionary<TKey, TValue>クラスが含まれています。


1
+1、特に「コンストラクタでこれを制御する必要があり、そこだけで」。IMOこれは大きな利点です。
ジョルジオ

10
別の利点:オブジェクトのコピーは無料です。ただのポインタ。
ロバートグラント14

1
@MainMa答えてくれてありがとう、しかし私が理解している限り、ReadOnlyDictionaryは他の誰かが基礎となる辞書を変更しないという保証を与えない後で使用するために)。ReadOnlyDictionaryは、奇妙な名前空間System.Collections.ObjectModelでも宣言されています。
デン14

2
@Den:それは私の愛petの1つに関連しています。「読み取り専用」と「不変」を同義語と見なす人々です。オブジェクトが読み取り専用ラッパーにカプセル化され、他の参照が存在しないか、ユニバースのどこにも保持されていない場合、オブジェクトをラップすると不変になり、ラッパーへの参照は、その中に含まれるオブジェクト。ただし、コードがそうであるかどうかを確認できるメカニズムはありません。逆に、ラッパーがラップされたオブジェクトの種類、不変オブジェクトをラップ...隠すために
supercat

2
...結果のラッパーが安全に不変と見なされるかどうかをコードが知ることができなくなります。
supercat

13

不変性を使用するシングルスレッドの理由は数多くあります。例えば

オブジェクトAにはオブジェクトBが含まれます。

外部コードはオブジェクトBを照会し、それを返します。

次の3つの状況が考えられます。

  1. Bは不変で、問題ありません。
  2. Bは変更可能です。防御的なコピーを作成して返します。パフォーマンスは低下しましたが、リスクはありません。
  3. Bは変更可能です。返してください。

3番目のケースでは、ユーザーコードは、ユーザーが行ったことを認識せず、オブジェクトに変更を加える可能性があります。そうすることで、オブジェクトの内部データを変更します。


9

不変性は、ガベージコレクターの実装を大幅に簡素化することもできます。GHCのwikiから:

[...]データの不変性により、大量の一時データが生成されますが、このガベージを迅速に収集するのにも役立ちます。秘trickは、不変データが決して若い値を指すことはないということです。実際、古い値が作成された時点では、新しい値はまだ存在していないため、ゼロから指すことはできません。また、値は変更されないため、後で指定することもできません。これは不変データの重要なプロパティです。

これにより、ガベージコレクション(GC)が大幅に簡素化されます。いつでも最後に作成された値をスキャンし、同じセットからポイントされていない値を解放することができます(もちろん、実際の値の階層の実際のルートはスタック内にあります)。[...]そのため、直感に反する動作があります。値の大部分がゴミであるほど、動作が速くなります。[...]


5

KChalouxが非常によく要約したものを拡張して...

理想的には、2種類のフィールドがあるため、それらを使用する2種類のコードがあります。どちらのフィールドも不変であり、コードは可変性を考慮する必要はありません。またはフィールドが可変であり、スナップショット(int x = p.x)を取得するか、そのような変更を適切に処理するコードを記述する必要があります。

私の経験では、ほとんどのコードは楽観的なコードであり、最初の呼び出しp.xが2番目の呼び出しと同じ結果になると仮定して、可変データを自由に参照します。そして、ほとんどの場合、これは真実です。おっと。

だから、本当に、その質問を好転させる:この可変性を作るための私の理由は何ですか?

  • メモリの割り当て/解放を減らしますか?
  • 本質的に可変ですか?(例:カウンター)
  • 修飾子、水平ノイズを保存しますか?(定数/最終)
  • いくつかのコードを短く/簡単にしますか?(デフォルトの初期化、場合によっては上書き)

防御的なコードを書いていますか?不変性は、コピーを節約します。楽観的なコードを書いていますか?不変性は、その奇妙で不可能なバグの狂気を免れます。


3

不変性のもう1つの利点は、これらの不変オブジェクトをプールに切り上げる最初のステップであることです。次に、それらを管理して、同じものを概念的および意味的に表す複数のオブジェクトを作成しないようにします。良い例は、Javaの文字列です。

言語学ではよく知られている現象ですが、いくつかの単語が多く登場し、他の文脈にも登場する可能性があります。したがってString、複数のオブジェクトを作成する代わりに、1つの不変オブジェクトを使用できます。ただし、これらの不変オブジェクトを処理するには、プールマネージャーを保持する必要があります。

これにより、多くのメモリを節約できます。これも興味深い記事です:http : //en.wikipedia.org/wiki/Zipf%27s_law


1

Java、C#、およびその他の類似言語では、クラス型フィールドを使用してオブジェクトを識別したり、オブジェクトの値または状態をカプセル化したりできますが、言語ではそのような使用法を区別しません。クラスオブジェクトGeorgeにタイプのフィールドがあるとしますchar[] chars;。そのフィールドは、次のいずれかの文字シーケンスをカプセル化できます。

  1. 決して変更されず、それを変更する可能性のあるコードには公開されないが、外部参照が存在する可能性がある配列。

  2. 外部参照は存在しないが、ジョージが自由に変更できる配列。

  3. ジョージが所有しているが、ジョージの現在の状態を表すと予想される外部ビューが存在する可能性のある配列。

さらに、変数は、文字シーケンスをカプセル化する代わりに、ライブビューを他のオブジェクトが所有する文字シーケンスにカプセル化できます。

場合はchars、現在の文字列[風]をカプセル化し、ジョージは望んでいるchars文字シーケンス[杖]をカプセル化するために、ジョージは行うことができ、物事の数があります。

A.文字[wand]を含む新しい配列を作成しchars、古い配列ではなくその配列を識別するように変更します。

B.何らかの方法で文字[ワンド]を常に保持する既存の文字配列charsを識別し、古い配列ではなくその配列を識別するように変更します。

C.によって識別さcharsれる配列の2番目の文字をに変更しaます。

ケース1では、(A)と(B)は望ましい結果を達成するための安全な方法です。(2)、(A)、および(C)は安全であるが、(B)は[即時の問題を引き起こすことはありませんが、ジョージは配列の所有権を持っていると想定するため、自由に配列を変更できます]。(3)の場合、選択肢(A)と(B)は外部のビューを破壊するため、選択肢(C)のみが正しいです。したがって、フィールドによってカプセル化された文字シーケンスを変更する方法を知るには、フィールドのセマンティックタイプを知る必要があります。

char[]潜在的に変更可能な文字シーケンスをカプセル化するtypeのフィールドを使用する代わりに、コードがString不変の文字シーケンスをカプセル化するtypeを使用した場合、上記の問題はすべてなくなります。タイプのすべてのフィールドは、String変更されない共有可能なオブジェクトを使用して文字のシーケンスをカプセル化します。その結果、タイプのフィールドString「風」をカプセル化します。「ワンド」をカプセル化する唯一の方法は、「ワンド」を保持する別のオブジェクトを識別することです。コードがオブジェクトへの唯一の参照を保持する場合、オブジェクトを変更する方が新しいオブジェクトを作成するよりも効率的かもしれませんが、クラスが変更可能な場合はいつでも、値をカプセル化できるさまざまな方法を区別する必要があります。個人的には、Apps Hungarianをこれに使用すべきだったと思います(char[]型システムはそれらを同一と見なしますが、Apps Hungarianが輝いているような状況でも、4つの用途は意味的に異なる型であると考えます)。このようなあいまいさを回避する最も簡単な方法は、値を一方向にのみカプセル化する不変の型を設計することではありませんでした。


それは理にかなった答えのように見えますが、読んで理解するのは少し難しいです。
デン14

1

ここにはいくつかの良い例がありますが、不変性が非常に役立ついくつかの個人的な例に飛び込みたいと思いました。私の場合、主に重複する読み取りおよび書き込みと並行してコードを自信を持って実行でき、競合状態を心配することなく、不変の並行データ構造の設計を開始しました。ジョン・カーマックがそのようなアイデアについて話したところ、ジョン・カーマックが私にそれをやる気にさせた話がありました。これは非常に基本的な構造であり、次のように実装するのは非常に簡単です。

ここに画像の説明を入力してください

もちろん、一定の時間内に要素を削除し、回収可能な穴を残して、特定の不変のインスタンスに対してブロックが空になり、潜在的に解放されるとブロックの参照を取り消すことができるように、いくつかの追加機能があります。ただし、基本的に構造を変更するには、「一時的な」バージョンを変更し、変更を原子的にコミットして、古いバージョンに影響を与えない新しい不変のコピーを取得します。新しいバージョンでは、浅くコピーし、他をカウントしながら参照を一意にする必要があります。

しかし、私はそれを見つけられませんでしマルチスレッドの目的に役立ちます。結局のところ、たとえば、物理システムが物理を同時に適用しているときに、プレイヤーが世界の中で要素を動かそうとしているという概念上の問題があります。変換されたデータのどの不変のコピーを使いますか、プレイヤーが変換したものか、物理システムが変換したものですか?そのため、この基本的な概念的な問題に対する素敵で簡単な解決策は、スマートな方法でロックし、スレッドのストールを回避するためにバッファーの同じセクションへの読み取りと書き込みの重複を防ぐ可変データ構造を持つことを除いて、本当に見つかりませんでした。それはジョン・カーマックが彼のゲームでどのように解決するかをおそらく考え出したようです。少なくとも彼は、ワームの車を開けることなくほとんど解決策を見ることができるようにそれについて話します。私は彼に関してはその点に関しては得ていません。不変物の周りのすべてを並列化しようとした場合、私が見ることができるのは無限の設計質問です。私の努力の大部分は彼が投げ出したアイデアから始まったので、私は彼の脳を選ぶことに一日を費やすことができたらいいのにと思います。

それにもかかわらず、私は他の分野でこの不変のデータ構造の莫大な価値を発見しました。私は今でもそれを使って本当に奇妙な画像を保存し、ランダムアクセスにさらに命令を必要とします(右シフトと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つの操作で、ここにいくつかの頂点を移動するだけの場合があります。別の人は、そこにいくつかのポリゴンを再分割したいだけかもしれません。ここで、不変のデータ構造は、メッシュの一部を変更した新しいバージョンを返すためにメッシュ全体のコピー全体を作成する必要性を回避するのに役立ちます。

副作用の最小化

これらの構造が手元にあれば、パフォーマンスを大幅に低下させることなく、副作用を最小限に抑える関数を簡単に作成できます。少し無駄に思えても、副作用を引き起こすことなく、不変のデータ構造全体を値で返す関数をますます多く書いていることに気づきました。

たとえば、通常、多数の位置を変換する誘惑は、マトリックスとオブジェクトのリストを受け入れ、それらを可変的な方法で変換することです。最近では、オブジェクトの新しいリストを返すだけです。

副作用を引き起こさないこのような関数がシステムにある場合、その正確性についての推論や正確性のテストが間違いなく容易になります。

安価なコピーの利点

とにかく、これらは不変のデータ構造(または永続的なデータ構造)の中で最も使用が多い領域です。また、最初は少し熱心になり、不変のツリーと不変のリンクリストと不変のハッシュテーブルを作成しましたが、時間が経つにつれてそれらの用途がめったに見つかりませんでした。私は主に、上の図で、チャンキーで不変の配列のようなコンテナの使用が最も多いことを発見しました。

私はまだミュータブルで動作する多くのコードも持っています(少なくとも低レベルのコードでは実際に必要です)が、主なアプリケーションの状態は不変の階層であり、不変のシーンから内部の不変のコンポーネントにドリルダウンします。一部の安価なコンポーネントはまだ完全にコピーされますが、メッシュや画像などの最も高価なコンポーネントは不変の構造を使用して、変換が必要な部分のみの部分的に安価なコピーを許可します。


0

すでに多くの良い答えがあります。これは、.NETに特有の追加情報にすぎません。私は古い.NETブログの投稿を掘り下げてみたところ、Microsoft Immutable Collections開発者の観点から見た利点の素晴らしい要約を見つけました。

  1. スナップショットのセマンティクス。受信者が決して変化しないことを期待できる方法でコレクションを共有できます。

  2. マルチスレッドアプリケーションでの暗黙的なスレッドセーフ(コレクションへのアクセスにロックは不要)。

  3. コレクション型を受け入れるか返すクラスメンバーがあり、読み取り専用のセマンティクスをコントラクトに含める場合。

  4. 機能的なプログラミングに優しい。

  5. 元のコレクションが変更されないようにしながら、列挙中にコレクションの変更を許可します。

  6. これらは、コードがすでに処理しているIReadOnly *インターフェイスと同じものを実装しているため、移行は簡単です。

誰かがReadOnlyCollection、IReadOnlyList、またはIEnumerableを渡した場合、唯一の保証はデータを変更できないことです。コレクションを渡した人がそれを変更しないという保証はありません。しかし、あなたはしばしば、それが変わらないというある程度の自信が必要です。これらのタイプは、コンテンツが変更されたときに通知するイベントを提供しません。変更された場合、おそらくそのコンテンツを列挙している間に別のスレッドで発生する可能性がありますか?このような動作は、アプリケーションのデータの破損やランダムな例外を引き起こす可能性があります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.