オーバーロードと継承

プログラミング言語には、実装方法を知るために必要な特定の機能セットがあります。 ここで、たとえばクロージャ 。 これは過度に複雑な概念ではありませんが、この獣がどのように設計されているかを知ることで、ループ変数を持つクロージャーの動作に関する特定の結論を引き出すことができます。 基本クラスのコンストラクターで仮想メソッドを呼び出す場合も同じことが当てはまります。正しい解決策はありません。言語開発者が決定したこと、継承メソッドが呼び出されるかどうか(JavaまたはC#の場合)基本クラスメソッドが呼び出されます(C ++の場合と同様)。



理想的な解決策を持たない別のタイプの問題は、メソッドのオーバーロードとメソッドのオーバーライドの組み合わせです。 次のC#の例を見てみましょう。 Derivedクラスに仮想メソッドFoo( int)と非仮想メソッドFoo( オブジェクト)を持つBaseDerivedのクラスがあるとします:







class Base { public virtual void Foo(int i) { Console.WriteLine("Base.Foo(int)"); } } class Derived : Base { public override void Foo(int i) { Console.WriteLine("Derived.Foo(int)"); } public void Foo(object o) { Console.WriteLine("Derived.Foo(object)"); } }
      
      







問題は、次の場合にどのメソッドが呼び出されるかです。



 int i = 42; Derived d = new Derived(); d.Foo(i);
      
      







最初の最も合理的な仮定は、 Derivedメソッドが呼び出されることです。 42はintであり、 DerivedクラスにはFoo( int)メソッドが含まれているため、 Foo( int) 。 ただし、実際にはそうではなく、 Derivedメソッドが呼び出されます。 Foo( オブジェクト)



もちろん、賢い人はすぐに仕様に進み、次の結論を出します:コンパイラは、彼らが言うには、メソッドの宣言と再定義を異なる方法で扱い、彼は最初に現在の変数のクラス(つまり、 派生クラス)で適切なメソッドを検索し、適切なオーバーロードであれば(暗黙的な型変換が必要な場合でも)見つかった場合、継承者によって再定義されたメソッドのより適切なバージョンがあったとしても、これに落ち着いてベースクラス(つまり、 ベースクラス)を検討します。



ただし、この場合、メソッドの宣言と再定義が異なって解釈され、基本クラスのメソッドが第2種の「メソッド」であるという事実だけでなく、コンパイラがそれらを二次的に分析し、コンパイラ(またはむしろ開発者)が決定した理由の数は興味深いまさにそのような動作を実装します。

現在の動作がどの程度論理的であるかという質問に答えるために、一歩戻ってこのケースを考えてみましょう。 クラス階層にFoo( オブジェクト)メソッドが1つだけあり、それがDerivedクラスにあるとします。



 class Base {} class Derived : Base { public void Foo(object o) { Console.WriteLine("Derived.Foo(object)"); } }
      
      







はい、非常に有用なクラス階層ではありませんが、それでもなお。 その中で最も重要なことは、次の場合にどのFooコールが呼び出されるかについて誰も質問しないということです(オプションは1つしかありません):new Derived()。Foo(42)。

しかし、異なる組織、または少なくとも異なる開発者がBaseクラスとDerivedクラスの開発に関与していると仮定しましょう。 Baseクラスの開発者はDerivedクラスの開発者が実際に何をするのか実際には知らないので、相続クラスの開発者の知識がなくても、ある時点でFooメソッドを基本クラスに追加できます。



 class Base { public virtual void Foo(int i) { Console.WriteLine("Base.Foo(int)"); } }
      
      







元の質問に答えるときに私たちに言われた常識に従えば、 Fooメソッドと次のコードのより適切なオーバーロードが得られます:new Derived()。Foo(42)は、ベースクラスメソッドと出力Baseの呼び出しにつながるはずです。 Foo( int) 。 ただし、 派生クラスの開発者の知識がなくても、基本クラスを変更した後、十分にテストされたコードが突然機能しなくなることは、どの程度論理的でしょうか? もちろん、この場合、基本クラスのメソッドを呼び出さないで、 Derivedクラスにオーバーロードがある場合にのみ呼び出すと言うことができます。 しかし、この動作はさらに奇妙になります。

この問題は、プログラミング言語のほとんどの開発者が何らかの方法で解決しようとする「脆弱な基底クラス症候群」の問題として、C#言語仕様の読者とEric Lippertのブログの幅広い読者に知られています。 この特定のケースでは、 コンパイラは最初に、使用される変数のクラスで直接宣言されたメソッドを分析し、適切なメソッドがない場合にのみ、基本クラスで宣言されたメソッドを考慮します。



他のプログラミング言語はどうですか?


はい、この問題が他のプログラミング言語、たとえばC ++、Java、またはEiffelでどのように解決されるかを学ぶことは非常に興味深いでしょう(最も洗練されたオブジェクト指向プログラミング言語の多くの意見)。



少し簡単になるので、最後から始めましょう。 Eiffelでは、問題は非常に簡単に解決されます。多くの難しいOOピースにもかかわらず、Eiffelにはメソッドのオーバーロードがなく、子孫の基本クラスメソッドと同じ名前のメソッドを宣言することはできません。 これは、この問題の診断がコンパイル時に延期され、実行時に存在しないことを意味します。 (ちなみに、これはばかげて聞こえますが、多くの問題に対処するための非常に効果的な方法です。同じエッフェルは、それを許可しないという事実によって多くの重要な問題をうまく解決します。ドメインドメインの多くの問題:解決するために6か月間殺すよりもユーザーの可能性を禁止する方が簡単な場合があります)。





実際、このようなトリックはエッフェル言語だけでなく使用されます。 そのため、たとえば、C#では仮想イベントに問題がありますが 、これはVB.NETで非常にエレガントに解決れています。仮想イベントは単に禁止されています。



JavaとC ++では、状況が多少異なります。これは、まず、これらの言語が派生クラスでメソッドオーバーライドを宣言する方法によるものです。 これらの言語では、デフォルトの仮想性への異なるアプローチが使用されます(Javaでは、すべてのメソッドはデフォルトで仮想であり、C ++ではメソッドの仮想性を明示的に明示的に宣言する必要があります)が、最初は後続のクラスで仮想メソッドをオーバーライドするために同じアプローチを使用しました:



 class Base { public: virtual void Foo(Integer& i) {} }; class Derived : public Base { public: // : // virtual void Foo(Integer& i) //  // void Foo(Integer& i) override // C++11 //    ! void Foo(Integer& i) {} void Foo(Object& o) {} };
      
      







JavaおよびC ++でメソッドをオーバーライドするには、追加のキーワードを使用する必要はありません。後継クラスに同じシグネチャを持つメソッドを実装するだけで十分です。 また、構文の観点から、オーバーライド可能なメソッドは新しいメソッドの宣言とまったく変わらないため( Derivedクラスの2つのメソッドを比較してください)、ここでの動作はC# は異なります。



 Integer i; Derived *pd = new Derived; pd->Foo(i);
      
      







この場合、最初に予想したとおりFoo( Integer&)メソッドが呼び出されます。

JavaおよびC ++では、プログラマーは後に、相続人のメソッドをオーバーライドするという観点から意図をより正確に伝えることができるようになりました。 Javaでは、バージョン5以降、特別な注釈-Overrideが登場し、C ++ 11では、新しいキーワード「override」が登場しました。 ただし、明らかな理由により、これらの言語の動作は変更されていません。





ところで、以前の標準と比較したC ++ 11の新機能の詳細については、Bjarne StraustrupのFAQ翻訳: C ++ 11 FAQを参照してください

確かに、Java言語とC ++言語の類似性はこれで終わりです。 DerivedクラスのFoo( Integer&)メソッドをコメントアウトすると、C ++でDerived :: Foo( Object&)が呼び出されます(つまり、より適切なベースクラスメソッドは候補と見なされません) Foo( 整数)



おわりに


オーバーロードの解決はそれ自体かなり興味深いものですが( ここでは確認のためのNikovの研究の1つです)、継承を追加するとさらに複雑になります。 一方で、C#の現在の動作は間違っているように見えるかもしれませんが、賛否両論を比較検討すると、かなり論理的でそれほど悪くないことがわかります。

いずれにせよ、使用するプログラミング言語に関係なく、アドバイスは同じです。可能であれば、メソッドのオーバーロードと再定義を混在させないことをお勧めします。



All Articles