web-dev-qa-db-ja.com

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

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

メソッドが文字列のディクショナリ(C#では不変)を変更しないことを確認したい状況があります。できるだけ制約を与えたいです。

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

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

53
Den

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

私は正直に、物事を不変にするデフォルトでにし、それが必要であることが証明されているところで変更可能にすることを好むでしょう、これはあなたがパフォーマンスを必要とすることを意味するか、あなたが持っているアルゴリズムが意味をなさないかを意味します不変性のため。

101
KChaloux

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

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

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

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

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

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

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

  • 並行性

ちなみに、あなたの場合、サードパーティのパッケージは必要ありません。 .NET Frameworkにはすでに ReadOnlyDictionary<TKey, TValue> クラス。

22

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

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

外部コードがオブジェクトBをクエリし、それを返します。

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

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

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

13
Tim B

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

[...]データの不変性により、多くの一時データが生成されますが、このガベージを迅速に収集することにも役立ちます。秘訣は、不変データが決して新しい値を指さないことです。実際、古い値が作成された時点では、新しい値はまだ存在していないため、ゼロからポイントすることはできません。また、値は変更されないため、後で参照することもできません。これは不変データの重要な特性です。

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

9
Petr Pudlák

何を拡張するか KChaloux 非常によく要約されています...

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

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

だから、本当に、その質問を好転させます:これを変更可能にする理由は何ですか

  • メモリの割り当て/解放を減らしますか?
  • 本質的に可変ですか? (例:カウンター)
  • モディファイア、水平ノイズを保存しますか? (const/final)
  • 一部のコードを短く/簡単にしますか? (初期デフォルト、後で上書きされる可能性があります)

防御的なコードを書いていますか?不変性により、コピーの手間が省けます。楽観的なコードを書いていますか?不変性は、その奇妙で不可能なバグの狂気を免れます。

5
JvR

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

いくつかの単語が頻繁に表示され、他のコンテキストでも表示されることは、言語学ではよく知られている現象です。したがって、複数のStringオブジェクトを作成する代わりに、1つの不変オブジェクトを使用できます。ただし、これらの不変オブジェクトを処理するには、プールマネージャーを保持する必要があります。

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

3
InformedA

ここにいくつかの素晴らしい例がありますが、私は不変がトンを助けたいくつかの個人的な例に飛び込みたかったです。私の場合、主に、不変の並行データ構造の設計から始めました。主に、読み取りと書き込みのオーバーラップと並行して自信を持ってコードを実行でき、競合状態を心配する必要がないことを期待しています。ジョン・カーマックがそのようなアイデアを私に与えてくれた話がありました。そこで彼はそのようなアイデアについて話しました。これはかなり基本的な構造であり、次のように実装するのはかなり簡単です。

enter image description here

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

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

それにもかかわらず、他の領域でこの不変データ構造の巨大値を見つけました。私は今でもそれを使用して、本当に奇妙でランダムアクセスにいくつかの追加の指示(右シフトとビット単位の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メガバイトにすぎません。さらに、必要に応じて、将来的にそれを押しつぶすこともできます。

Exception-Safety

複雑なアプリケーションでの例外安全性は重要な問題ではありません。ただし、アプリケーションの状態が不変であり、一時的なオブジェクトを使用してアトミック変更トランザクションをコミットしようとしている場合、コードの一部がスローされた場合、新しい不変のコピーを与える前に一時的に破棄されるため、本質的に例外セーフです。 。そのため、複雑なC++コードベースで正しく機能するために私がいつも見つけてきた最も困難なことの1つが簡単になります。

多くの場合、C++でRAII準拠のリソースを使用しているだけで、例外に対して安全であると考える人が多すぎます。多くの場合、そうではありません。関数は通常、そのスコープに対してローカルな状態を超えて、状態に副作用を引き起こす可能性があるためです。通常、これらのケースでは、スコープガードと高度なロールバックロジックの処理を開始する必要があります。このデータ構造はそれを作ったので、関数が副作用を引き起こしていないので、私はしばしばそれを気にする必要はありません。アプリケーションの状態を変換するのではなく、変換されたアプリケーションの状態の不変のコピーを返します。

非破壊編集

enter image description here

非破壊編集とは、基本的に、元のユーザーのデータに触れることなく操作を重ねる/重ねる/接続することです(入力に触れることなく、データを入力して出力するだけです)。多くの操作では画像全体のすべてのピクセルを変換したいだけなので、Photoshopのような単純な画像アプリケーションで実装するのは通常は簡単で、このデータ構造からそれほどメリットを得られない可能性があります。

ただし、非破壊メッシュ編集などでは、多くの操作でメッシュの一部のみを変換したい場合がよくあります。 1つの操作で、いくつかの頂点をここに移動したい場合があります。別の人は、いくつかのポリゴンをそこで細分割したいだけかもしれません。ここで、不変のデータ構造は、メッシュの一部が変更された新しいバージョンのメッシュを返すためだけにメッシュ全体のコピー全体を作成する必要をなくすのに役立ちます。

副作用の最小化

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

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

副作用を引き起こさないこのような機能がシステムにもっとある場合、それが間違いなくその正当性を推論し、その正当性をテストすることを容易にします。

安価なコピーの利点

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

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

1
user204677

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

  1. 変更されることも、変更される可能性のあるコードに公開されることもないが、外部参照が存在する可能性がある配列。

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

  3. Georgeが所有する配列ですが、Georgeの現在の状態を表すと予想される外部ビューが存在する可能性があります。

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

現在charsが文字シーケンス[w i n d]をカプセル化しており、Georgeがcharsに文字シーケンス[w a n d]をカプセル化することを希望している場合、Georgeが実行できることはいくつかあります。

A.文字[w a n d]を含む新しい配列を作成し、charsを変更して、古い配列ではなくその配列を識別します。

B.どういうわけか、常に文字[w a n d]を保持する既存の文字配列を特定し、charsを変更して、古い配列ではなくその配列を特定します。

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

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

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

1
supercat

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

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

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

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

  4. 関数型プログラミングに対応。

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

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

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

0
Den