私は次のようなポリモーフィズムの定義をよく読みます。
ポリモーフィズムは、異なるタイプのオブジェクトに同じメッセージを理解させる機能です。
しかし、上記の定義は、ポリモーフィズムを使用しない場合にも適用されます。たとえば、メソッドdraw()
を持つタイプCircle
のオブジェクトとタイプRectangle
の別のオブジェクトがある場合などです。メソッドdraw()
を使用すると、次のことができます。
circle1.draw();
rectangle1.draw();
したがって、circle1
とrectangle1
は、ポリモーフィズムを使用せずに同じメッセージdraw()
を理解しました!
何か不足していますか?
あなたの例では、実際には同じメッセージを表示するのではなく、たまたま同じ名前を持つ2つの異なるメッセージを表示します。ポリモーフィズムでは、メッセージのsenderが正確な受信者を知らなくても送信できる必要があります。呼び出し側がshape
に円と長方形のどちらが含まれているかを知らなくてもshape.draw()
のようなことができるという証拠がなければ、実際のポリモーフィズムがある場合とない場合があります。それらはcircle.draw()
およびweapon.draw()
と同じように無関係でもかまいません。
それらは必ずしも両方が同じ名目上のインターフェースを実装する必要はありません。言語は、構造型付けまたはコンパイル時のテンプレート化をサポートでき、それでもポリモーフィズムと呼ばれます。呼び出し元が呼び出し先が誰であるかを気にしない限り。
ポリモーフィズムは、異なるタイプのオブジェクトに同じメッセージを理解させる機能です。
これは多態性についての私にはかなり貧弱な説明のようです。技術的には正確ですが、その有用性を説明するのにはあまり役に立ちません。基本的には、ポリモーフィズムが実際に使用される方法とは逆です。したがって、次のように例を変更できます。
_circle1.draw();
rectangle1.render();
_
そして、それはまだうまくいくでしょう。これは混乱の核心です。また、「同じメッセージを理解しているオブジェクト」が本当に役立つ説明ではない理由を示しています。私が円なら、長方形が同じメソッドを持つかもしれないという事実は私には関係ありません。円として、私は丸いものだけを気にし、愚かな角のある形は気にしない.
ポリモーフィズムの価値を理解するには、これを呼び出すコードについて考える必要があります。 Pythonから始めましょう。この概念は、この文脈では少し理解しやすいと思うので、次の方法を検討してください。
_def Paint(*shapes):
for shape in shapes:
shape.draw()
_
この場合、任意のオブジェクトをこのメソッドに渡すことができ、ゼロのパラメーターを受け入れるdraw()
メソッドがある限り、「描画」メッセージが各オブジェクトに送信されます。これは「ダックタイピング」と呼ばれる多型の一種です。したがって、最初の例はこの種のアプローチに合わせることができます。四角形のメソッドをrender()
に変更すると、四角形が渡されたときに失敗します。共通の(暗黙的な)インターフェイスはなくなります。
潜在的な落とし穴は、すべてのタイプが「描画」メッセージを同じように理解するわけではないということです。たとえば、Gunslinger
オブジェクトを渡すと、PaintメソッドはGunslingerのdrawメソッドを問題なく呼び出しますが、Gunslinger
draw()
メソッドの意味は大きく異なります意図したよりも。実際には、この問題は珍しい傾向がありますが、発生する可能性があります。
JavaまたはC#などの言語では、明示的なインターフェイスの概念があります。同じ名前のメソッドを用意するだけでは十分ではありません。クラスに共通のインターフェイスを実装する必要があります。メソッドが「同じメッセージ」になるための順序。たとえば、Javaの上記のPaint
メソッドと同等のものは次のようになります。
_void Paint(Object... shapes) {
for (Object shape : shapes) {
shape.draw();
}
}
_
しかし、Pythonバージョンとは異なり、これは機能しません。コンパイルすらできません。その理由は、オブジェクトタイプにdraw()
が定義されていないためです。 draw()
メソッドを定義するShape
などの型が必要です。メソッドは次のようになります。
_void Paint(Shape... shapes) {
for (Shape shape : shapes) {
shape.draw();
}
}
_
期待どおりに動作します。これとPythonバージョン:の間に大きな違いがあります。Shape
を実装していないものを渡そうとすると、コンパイル時エラーが発生します(またはランタイムキャストエラー。)Gunslinger
オブジェクトを渡そうとすると、機能しなくなります。同様に、Circle
やRectangle
がShapeインターフェースを実装すると、それらも受け入れられません。コンパイラに関する限り、これら2つの間に共通のインターフェースがない場合、これらの2つはGunslinger
バージョンと似ています。
つまり、このタイプのタイピングでは、メソッドの「メッセージ」は同じではありません。メソッドの名前(およびシグネチャ)が同じだからといって、「メッセージ」はインターフェースのメソッド定義によって定義されます。共通のインターフェースがない場合、Circle.draw()
およびRectangle.draw()
はたまたま同じ名前を持つ2つのメソッドですが、「同じメッセージ」とは見なされません
概念的には2つのアプローチに大きな違いはないことを理解することが重要だと思います。違いは、インターフェイス(またはコントラクト)がコードで暗黙的であるか、明示的であるかです。 Shape
のコンパイル済みインターフェースがないため、Gunslinger.draw()
はShape.draw()
と同等にはなりません。
したがって、_
circle1
_および_rectangle1
_は、ポリモーフィズムを使用せずに同じメッセージdraw()
を理解しました!
彼らが多態性を使用していないと思うのはなぜですか?
何か不足していますか?
はい:あなたが説明するものisポリモーフィズム、定義により。
しかし、上記の定義は、ポリモーフィズムを使用しない場合にも適用されます。[...]つまり、circle1と長方形1は、ポリモーフィズムを使用せずに同じメッセージdraw()を理解しました。
これは、一般的な論理的誤りの完璧な例です。前提を前提
A implies B
できるできないと結論する
if we have B, therefore we also must have A. <-- WRONG
唯一の有効な結論は次のとおりです。
If there's no B then there's certainly also no A.
これがポリモーフィズムの良い例です。さまざまな種類の銀行口座を表す次のクラスがあるとします。
(そして、それぞれがaccount
スーパークラスから継承すると仮定しましょう。)
次に、これらのクラスのeachに次のメソッドを追加したとします。
そうすれば、システムで普通預金口座を開くには、savings.open()
を使用できることがわかります。ビジネスアカウントの場合も同じです:business.open()
。
そして、アカウントを閉鎖したい場合は、.close()
などを使用できることがわかります。さまざまな.close()
メソッドが想定どおりの処理を行い、各アカウントを閉鎖した場合、次に多態性になります。
それを行わず、異なる名前のメソッド(例:savings.delete()
、shared.erase()
、current.remove()
)を使用すると、非常に混乱します。ポリモーフィズムは、コードをより直感的で混乱の少ないものにする習慣です。
したがって、circle.draw()
およびsquare.draw()
の例は、ポリモーフィズムの完璧な例です。それらは形状なので、おそらくshape
クラスから継承します。ポリモーフィズムを使用しなかった場合は、circle.render()
やsquare.create()
などのメソッドがあります。
「circle1」という名前のサークルクラスのインスタンスを作成したように見えても、何も変わりません。
要するに:あなたの例IS最も基本的な形の多型のデモンストレーションです。
関数について覚えておくべき重要なことは、それらを呼び出したときに何が起こるかです:
インターフェースが、引数を受け入れず、意味のある戻り値を持たない単一のメソッドを実装するだけの単純なものである場合、多くのものがインターフェースを実装する可能性があります。
interface IDrawable {
draw: () => any;
}
バグとサムの両方があなたのインターフェースを実装しています。カードのデッキはあなたのインターフェースを実装します。ちなみに、戻り値の型を1つ追加すると、話はかなり異なります。
interface IDrawable {
draw: () => Image;
}
インターフェースが実装者が画像を返す必要があると述べた場合、おそらく、バグがドローアブルの配列に収まる唯一のものであるでしょう。
あなたの例isポリモーフィズムとアプリケーションによっては、役に立つかもしれません。ただし、より多くのコンテキストインターフェイスは、特定の要件が必要な他の場合に役立ちます。
「ポリモーフィック」はおおまかに「乗算型」に変換され、単一のコードが多くの異なる(データ/コード)構造で機能することを意味します。 circle
のコードとrectangle
のコードが1行あるため、コード例はquiteポリモーフィックではありません。これを真にポリモーフィックにする方法を次に示します。
for shape in [circle1, rectangle1]:
shape.draw()
ここで、コードの行shape.draw()
は多態性です。その1行のコードは複数のデータ構造(circle
およびrectangle
)で機能するためです。
「一流の関数/メソッド」(つまり、メソッドを表す値)の観点から多態性を理解できます。ポリモーフィックコードは、まずfetching現在の状況に適したメソッド、次にrunningそのメソッドによって、複数の状況で機能します。
上記の例では、これらの2つのステップは次のように書くことができます。
for shape in [circle1, rectangle1]:
myMethod = Object.lookup(shape, "draw")
Method.invoke(myMethod)
私は静的メソッドObject.lookup
とMethod.invoke
を作成しましたが、うまくいけば、そのアイデアを理解できます。このメソッドを表すshape
の値を返すmyMethod
オブジェクトの"draw"
スロットを探しています。 Object.lookup
は、任意の文字列とオブジェクトに対して同じように機能するため、多態的ではありません。静的メソッドMethod.invoke
はmyMethod
を実行します。繰り返しになりますが、これはすべてのメソッド値に対して同じように機能するため、多態性もありません。
では、なぜOOPがポリモーフィズムについてそれほど重要なことをするのですか?)主な理由は2つあります。
一部の言語はポリモーフィズム(shape.draw()
など)をサポートしていますが、しないはファーストクラスのメソッド(myMethod
など)をサポートしています。 JavaとC++はそのような言語の例でしたが、新しいバージョンはファーストクラスのメソッド(「ラムダ」と呼ばれます)をサポートしています。そのような言語では、myMethod
などのコードを記述できないため、上記の説明実際には機能しません(ただし、ideaは引き続き適用されます)。
コードObject.lookup(shape, "draw")
はdynamicです:ルックアップは実行時に行われ、それが機能するかどうかを知る方法はありません(たとえば、リテラル文字列"draw"
の代わりに、代わりに、ファイルまたはユーザー入力から文字列を取得できます)。ポリモーフィックコードでは、メソッド名draw
はstaticです。これは常に文字どおりコード内にあり、ルックアップが機能するかどうかを確認するための十分な情報を提供する可能性がありますbefore実行コード。繰り返しますが、JavaおよびC++は、これらのチェックを(コンパイルの一部として)実行する言語の例です。
(JavaおよびC++のような言語は、プログラミングに大きな影響を与えました。特にOOPなので、その機能とスタイルがたくさん登場するのは当然のことです。 OOPポリモーフィズムのような概念。Pythonのような他の言語はそのようなチェックを行わないため、他の人が言及する「ダックタイピング」のような異なるスタイルにつながります。)
ルックアップが機能するかどうかを確認する1つの方法はサブタイプポリモーフィズムと呼ばれ、メソッド名はいくつかの明示的な「契約」(たとえば、「クラス」、「インターフェース」、「署名」、「抽象」に関連付けられていますtype」、「existential type」、「type class」など、言語に応じて)、その契約の履行を主張する値(たとえば、「クラス/サブクラスをインスタンス化するオブジェクト」、「オブジェクト継承プロトタイプから」、「署名を実装するモジュール」、「型クラスをインスタンス化する型」など)。
サブタイプのポリモーフィズムなどのチェックは保守的です。
Circle
オブジェクトが必要で、Circle
クラスがdraw
メソッドを実装している場合、指定されたオブジェクトでdraw
を呼び出すと、実行する実装を見つけることができます。Rectangle
オブジェクトを指定しようとすると、Circle
ではないため、チェックは失敗します。ただし、この場合、Rectangle
にはdraw
メソッドがあるため、コードwouldが機能します(チェックに失敗しても停止しなかった場合)。あなたの質問は、この2番目の状況を説明しています。型チェックは数学的な証明の一種と考えることができます。shape
がCircle
であることを知っている場合、これはdraw
メソッドがあることを意味するため、shape.draw()
を呼び出しても安全です。一方、shape
がnotCircle
であることがわかっている場合、draw
メソッドがないかどうかわからないため、shape.draw()
の呼び出しを許可するのは危険です。 。
一部の言語(C++やJavaなど)はこれらの安全でない状況を禁止するため、安全であることを納得させるために、より複雑な証明を行う必要があります(たとえば、Rectangle
とCircle
が親クラスからdraw
を継承するか、同じDrawable
インターフェースを実装するなどします。 )。他の言語(PythonおよびSmalltalkなど)では、これらの安全でない状況が許可されますが、プログラムが期待どおりに動作することを確認するのは私たち次第です。
他にも多態性の形式があることに注意してください!たとえば、「パラメトリックポリモーフィズム」は、値の一部の詳細を考慮しないコードを記述します。したがって、これらの詳細には任意のタイプを含めることができます。たとえば、リストの長さを取得します。
def length(l):
result = 0
while l.next:
result = result + 1
l = l.next
return result
これは、整数のリスト、文字列のリスト、ブール値のリストのリストなどで機能するため、(パラメトリックに)多態的です。
OOPという言葉では、「ポリモーフィズム」という用語はサブタイプのポリモーフィズムを意味する傾向がありますが、パラメトリックポリモーフィズムは「ジェネリックス」と呼ばれる傾向があります。Pythonなどのチェックを強制しない言語は、そのような分類に:Python(サブタイプ)ポリモーピズムを使用するコード(たとえば、クラスを使用)を書くことができますが、Pythonコードを書く次のように見えます(サブタイプ)ポリモーフィズム。これを強制する明示的なメカニズムはありません(別名「ダックタイピング」)。
私たちの言語がファーストクラスのメソッド/関数をサポートしている場合、ポリモーフィックコードと同じように機能する「高次関数」を記述して、境界をさらにぼかすことができます。例えば:
def runOn(f, x):
return f(x)
for (draw, shape) in [(drawCircle, circle1), (drawRectangle, rectangle1)]:
runOn(draw, shape)
簡単に理解する1つの方法は、IS_A関係を考えることです。
フェラーリIS車。フォードフォーカスもそうです。
ポリモーフィズムとは、あなたが車を求めている関数を書いているということです。つまり、クラスの定義です。
result = relishOverItsExoticBeauty(car);
これも機能します:
result = relishOverItsExoticBeauty(ferarri);
これもそうです:
result = relishOverItsExoticBeauty(fordfocus);
重要な(そして上記が有用な方法で機能するために必要な)ポリモーフィズムの別の機能は、メソッドをオーバーライドする機能です。
これをOOPと混同しないでください。違います。プロパティとメソッドでオブジェクトを作成したからといって、それが多態性であるとは限りません。一部の言語では、いくつかのOOPを実行し、多態性を実行しません(少なくとも非常に適切です)。
何か不足していますか?
はい、描画は、理想的には、円と長方形の場合、独自の方法で動作します。よく見ると、それが定義です。
あなたが考えていることは、「アヒルのタイピング」と呼ばれ、「アヒルのように見えて、アヒルのように歩き、アヒルのように鳴るなら、それはアヒルです」と古くから言われています。
広く使用している言語があります。 C++標準ライブラリは、_<iostream>
_のようなクラス階層の定義から、非公式の「概念」を使用するテンプレートの組み立てに大きく移行しました。たとえば、すべてのコンテナまたはすべてのイテレータが継承する正式なクラスはありません。逆参照、増分、比較できる場合は、イテレータです。特に、ネイティブポインタはイテレータです。 begin()
およびend()
がイテレータを返す場合(およびend()
イテレータは有効なデータを保持せず、begin()
イテレータを繰り返しインクリメントして到達可能である場合有効なイテレータのシーケンスなどを取得します)これはコンテナです。
これには、複数のクラスを一度に実装しようとする場合に比べて、特にパフォーマンスが優れているという利点があります。
ただし、この方法ではできないこともあります。 1つは、抽象インターフェイスを実装し、コンパイル時に型がわからないオブジェクトを動的に渡すことです。インターフェイスをサポートする汎用オブジェクトを受け入れるライブラリにリンクすることはできません。すべてのコードは、ヘッダーファイルのテンプレートでなければなりません。多くの言語では、コンパイラは、考えられるすべてのオブジェクトで実行される汎用コードではなく、テンプレートの考えられるすべてのバージョンに対して異なるバージョンの関数を作成する必要があります。 (または、ネイティブコードではなく、上位レベルの中間コードにコンパイルする必要があります。)偶然に偶然にコードでコンパイルされるdraw()
と呼ばれるメソッドを誰かが書く可能性もあります。正式なクラス(またはtypeclass)インターフェイスがある場合、それは偶然には起こりません。コードがそのインターフェイスを使用できるのは、型がそれをサポートしているとプログラマーが主張した場合のみです。