質問
私は可変オブジェクトと不変オブジェクトについて理解しようとしています。可変オブジェクトの使用は多くの悪い評判を受けます (例:メソッドから文字列の配列を返します)が、これによる悪影響が何であるかを理解するのが困難です。可変オブジェクトの使用に関するベスト プラクティスは何ですか?可能な限りそれらを避けるべきでしょうか?
解決
まあ、これにはいくつかの側面があります。第一に、参照IDを持たない可変オブジェクトは、奇妙な場合にバグを引き起こす可能性があります。たとえば、値ベースの equals
メソッドを持つ Person
Beanを考えてみましょう。
Map<Person, String> map = ...
Person p = new Person();
map.put(p, "Hey, there!");
p.setName("Daniel");
map.get(p); // => null
Person
インスタンスは「失われ」ます; hashCode
であり、等価性が変更可能な値に基づいていたため、キーとして使用されたときのマップ内。これらの値はマップ外で変更され、すべてのハッシュは廃止されました。理論家はこの点をハープしたいのですが、実際にはそれがあまりにも大きな問題だとは思いません。
もう1つの側面は、論理的な「合理性」です。あなたのコードの。これは定義が難しい用語であり、読みやすさからフローまですべてを網羅しています。一般的に、コードを見て、それが何をするのかを簡単に理解できるはずです。しかし、それよりも重要なことは、それが正しく行うことを行うことを自分自身に納得させることができるはずです。オブジェクトが異なるコード「ドメイン」間で独立して変更できる場合、何がどこに、なぜあるのかを追跡することが困難になる場合があります(「離れた場所での不審なアクション」)。これは例示するのがより難しい概念ですが、より大きく、より複雑なアーキテクチャでしばしば直面するものです。
最後に、可変オブジェクトは同時状況では killer です。個別のスレッドから可変オブジェクトにアクセスするときは常に、ロックを処理する必要があります。これにより、スループットが低下し、コードの保守が劇的に 難しくなります。十分に複雑なシステムでは、この問題があまりにも不均衡に吹き飛ばされるため、保守がほぼ不可能になります(同時実行の専門家であっても)。
不変オブジェクト(特に不変コレクション)は、これらの問題をすべて回避します。それらがどのように機能するかを理解すると、コードは読みやすく、保守しやすく、奇妙で予測不可能な方法で失敗しにくいものになります。不変オブジェクトは、モックが容易であるだけでなく、強制する傾向があるコードパターンもあるため、テストがさらに簡単です。要するに、それらはあらゆる点で優れた実践です!
とはいえ、私はこの問題に関してほとんど熱狂的ではありません。すべてが不変である場合、一部の問題はうまくモデル化されません。しかし、もちろんこれを受け入れがたい意見にする言語を使用していると仮定して、できるだけ多くのコードをその方向にプッシュしようとするべきだと思います(C / C ++はこれを非常に難しくしますが、Javaもそうです) 。要するに、利点はあなたの問題にいくらか依存しますが、私は不変性を好む傾向があります。
他のヒント
不変オブジェクトと不変コレクション
可変オブジェクトと不変オブジェクトをめぐる議論の細かい点の1つは、不変の概念をコレクションに拡張する可能性です。不変オブジェクトは、多くの場合、データの単一の論理構造(不変文字列など)を表すオブジェクトです。不変オブジェクトへの参照がある場合、オブジェクトの内容は変わりません。
不変コレクションは、決して変更されないコレクションです。
変更可能なコレクションに対して操作を実行すると、コレクションがその場で変更され、コレクションへの参照を持つすべてのエンティティに変更が反映されます。
不変のコレクションで操作を実行すると、変更が反映された新しいコレクションへの参照が返されます。コレクションの以前のバージョンへの参照を持つすべてのエンティティには、変更は表示されません。
賢い実装は、その不変性を提供するためにコレクション全体をコピー(クローン)する必要は必ずしもありません。最も単純な例は、単一リンクリストとして実装されたスタックとプッシュ/ポップ操作です。新しいコレクションで以前のコレクションのすべてのノードを再利用し、プッシュ用に1つのノードのみを追加し、ポップ用にノードを複製しないでください。一方、単一リンクリストのpush_tail操作はそれほど単純でも効率的でもありません。
不変と可変の変数/参照
一部の関数型言語は、オブジェクト参照自体に不変性の概念を取り入れており、単一の参照割り当てのみを許可しています。
- Erlangでは、これはすべての「変数」に当てはまります。オブジェクトを参照に割り当てることができるのは一度だけです。コレクションを操作する場合、新しいコレクションを古い参照(変数名)に再割り当てすることはできません。
- Scalaはこれを言語に組み込み、すべての参照を var または val で宣言します。valsは単一の割り当てであり、機能的なスタイルを促進しますが、 cライクまたはjavaライクなプログラム構造。
- var / val宣言が必要ですが、多くの従来の言語では、javaの final やc。の const などのオプションの修飾子を使用しています。
開発の容易さ対パフォーマンス
不変オブジェクトを使用するほとんどの理由は、副作用のないプログラミングとコードに関する単純な推論を促進するためです(特に、高度な並行/並列環境)。オブジェクトが不変である場合、基礎となるデータが別のエンティティによって変更されることを心配する必要はありません。
主な欠点はパフォーマンスです。 Javaで行った簡単なテストで、おもちゃの問題で不変オブジェクトと可変オブジェクトを比較した記事を次に示します。 。
パフォーマンスの問題は多くのアプリケーションで重要ではありませんが、すべてではありません。そのため、PythonのNumpy Arrayクラスなどの多くの大きな数値パッケージでは、大きな配列のインプレース更新が許可されます。これは、大きな行列演算とベクトル演算を使用するアプリケーション分野にとって重要です。これらの大規模なデータ並列および計算集約的な問題は、適切に動作することで大幅な高速化を実現します。
このブログ投稿を確認してください: http://www.yegor256.com/ 2014/06/09 / objects-should-be-immutable.html 。不変オブジェクトが可変より優れている理由を説明します。要するに:
- 不変オブジェクトは、構築、テスト、使用がより簡単です
- 真に不変なオブジェクトは常にスレッドセーフです
- 時間的カップリングを回避するのに役立ちます
- それらの使用には副作用がありません(防御的なコピーはありません)
- アイデンティティの可変性の問題は回避されます
- 常に失敗の原子性があります
- キャッシュがはるかに簡単です
不変オブジェクトは非常に強力な概念です。これらは、すべてのクライアントに対してオブジェクト/変数の一貫性を維持しようとする負担の多くを軽減します。
これらは、主に値セマンティクスで使用される、CPoint クラスなどの低レベルの非ポリモーフィック オブジェクトに使用できます。
または、数学関数を表す IFunction など、オブジェクト セマンティクスでのみ使用される高レベルのポリモーフィック インターフェイスにこれらを使用することもできます。
最大の利点:不変性 + オブジェクト セマンティクス + スマート ポインターにより、オブジェクトの所有権は問題になりません。オブジェクトのすべてのクライアントは、デフォルトで独自のプライベート コピーを持ちます。これは暗黙的に、並行性が存在する場合の決定的な動作も意味します。
不利益 :大量のデータを含むオブジェクトで使用すると、メモリ消費が問題になる可能性があります。これに対する解決策は、オブジェクトに対する操作をシンボリックに保ち、遅延評価を行うことです。ただし、インターフェイスがシンボリック操作に対応するように設計されていない場合、これによりシンボリック計算の連鎖が発生し、パフォーマンスに悪影響を及ぼす可能性があります。この場合に絶対に避けなければならないのは、メソッドから大量のメモリを返すことです。連鎖されたシンボリック操作と組み合わせると、大量のメモリ消費とパフォーマンスの低下につながる可能性があります。
したがって、不変オブジェクトは間違いなくオブジェクト指向設計についての私の主な考え方ですが、定説ではありません。これらはオブジェクトのクライアントにとって多くの問題を解決しますが、特に実装者にとっては多くの問題も生み出します。
話している言語を指定する必要があります。 CやC ++のような低レベル言語では、可変オブジェクトを使用してスペースを節約し、メモリチャーンを減らします。高レベル言語では、不変のオブジェクトにより、「遠くでの不気味なアクション」がないため、コード(特にマルチスレッドコード)の動作について推論しやすくなります。
可変オブジェクトは、作成/インスタンス化後に変更できるオブジェクトと、変更できない不変オブジェクトです(件名のウィキペディアページ)。プログラミング言語でのこの例は、Pythonリストとタプルです。リストは変更できます(たとえば、作成後に新しいアイテムを追加できます)が、タプルはできません。
すべての状況でどちらが良いかについて、明確な答えがあるとは本当に思いません。どちらにも場所があります。
クラスタイプが変更可能な場合、そのクラスタイプの変数にはさまざまな意味があります。たとえば、オブジェクト foo
にフィールド int [] arr
があり、数字を保持する int [3]
への参照を保持するとします。 {5、7、9}。フィールドのタイプはわかっていますが、少なくとも4つの異なる表現があります:
-
潜在的に共有される参照。すべての所有者は、値5、7、および9をカプセル化することだけを気にします。
foo
がarr
をカプセル化する場合値が異なる場合は、目的の値を含む別の配列に置き換える必要があります。foo
のコピーを作成する場合は、コピーにarr
への参照、または値{1,2,3}を保持する新しい配列のいずれかを与えることができます。より便利です。 -
値5、7、および9をカプセル化する配列への唯一の参照は、ユニバースのどこにでもあります。現時点で値5、7、および9を保持する3つの格納場所のセット。
foo
が値5、8、および9をカプセル化する場合、その配列の2番目の項目を変更するか、値5、8、および9を保持して古い配列を破棄する新しい配列を作成します。 1。foo
のコピーを作成する場合は、コピーでarr
をfoo.arr <の新しい配列への参照に置き換える必要があることに注意してください。 / code>を使用して、ユニバース内のその配列への唯一の参照として残します。
-
何らかの理由で
foo
に公開した other オブジェクトが所有する配列への参照(たとえばfoo
にデータを保存します)。このシナリオでは、arr
は配列の内容をカプセル化するのではなく、アイデンティティをカプセル化します。arr
を新しい配列への参照で置き換えるとその意味が完全に変わるため、foo
のコピーは同じ配列への参照を保持する必要があります。 -
foo
が唯一の所有者であるが、何らかの理由で参照が他のオブジェクトに保持されている配列への参照(たとえば、他のオブジェクトにデータを格納させたい場合)そこに-前のケースの裏返し)。このシナリオでは、arr
は配列のIDとその内容の両方をカプセル化します。arr
を新しい配列への参照に置き換えると、その意味は完全に変わりますが、クローンのarr
がfoo.arr
を参照すると、仮定に違反しますfoo
が唯一の所有者です。したがって、foo
をコピーする方法はありません。
理論的には、 int []
は明確で明確な型である必要がありますが、4つの非常に異なる意味があります。対照的に、不変オブジェクト(たとえば、 String
)への参照には、通常1つの意味しかありません。 「パワー」の多くは不変のオブジェクトはその事実に由来します。
配列または文字列の参照を返す場合、外部はそのオブジェクトのコンテンツを変更できるため、変更可能な(変更可能な)オブジェクトとして作成できます。
不変は変更できないことを意味し、不変は変更できることを意味します。
オブジェクトは、Javaのプリミティブとは異なります。プリミティブは型(boolean、intなど)に組み込まれ、オブジェクト(クラス)はユーザー作成型です。
プリミティブおよびオブジェクトは、クラスの実装内でメンバー変数として定義されている場合、可変または不変である可能性があります。
多くの人々は、プリミティブやオブジェクト変数の前に最終修飾子が付いているオブジェクト変数は不変であると考えていますが、これは必ずしも正しくありません。したがって、finalは変数に対して不変を意味することはほとんどありません。こちらの例をご覧ください
http://www.siteconsortium.com/h/D0000F.php 。
可変性 インスタンスは参照渡しされます。
不変 インスタンスは値で渡されます。
抽象的な例。 HDDに txtfile という名前のファイルが存在するとします。さて、 txtfile を私に尋ねると、2つのモードで返すことができます:
- txtfile へのショートカットとあなたへのpasショートカットを作成する、または
- txtfile のコピーを作成し、pasコピーを送信します。
最初のモードで返される txtfile は変更可能なファイルです。ショートカットファイルを変更すると、元のファイルも変更されるためです。このモードの利点は、返されたショートカットごとに必要なメモリ(RAMまたはHDD)が少なくなることと、欠点は、ファイルコンテンツを変更するためのアクセス許可がすべてのユーザー(私だけでなく所有者)にあることです。
2番目のモードでは、返された txtfile は不変ファイルです。これは、受信したファイルのすべての変更が元のファイルを参照していないためです。このモードの利点は、自分(所有者)だけが元のファイルを変更できることであり、欠点は、返される各コピーがメモリ(RAMまたはHDD)を必要とすることです。