Javaには、メソッド定義用のvirtual
、new
、override
キーワードがないため、メソッドの動作は簡単に理解できます。 DerivedClassがBaseClassを拡張し、同じ名前と同じメソッドを持っている場合に発生しますBaseClassのシグネチャは、実行時のポリモーフィズムでオーバーライドされます(メソッドがstatic
でない場合)。
BaseClass bcdc = new DerivedClass();
bcdc.doSomething() // will invoke DerivedClass's doSomething method.
さて、C#に来ると、new
またはvirtual+derive
または new + virtual override がどのように機能しているかがわかりにくく、混乱する可能性があります。
なぜ世界でDerivedClass
と同じ名前と同じ署名のメソッドをBaseClass
に追加して、新しい動作を定義するのか理解できませんが、実行時に時間ポリモーフィズム、BaseClass
メソッドが呼び出されます! (これはオーバーライドではありませんが、論理的にはオーバーライドする必要があります)。
virtual + override
の場合、論理的な実装は正しいですが、プログラマーは、コーディング時にオーバーライドするための許可をユーザーに与える必要がある方法を考える必要があります。これにはいくつかのpro-conがあります(今はそこには行きません)。
それで、なぜC#ではun論理的な推論と混乱のために多くのスペースがあるのですか?では、実際のコンテキストでnew
の代わりにvirtual + override
を使用し、virtual + override
の代わりにnew
を使用することを検討する必要があるとして、質問を再構成できますか?
特に Omar からいくつかの非常に良い回答を受け取った後、C#デザイナーは、プログラマーがメソッドを作成する前に考える必要があることについてより多くのストレスを与えたことがわかります。
今、私は質問を念頭に置いています。 Javaのように、もし私が
Vehicle vehicle = new Car();
vehicle.accelerate();
その後、SpaceShip
から派生した新しいクラスVehicle
を作成します。次に、すべてのcar
をSpaceShip
オブジェクトに変更します。コードを1行変更するだけです。
Vehicle vehicle = new SpaceShip();
vehicle.accelerate();
これにより、コードのどの時点でも私のロジックが壊れることはありません。
ただし、C#の場合、SpaceShip
がVehicle
クラス 'accelerate
をオーバーライドせず、new
を使用すると、コードのロジックが壊れます。それは不利ではありませんか?
なぜC#がこのようにしたのかを尋ねたので、C#の作成者に尋ねることが最善です。 C#の主任アーキテクトであるAnders Hejlsbergが インタビュー でデフォルトで仮想(Javaのように)を使用しないことを選択した理由を回答しました。関連するスニペットは以下のとおりです。
Javaには、デフォルトで final キーワードを使用して仮想があり、メソッドを非仮想としてマークします。まだ学ぶべき2つの概念がありますが、多くの人は知らないことに注意してください。最終的なキーワードについて、または積極的に使用しないでください。C#は、仮想および新規/オーバーライドを使用して、これらの決定を意識的に行うことを強制します。
いくつかの理由があります。 1つはperformanceです。人々がJavaでコードを書くとき、彼らは彼らのメソッドをfinalとマークするのを忘れているのを観察できます。したがって、これらのメソッドは仮想です。それらは仮想であるため、パフォーマンスも低下します。仮想メソッドであることに関連するパフォーマンスのオーバーヘッドがあります。それが一つの問題です。
より重要な問題はversioningです。仮想メソッドについては2つの考え方があります。アカデミックスクールでは、「いつかはオーバーライドしたいので、すべてを仮想化する必要があります」と述べています。現実の世界で実行される実際のアプリケーションを構築することから生まれた実用的な考え方は、「仮想化するものについては十分に注意する必要があります」と述べています。
プラットフォームで何かを仮想化するとき、それが将来どのように進化するかについて、非常に多くの約束をします。非仮想メソッドの場合、このメソッドを呼び出すとxとyが発生することを約束します。 APIで仮想メソッドを公開するとき、このメソッドを呼び出すとxとyが発生することを約束するだけではありません。また、このメソッドをオーバーライドすると、これらの他のメソッドに関してこの特定のシーケンスでメソッドが呼び出され、状態はこれとその不変状態になることも約束します。
APIで仮想と言うたびに、コールバックフックを作成しています。 OSまたはAPIフレームワークの設計者は、それについて十分に注意する必要があります。ユーザーがAPIの任意の時点でオーバーライドおよびフックすることは望ましくありません。これらの約束を必ずしも満たすことができないためです。そして、人々は彼らが何かを仮想的にするとき彼らがしている約束を完全に理解していないかもしれません。
インタビューでは、開発者がクラス継承の設計についてどのように考えるか、そしてそれがどのようにして彼らの決定につながったかについてより多くの議論があります。
次の質問です。
私はなぜDerivedClassにBaseClassと同じ名前と同じシグネチャでメソッドを追加し、新しい動作を定義するのか理解できませんが、実行時のポリモーフィズムでBaseClassメソッドが呼び出されます! (これはオーバーライドではありませんが、論理的にはオーバーライドする必要があります)。
これは、派生クラスが基本クラスの規約に準拠していないが、同じ名前のメソッドがあることを宣言したい場合です。 (C#のnew
とoverride
の違いを知らない人は、これを参照してください MSDNページ )。
非常に実用的なシナリオは次のとおりです。
Vehicle
というクラスを持つAPIを作成しました。Vehicle
を派生させました。Vehicle
クラスにはメソッドPerformEngineCheck()
がありませんでした。Car
クラスに、メソッドPerformEngineCheck()
を追加します。PerformEngineCheck()
を追加しました。したがって、新しいAPIに対して再コンパイルすると、C#がこの問題を警告します。
ベースPerformEngineCheck()
がvirtual
ではなかった場合:
_app2.cs(15,17): warning CS0108: 'Car.PerformEngineCheck()' hides inherited member 'Vehicle.PerformEngineCheck()'.
Use the new keyword if hiding was intended.
_
そして、ベースPerformEngineCheck()
がvirtual
だった場合:
_app2.cs(15,17): warning CS0114: 'Car.PerformEngineCheck()' hides inherited member 'Vehicle.PerformEngineCheck()'.
To make the current member override that implementation, add the override keyword. Otherwise add the new keyword.
_
ここで、クラスが実際に基本クラスのコントラクトを拡張するのか、それとも別のコントラクトであるのに偶然同じ名前であるのかを明示的に判断する必要があります。
new
にすることで、ベースメソッドの機能が派生メソッドと異なる場合でもクライアントが壊れることはありません。 Vehicle
を参照したコードはCar.PerformEngineCheck()
が呼び出されたのを確認できませんが、Car
への参照があったコードでは、PerformEngineCheck()
。同様の例は、基本クラスの別のメソッドがPerformEngineCheck()
(特に新しいバージョン)を呼び出している可能性がある場合、派生クラスのPerformEngineCheck()
を呼び出せないようにするにはどうすればよいですか? Javaでは、その決定は基本クラスに依存しますが、派生クラスについては何も知りません。 C#では、その決定は(virtual
キーワードを介して)基本クラスにboth、および派生クラス(new
およびoverride
キーワードを使用)。
もちろん、コンパイラーがスローするエラーは、プログラマーが予期せずエラーを発生させないための便利なツールも提供します(つまり、それを実現せずにオーバーライドまたは新しい機能を提供します)。
アンダースが言ったように、現実の世界は私たちをそのような問題に強制します。
編集:インターフェイスの互換性を確保するためにnew
を使用する必要がある場所の例を追加しました。
編集:コメントを検討しているときに、他のサンプルシナリオ(ブライアンによる言及)で Eric Lippertによる記述 (C#設計委員会のメンバーの1人)にも出会いました。
パート2:更新された質問に基づいて
しかし、C#の場合、SpaceShipがVehicleクラスのアクセラレーションをオーバーライドして新しいものを使用しないと、コードのロジックが壊れます。それは不利ではありませんか?
SpaceShip
が実際にVehicle.accelerate()
をオーバーライドしているかどうか、またはそれが異なるかどうかは誰が決定しますか? SpaceShip
開発者である必要があります。したがって、SpaceShip
開発者が基本クラスのコントラクトを保持していないと判断した場合、Vehicle.accelerate()
への呼び出しはSpaceShip.accelerate()
に送るべきではありませんか?そのとき、彼らはそれをnew
とマークします。ただし、実際に契約を維持すると判断した場合は、実際にoverride
とマークします。どちらの場合でも、contractに基づいて適切なメソッドを呼び出すことにより、コードは正しく動作します。 SpaceShip.accelerate()
が実際にVehicle.accelerate()
をオーバーライドしているかどうか、または名前の衝突かどうかをコードで判断するにはどうすればよいですか? (上記の私の例を参照してください)。
ただし、暗黙的な継承の場合、SpaceShip.accelerate()
がVehicle.accelerate()
のcontractを保持していなくても、メソッド呼び出しは引き続きSpaceShip.accelerate()
に行きます。
それは正しいことだからです。実際、allメソッドのオーバーライドを許可することは間違っています。それは脆弱な基本クラスの問題につながり、基本クラスへの変更がサブクラスを破壊するかどうかを知る方法がありません。したがって、オーバーライドしてはならないメソッドをブラックリストに登録するか、オーバーライドを許可するメソッドをホワイトリストに登録する必要があります。 2つのうち、ホワイトリストは安全であるだけでなく(壊れやすい基本クラス偶然を作成できないため)、構成を優先して継承を回避する必要があるため、必要な作業も少なくなります。
ロバート・ハーヴェイが言ったように、それはあなたが慣れ親しんでいることのすべてです。この柔軟性のJavaの不足は奇妙だと思います。
とはいえ、なぜこれが最初にあるのですか? C#にpublic
、internal
( "nothing")、protected
、protected internal
、private
があるのと同じ理由で、= Javaはpublic
、protected
、何もない、およびprivate
のみです。コーディングしているものの動作をより細かく制御できます。追跡する用語とキーワードが増えることを犠牲にして。
new
とvirtual+override
の場合、次のようになります。
abstract
およびoverride
を使用します。virtual
およびoverride
を使用します。new
を使用します。sealed
を使用します。実際の例として、さまざまなソースからのeコマース注文の処理に取り組んだプロジェクト。各ソースの子クラスがオーバーライドする特定のOrderProcessor
/abstract
メソッドを備えた、ほとんどのロジックを持つベースvirtual
がありました。これは、完全に注文の処理方法が異なる新しいソースを取得するまでは正常に機能し、コア関数を置き換える必要がありました。この時点で2つの選択肢がありました。1)virtual
を基本メソッドに追加し、override
を子に追加します。または2)子にnew
を追加します。
どちらか一方は機能しますが機能しますが、前者を使用すると、その特定のメソッドを将来非常に簡単にオーバーライドできます。たとえば、オートコンプリートで表示されます。ただし、これは例外的なケースだったため、代わりにnew
を使用することにしました。これにより、「このメソッドをオーバーライドする必要がない」という標準が維持され、特殊なケースが可能になりました。それは人生を楽にする意味論的な違いです。
ただし、意味的な違いだけでなく、これに関連する動作の違いがあることに注意してください。詳細は この記事 を参照してください。ただし、この動作を利用する必要がある状況に遭遇したことはありません。
Javaの設計は、オブジェクトへの参照が与えられた場合、特定のパラメータータイプを持つ特定のメソッド名への呼び出しが許可されている場合、常に同じメソッドを呼び出すようになっています。暗黙的なパラメーター型変換が参照の型の影響を受ける可能性がありますが、このような変換がすべて解決されると、参照の型は無関係になります。
これによりランタイムが簡素化されますが、いくつかの不幸な問題が発生する可能性があります。 GrafBase
はvoid DrawParallelogram(int x1,int y1, int x2,int y2, int x3,int y3)
を実装していないが、GrafDerived
は、計算された4番目の点が最初の点と反対の平行四辺形を描画するパブリックメソッドとして実装するとします。さらに、GrafBase
の新しいバージョンが同じシグネチャを持つpublicメソッドを実装しているが、計算された4番目のポイントは2番目のポイントと反対であるとします。 GrafBase
を受け取るがGrafDerived
への参照を受け取るクライアントは、DrawParallelogram
が新しいGrafBase
メソッドと同じように4番目のポイントを計算することを期待しますが、基本メソッドが変更される前にGrafDerived.DrawParallelogram
を使用していたクライアントは、GrafDerived
が最初に動作したことを期待します実装されました。
Javaでは、GrafDerived
を定義する前にGrafBase.DrawParallelogram
を使用した既存のクライアントコードとの互換性を損なうことなく、GrafDerived
の作成者がそのクラスを新しいGrafDerived.DrawParallelogram
メソッドを使用するクライアントと共存させる方法はありません(また、GrafBase
が存在することさえ知らない場合もあります)。 。 DrawParallelogram
は、どの種類のクライアントがそれを呼び出しているのかを判別できないため、種類のクライアントコードによって呼び出された場合と同じように動作する必要があります。 2種類のクライアントコードはその動作について異なる期待を持っているため、GrafDerived
がそれらの1つで正当な期待に違反する(つまり、正当なクライアントコードを破る)ことを回避する方法はありません。
C#では、GrafDerived
が再コンパイルされない場合、ランタイムは、タイプDrawParallelogram
の参照時にGrafDerived
メソッドを呼び出すコードが最後にコンパイルされたときのGrafDerived.DrawParallelogram()
の動作を期待するが、参照時にメソッドを呼び出すコードを想定します。タイプGrafBase
の場合、GrafBase.DrawParallelogram
(追加された動作)が想定されます。拡張されたGrafDerived
が存在する場合にGrafBase
が後で再コンパイルされると、コンパイラーは、プログラマーがそのメソッドが継承メンバーGrafBase
の有効な置換であるか、またはその動作をGrafDerived
型の参照に関連付ける必要があるかどうかを指定するまでスコークします、ただし、タイプGrafBase
の参照の動作を置き換えることはできません。
GrafDerived
のメソッドを使用すると、同じシグネチャを持つGrafBase
のメンバーとは異なる何かを行うことは、設計が悪いことを示すため、サポートされるべきではないと合理的に主張するかもしれません。残念ながら、基本型の作成者は、派生型にどのメソッドを追加できるか、またはその逆を知る方法がないため、基本クラスと派生クラスのクライアントが同じような名前のメソッドに対して異なる期待を持つ状況は、基本的には避けられません他の誰かが追加する可能性のある名前を追加することはできません。問題は、そのような名前の重複が発生するかどうかではなく、発生した場合の害を最小限に抑える方法です。
標準的な状況:
あなたは、複数のプロジェクトで使用される基本クラスの所有者です。実際の価値を提供するプロジェクトにある1と無数の派生クラスの間で分割される上記の基本クラスに変更を加えたい(フレームワークは1つの削除で最高の値を提供し、実際の人間はフレームワークを望んでいない、彼らは望んでいる)フレームワークで実行されているもの)。派生クラスの忙しい所有者に幸運を祈ります。決定を承認する必要のある人々に「フレームワーク:プロジェクトの遅延者とバグの原因者」としての代表者を獲得することなく、「まあ、あなたは変更する必要があります。あなたはその方法をオーバーライドすべきではありません」。
特に、オーバーライド不可として宣言しないことで、変更を妨げる処理を実行してもよいと暗黙のうちに宣言したことになります。
そして、基本クラスをオーバーライドすることによって実際の価値を提供する派生クラスがあまり多くない場合、そもそもなぜそれが基本クラスなのですか?希望は強力な動機ですが、参照されていないコードで終わる非常に良い方法でもあります。
最終結果:フレームワークの基本クラスのコードは信じられないほど壊れやすく、静的になり、現在/効率を維持するために必要な変更を実際に行うことはできません。または、フレームワークを使用すると、コーディングをより高速で信頼性の高いものにするため、フレームワークは不安定さの派生(派生クラスが壊れ続ける)を取得し、人々はそれをまったく使用しません。
簡単に言えば、多忙なプロジェクトオーナーに、導入しているバグを修正するためにプロジェクトを遅らせるよう依頼することはできません。また、元の「障害」であっても、大きなメリットを提供しない限り、「撤退」以上のことを期待することはできません。彼らのものだった、それはせいぜい議論の余地がある。
そもそも、「デフォルトで非仮想」が使用されている場所で間違ったことをさせない方が良いでしょう。そして、誰かがこの特定のメソッドをオーバーライド可能にする必要があるという非常に明確な理由があるときに、安全である必要があります。「ロックを解除」できますなし他人のコードを壊す危険があります。
非仮想のデフォルトは、基本クラスの開発者が完璧であると想定しています。私の経験では、開発者は完璧ではありません。基本クラスの開発者が、メソッドがオーバーライドされる可能性があるユースケースを想像できない場合、または仮想を追加するのを忘れた場合、基本クラスを変更せずに基本クラスを拡張するときに、ポリモーフィズムを利用できません。実際には、基本クラスを変更することはしばしばオプションではありません。
C#では、基本クラスの開発者はサブクラスの開発者を信頼しません。 Javaで、サブクラスの開発者は基本クラスの開発者を信頼していません。サブクラスの開発者はサブクラスに責任があり、基本クラスを拡張する権限を(私見)与える必要があります(禁止)明示的な拒否、およびJava=の場合、これは間違っている可能性さえあります)。
これは言語定義の基本的な特性です。それは正しいか間違っているのではなく、それが何であるかであり、変えることはできません。