完全な不変性とオブジェクト指向プログラミング


43

ほとんどのOOP言語では、オブジェクトは通常、限られた例外セット(たとえば、Pythonのタプルや文字列など)を使用して変更可能です。ほとんどの関数型言語では、データは不変です。

可変オブジェクトと不変オブジェクトの両方が、独自の長所と短所の完全なリストをもたらします。

たとえば、(明示的に宣言された)可変データと不変データがあるscalaのような両方の概念を結合しようとする言語があります(間違っている場合は修正してください、scalaの知識は限られています)。

私の質問は次のとおりです完全な(原文のまま!)不変性-つまり、オブジェクトが作成された後はオブジェクトを変更できません-は、OOPコンテキストで意味をなしますか?

そのようなモデルの設計または実装はありますか?

基本的に、(完全な)不変性とOOPは反対ですか、それとも直交ですか?

動機:OOPでは通常データを操作、基礎となる情報を変更(変更)し、それらのオブジェクト間の参照を保持します。たとえば、別のオブジェクトを参照Personするメンバーを持つクラスのオブジェクト。父親の名前を変更すると、更新の必要なく、これはすぐに子オブジェクトに表示されます。不変なので、父と子の両方に新しいオブジェクトを構築する必要があります。ただし、共有オブジェクト、マルチスレッド、GILなどを使用すると、kerfuffleがはるかに少なくなります。fatherPerson


2
不変は、オブジェクトアクセスポイントをメソッドまたはデータを変更しない読み取り専用プロパティとして公開するだけで、OOP言語でシミュレートできます。不変性は、関数型言語の機能が欠落している可能性があることを除いて、OOP言語でも関数型言語と同じように機能します。
ロバートハーヴェイ14年

4
可変性は、C#やJavaなどのOOP言語のプロパティではなく、不変性でもありません。クラスの記述方法によって、可変性または不変性を指定します。
ロバートハーヴェイ14年

9
あなたの推測では、可変性はオブジェクト指向の中心的な特徴であると思われます。そうではありません。可変性は、オブジェクトまたは値のプロパティです。オブジェクト指向には、突然変異とほとんどまたはまったく関係のない多くの固有の概念(カプセル化、ポリモーフィズム、継承など)が含まれますが、これらの機能の利点は引き続き得られます。すべてを不変にしたとしても。
ロバートハーヴェイ14年

2
@MichaelT問題は、特定のものを可変にすることではなく、すべてのものを不変にすることです。

回答:


43

OOPと不変性は、ほぼ完全に直交しています。ただし、命令型プログラミングと不変性はそうではありません。

OOPは、2つのコア機能によって要約できます。

  • カプセル化:オブジェクトのコンテンツに直接アクセスするのではなく、特定のインターフェイス(「メソッド」)を介してこのオブジェクトと通信します。このインターフェイスは、内部データを隠すことができます。技術的には、これはOOPではなくモジュラープログラミングに固有です。定義されたインターフェースを介したデータへのアクセスは、抽象データ型とほぼ同等です。

  • 動的ディスパッチ:オブジェクトのメソッドを呼び出すと、実行されたメソッドは実行時に解決されます。(たとえば、クラスベースのOOPではsizeIListインスタンスのメソッドを呼び出すことがありますが、呼び出しはLinkedListクラスの実装に解決される場合があります)。動的ディスパッチは、多態的な動作を許可する1つの方法です。

カプセル化は、可変性なしでは意味がありません(外部の干渉により破損する可能性のある内部状態はありません)が、すべてが不変であっても抽象化を容易にする傾向があります。

命令型プログラムは、順番に実行されるステートメントで構成されます。ステートメントには、プログラムの状態を変更するなどの副作用があります。不変性では、状態を変更できません(もちろん、新しい状態を作成できます)。したがって、命令型プログラミングは基本的に不変性と互換性がありません。

OOPは歴史的に常に命令型プログラミングに関連付けられており(SimulaはAlgolに基づいています)、すべての主流のOOP言語には命令型のルートがあります(C ++、Java、C#などはすべてCに根ざしています)。これは、OOP自体が命令型または可変であることを意味するのではなく、これらの言語によるOOPの実装により可変性が許可されることを意味します。


2
特に、2つのコア機能の定義に感謝します。
ハイパーボレウス14年

Dynamic DispatchはOOPのコア機能ではありません。どちらも実際にはカプセル化ではありません(既に認めたとおり)。
停止ハーミングモニカ14年

5
@OrangeDogはい、普遍的に受け入れられているOOPの定義はありませんが、使用する定義が必要でした。それで、私はそれについて完全な論文を書かずに得ることができる限り真実に近いものを選びました。ただし、動的ディスパッチは、OOPを他のパラダイムと区別する単一の主要な機能と見なしています。OOPのように見えるが、実際にはすべての呼び出しが静的に解決されるものは、実際にはアドホックなポリモーフィズムを備えたモジュール式プログラミングです。オブジェクトはメソッドとデータのペアであり、クロージャーと同等です。
アモン14年

2
「オブジェクトとはメソッドとデータの組み合わせです」ということです。必要なのは、不変性とは関係のないものだけです。
停止ハーミングモニカ14年

3
@CodeYogiデータ隠蔽は、最も一般的な種類のカプセル化です。ただし、非表示にする必要がある実装の詳細は、オブジェクトによってデータが内部的に保存される方法だけではありません。同様に、パブリックインターフェイスの実装方法、たとえばヘルパーメソッドを使用するかどうかを非表示にすることも重要です。このようなヘルパーメソッドも、一般的に言えばプライベートでなければなりません。要約すると、カプセル化は原則であり、データ隠蔽はカプセル化技術です。
アモン

25

オブジェクト指向プログラマーの間には、OOPを実行している場合、ほとんどのオブジェクトは変更可能であると想定する文化がありますが、OOPに可変性が必要かどうかは別の問題です。また、その文化は、人々が関数型プログラミングにさらされているため、より不変性に向かってゆっくりと変化しているようです。

Scalaは、オブジェクト指向には可変性は必要ないという実に良い例です。Scala 可変性をサポートしていますが、その使用は推奨されていません。慣用的なScalaは非常にオブジェクト指向であり、ほぼ完全に不変です。ほとんどの場合、Javaとの互換性のために可変性が可能になります。特定の状況では、不変オブジェクトは非効率であるか、複雑に動作するためです。

たとえば、ScalaリストJavaリストを比較します。Scalaの不変リストには、Javaの可変リストと同じオブジェクトメソッドがすべて含まれています。実際、Javaはsortなどの操作に静的関数を使用し、Scalaはのような機能スタイルのメソッドを追加するためですmap。OOPの特徴であるカプセル化、継承、およびポリモーフィズムはすべて、オブジェクト指向プログラマーに馴染みのある形式で利用でき、適切に使用されます。

表示される唯一の違いは、リストを変更すると、結果として新しいオブジェクトが取得されることです。多くの場合、可変オブジェクトを使用する場合とは異なる設計パターンを使用する必要がありますが、OOPを完全に放棄する必要はありません。


17

不変は、オブジェクトアクセスポイントをメソッドまたはデータを変更しない読み取り専用プロパティとして公開するだけで、OOP言語でシミュレートできます。不変性は、関数型言語の機能が欠落している可能性があることを除いて、OOP言語でも関数型言語と同じように機能します。

あなたの推定では、可変性はオブジェクト指向のコア機能であると思われます。しかし、可変性はオブジェクトまたは値のプロパティです。オブジェクト指向には、突然変異とほとんどまたはまったく関係のない多くの固有の概念(カプセル化、ポリモーフィズム、継承など)が含まれます。

すべての関数型言語が不変性を必要とするわけでもありません。Clojureには、型を変更可能にする特定の注釈があり、ほとんどの「実用的な」関数型言語には、変更可能な型を指定する方法があります。

質問すべきより良い質問は、命令型プログラミングでは完全な不変性が意味をなすか?」 その質問に対する明白な答えはノーだと思います。命令型プログラミングで完全な不変性を実現するには、forループのようなもの(ループ変数を変更する必要があるため)を控えて、再帰を優先しなければなりません。


ありがとうございました。最後の段落について少し詳しく説明してください(「自明」は少し主観的かもしれません)。
ハイパーボレウス14年

すでに....やった
ロバート・ハーヴェイ

1
@Hyperboreus ポリモーフィズムを実現する方法はたくさんあります。ダイナミックディスパッチ、静的アドホックポリモーフィズム(別名、関数のオーバーロード)およびパラメトリックポリモーフィズム(別名ジェネリック)を使用したサブタイプは、これを行う最も一般的な方法であり、すべての方法には長所と短所があります。最近のOOP言語はこれら3つの方法すべてを組み合わせていますが、Haskellは主にパラメトリック多型とアドホック多型に依存しています。
アモン14年

3
@RobertHarveyループする必要があるため、可変性が必要だと言います(そうでなければ、再帰を使用する必要があります)。2年前にHaskellを使い始める前に、可変変数も必要だと思っていました。「ループ」する他の方法(マップ、フォールド、フィルターなど)があると言っています。テーブルからループを取り去った後、なぜ可変変数が必要なのでしょうか?
cimmanon 14年

1
@RobertHarveyしかし、これはまさにプログラミング言語のポイントです。あなたに公開されているものであり、内部で起こっていることではありません。後者は、アプリケーション開発者ではなく、コンパイラーまたはインタープリターの責任です。それ以外の場合は、アセンブラーに戻ります。
ハイパーボレウス14年

5

オブジェクトを値またはエンティティをカプセル化するものとして分類すると便利なことがよくありますが、何かが値である場合、その参照を保持するコードは、コード自体が開始しなかった方法でその状態の変化を見ることはありません。対照的に、エンティティへの参照を保持するコードは、参照所有者の制御を超える方法でエンティティが変更されることを期待する場合があります。

可変型または不変型のオブジェクトを使用してカプセル化された値を使用することは可能ですが、オブジェクトは、次の条件の少なくとも1つが当てはまる場合にのみ値として動作できます。

  1. オブジェクトへの参照は、その中にカプセル化された状態を変更する可能性のあるものにさらされることはありません。

  2. オブジェクトへの参照の少なくとも1つの所有者は、既存の参照が置かれる可能性のあるすべての用途を知っています。

不変型のすべてのインスタンスは最初の要件を自動的に満たすため、それらを値として使用するのは簡単です。対照的に、可変タイプを使用するときにどちらかの要件が満たされるようにすることは、はるかに困難です。不変型への参照は、その中にカプセル化された状態をカプセル化する手段として自由に渡すことができますが、可変型に格納された状態を渡すには、不変のラッパーオブジェクトを構築するか、私有オブジェクトによってカプセル化された状態を他のオブジェクトにコピーする必要がありますデータの受信者によって提供または構築されます。

不変の型は値を渡すために非常にうまく機能し、多くの場合、値を操作するために少なくともある程度使用できます。ただし、エンティティの処理にはあまり適していません。純粋に不変の型を持つシステムのエンティティに最も近いものは、システムの状態が与えられると、その一部の属性を報告するか、または次のような新しいシステム状態インスタンスを生成する関数です選択可能な方法で異なる特定の部分を除いて提供されます。さらに、エンティティの目的が、現実の世界に存在する何かに何らかのコードをインターフェイスさせることである場合、エンティティが可変状態の公開を回避することは不可能かもしれません。

たとえば、TCP接続を介してデータを受信した場合、古い「世界の状態」への参照に影響を与えることなく、そのデータをバッファに含む新しい「世界の状態」オブジェクトを作成できますが、データの最後のバッチを含まない世界の状態は欠陥があり、実際のTCPソケットの状態と一致しなくなるため、使用しないでください。


4

C#では、一部の型は文字列のように不変です。

これは、選択が強く検討されたことをさらに示唆しているようです。

確かに、そのタイプを数十万回変更する必要がある場合、不変のタイプを使用することは本当にパフォーマンスを要求します。そのため、この場合はStringBuilderクラスの代わりにクラスを使用することをお勧めしますstring

私はプロファイラーで実験を行いましたが、不変タイプを使用すると、実際にはより多くのCPUとRAMが必要になります。

4000文字の文字列の1文字だけを変更する場合、RAMの別の領域にあるすべての文字をコピーする必要があることを考慮すると、直感的です。


6
不変のデータを頻繁に変更することは、string連結の繰り返しのように壊滅的なほど遅くする必要はありません。事実上すべての種類のデータ/ユースケースについて、効率的な永続構造を作成できます(多くの場合、既に作成されています)。これらのほとんどは、一定の要因がより悪い場合でも、ほぼ同等のパフォーマンスを発揮します。

@delnanまた、答えの最後の段落は、(im)mutabilityよりも実装の詳細に関するものだと思います。
ハイパーボレウス14年

@Hyperboreus:その部分を削除する必要があると思いますか?しかし、文字列が不変である場合、どのように変更できますか?私の意見では..しかし、間違いなく間違っている可能性があり、それがオブジェクトが不変ではない主な理由かもしれません。
14年

1
@Revious決して。そのままにしておくと、議論やより興味深い意見や視点が生まれます。
ハイパーボレウス14年

1
@Reviousはい、読み取りは遅くなりますが、a string(従来の表現)を変更するほど遅くはありません。1000個の変更後の「文字列」(私が話している表現)は、新しく作成された文字列(モジュロコンテンツ)のようになります。X操作後に品質が低下する有用な、または広く使用されている永続的なデータ構造はありません。メモリの断片化は深刻な問題ではありません(多くの割り当てがあるでしょう、はい、しかし断片化は現代のガベージコレクターでは問題ではありません)

0

すべての完全な不変性は、1つの非常に大きな理由で、OOPやその他のほとんどのパラダイムではあまり意味がありません。

すべての有用なプログラムには副作用があります。

何も変更しないプログラムは価値がありません。効果は同じなので、実行しなくてもかまいません。

何も変更していないと思っていて、受け取った数字のリストを単純に要約している場合でも、結果を使って何かを行う必要があることを考慮してください。標準出力に出力するか、ファイルに書き込むか、またはどこでも。そして、バッファを変更し、システムの状態を変更する必要があります。

変更できるようにする必要がある部分だけに可変性を制限することは非常に理にかなっています。しかし、変更する必要がまったくない場合、実行する価値のあることは何もしていません。


4
私は純粋な関数型言語に取り組んでいなかったので、あなたの答えが質問にどのように関係しているかはわかりません。アーランを例に取りましょう。不変データ、破壊的な割り当て、副作用への影響はありません。また、状態で機能する関数とは異なり、状態が関数を「流れる」という関数言語の状態があります。状態は変化しますが、その場では変化しませんが、現在の状態は将来の状態に置き換わります。不変性とは、メモリバッファが変更されたかどうかではなく、これらの変異が外部から見えるかどうかです。
ハイパーボレウス14年

そして、将来の状態は現在の状態をどのように置き換えますか?OOプログラムでは、その状態はどこかのオブジェクトのプロパティです。状態を置き換えるには、オブジェクトを変更する必要があります(または、オブジェクトを別のオブジェクトに置き換えるには、それを参照するオブジェクトを変更する必要があります(または別のオブジェクトに置き換える必要があります)。何らかの種類のモナドハックを思いつくかもしれません。すべてのアクションが完全に新しいアプリケーションを作成することになりますが、それでもプログラムの現在の状態をどこかに記録する必要があります。
cHao 14年

7
-1。これは間違っています。副作用と突然変異を混同していて、関数型言語では同じように扱われることが多いのですが、違いがあります。すべての有用なプログラムには副作用があります。すべての有用なプログラムに突然変異があるわけではありません。
マイケルショー14年

@Michael:OOPに関しては、突然変異と副作用が絡み合っているため、現実的に分離することはできません。突然変異がない場合、大量のハッカーなしでは副作用はありません。
cHao 14年


-2

OOPの定義がメッセージパッシングスタイルを使用することであるかどうかにかかっていると思います。

純粋な関数は、新しい変数に格納できる値を返すため、何も変更する必要はありません。

var brandNewVariable = pureFunction(foo);

メッセージパッシングスタイルでは、新しい変数に保存する新しいデータを尋ねるのではなく、新しいデータを保存するようにオブジェクトに指示します。

sameOldObject.changeMe(foo);

オブジェクトのメソッドを純粋な関数にすることにより、オブジェクトを保持し、変化させないようにすることができます。

var brandNewVariable = nonMutatingObject.askMe(foo);

ただし、メッセージパッシングスタイルと不変オブジェクトを混在させることはできません。

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