継承が期待どおりに機能しないのはなぜですか?
-
09-06-2019 - |
質問
クライアント実装を作成するためにすべて一緒にオーバーライドする必要がある相互関連する抽象クラスのグループがあるため、継承の問題がいくつかあります。理想的には、次のようなことをしたいと思います。
abstract class Animal
{
public Leg GetLeg() {...}
}
abstract class Leg { }
class Dog : Animal
{
public override DogLeg Leg() {...}
}
class DogLeg : Leg { }
これにより、Dog クラスを使用する人は誰でも DogLegs を自動的に取得でき、Animal クラスを使用する人は誰でも Legs を自動的に取得できるようになります。問題は、オーバーライドされた関数が基本クラスと同じ型を持つ必要があるため、コンパイルできないことです。DogLeg は暗黙的に Leg にキャスト可能であるため、なぜそうすべきではないのかわかりません。これを回避する方法はたくさんあることはわかっていますが、なぜこれが C# で不可能/実装できないのかということの方が気になります。
編集:実際にはコード内で関数ではなくプロパティを使用しているため、これを多少変更しました。
編集:答えはその状況(プロパティの設定関数の値パラメーターの共分散)にのみ適用されるため、それを関数に戻しました。 すべきではありません 仕事)。変動があってごめんなさい!それが多くの答えが無関係であるように見えることを私は理解しています。
解決
簡単に言うと、GetLeg の戻り値の型は不変です。長い答えはここにあります。 共分散と反分散
通常、ほとんどの開発者がツールボックスから取り出す最初の抽象化ツールは継承ですが、代わりに合成を使用することもほとんどの場合可能であることを付け加えておきたいと思います。API 開発者にとって構成は少し手間がかかりますが、利用者にとって API はより便利になります。
他のヒント
明らかに、壊れたドッグレッグを操作している場合は、キャストが必要です。
Dog は戻り値の型として DogLeg ではなく Leg を返す必要があります。実際のクラスは DogLeg である可能性がありますが、重要なのは、Dog のユーザーが DogLegs について知る必要がなく、Legs についてだけ知っていればよいように分離することです。
変化:
class Dog : Animal
{
public override DogLeg GetLeg() {...}
}
に:
class Dog : Animal
{
public override Leg GetLeg() {...}
}
これはやってはいけない:
if(a instanceof Dog){
DogLeg dl = (DogLeg)a.GetLeg();
それは抽象型にプログラミングする目的を損ないます。
DogLeg を非表示にする理由は、抽象クラスの GetLeg 関数が Abstract Leg を返すためです。GetLeg をオーバーライドする場合は、Leg を返す必要があります。それが抽象クラスにメソッドを持つことのポイントです。そのメソッドをその子に伝播します。Dog のユーザーに DogLegs について知ってもらいたい場合は、GetDogLeg というメソッドを作成し、DogLeg を返します。
質問者の希望どおりにできるのであれば、Animal のすべてのユーザーがすべての動物について知る必要があるでしょう。
オーバーライドするメソッドの戻り値の型が、オーバーライドされるメソッドの戻り値の型のサブタイプであるシグネチャを持たせたいという要望は完全に正当です (ふう)。結局のところ、これらは実行時の型互換性があります。
ただし、C# はオーバーライドされたメソッドでの「共変戻り値の型」をまだサポートしていません (C++ [1998] や Java [2004] とは異なります)。
エリック・リッパート氏が次のように述べているように、当面は回避策を講じて間に合わせなければなりません。 彼のブログ[2008 年 6 月 19 日]:
この種の分散は「戻り型の共分散」と呼ばれます。
C# でそのような差異を実装する計画はありません。
abstract class Animal
{
public virtual Leg GetLeg ()
}
abstract class Leg { }
class Dog : Animal
{
public override Leg GetLeg () { return new DogLeg(); }
}
class DogLeg : Leg { void Hump(); }
次のように行うと、クライアントで抽象化を活用できます。
Leg myleg = myDog.GetLeg();
その後、必要に応じてキャストできます。
if (myleg is DogLeg) { ((DogLeg)myLeg).Hump()); }
完全に不自然ですが、重要なのは次のことができるということです。
foreach (Animal a in animals)
{
a.GetLeg().SomeMethodThatIsOnAllLegs();
}
ドッグレッグに特別なハンプ メソッドを適用する機能も保持しています。
ジェネリックスとインターフェイスを使用して、これを C# で実装できます。
abstract class Leg { }
interface IAnimal { Leg GetLeg(); }
abstract class Animal<TLeg> : IAnimal where TLeg : Leg
{ public abstract TLeg GetLeg();
Leg IAnimal.GetLeg() { return this.GetLeg(); }
}
class Dog : Animal<Dog.DogLeg>
{ public class DogLeg : Leg { }
public override DogLeg GetLeg() { return new DogLeg();}
}
GetLeg() はオーバーライドとなる Leg を返さなければなりません。ただし、Dog クラスは Leg の子クラスであるため、DogLeg オブジェクトを返すことができます。その後、クライアントはドッグレッグとしてキャストして操作できるようになります。
public class ClientObj{
public void doStuff(){
Animal a=getAnimal();
if(a is Dog){
DogLeg dl = (DogLeg)a.GetLeg();
}
}
}
問題の原因となっている概念については、次の場所で説明されています。 http://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science)
あまり用途があるわけではありませんが、Java が共変の戻り値をサポートしているため、これがまさに期待どおりに動作することに注目するのは興味深いかもしれません。明らかに Java にはプロパティがないことを除いて ;)
おそらく、次の例で問題を理解するのが簡単になるでしょう。
Animal dog = new Dog();
dog.SetLeg(new CatLeg());
Dog がコンパイルされている場合はコンパイルできるはずですが、おそらくそのようなミュータントは必要ありません。
関連する問題は、Dog[] を Animal[] にするべきか、それとも IList<Dog> を IList<Animal> にするべきかということです。
C# には、この問題に対処するための明示的なインターフェイス実装があります。
abstract class Leg { }
class DogLeg : Leg { }
interface IAnimal
{
Leg GetLeg();
}
class Dog : IAnimal
{
public override DogLeg GetLeg() { /* */ }
Leg IAnimal.GetLeg() { return GetLeg(); }
}
Dog 型の参照を介して Dog がある場合、GetLeg() を呼び出すと DogLeg が返されます。同じオブジェクトがあるが、参照の型が IAnimal である場合、Leg を返します。
そうです、キャストするだけでよいことは理解していますが、それはクライアントが Dog に DogLegs があることを知っている必要があることを意味します。私が疑問に思っているのは、暗黙的な変換が存在する場合、これが不可能な技術的な理由があるのかどうかです。
@Brian Leahyは、あなたがそれを脚としてのみ操作している場合、キャストする必要や理由はありません。ただし、DogLeg または Dog に特有の動作がある場合、キャストが必要な理由が存在することがあります。
Leg と DogLeg の両方が実装するインターフェイス ILeg を返すこともできます。
覚えておくべき重要なことは、基本型を使用するすべての場所で派生型を使用できるということです (Animal を期待する任意のメソッド/プロパティ/フィールド/変数に Dog を渡すことができます)。
この関数を見てみましょう:
public void AddLeg(Animal a)
{
a.Leg = new Leg();
}
完全に有効な関数です。次のように関数を呼び出してみましょう。
AddLeg(new Dog());
Dog.Leg プロパティの型が Leg ではない場合、AddLeg 関数に突然エラーが発生し、コンパイルできなくなります。
@ルーク
おそらく継承を誤解していると思います。Dog.GetLeg() は DogLeg オブジェクトを返します。
public class Dog{
public Leg GetLeg(){
DogLeg dl = new DogLeg(super.GetLeg());
//set dogleg specific properties
}
}
Animal a = getDog();
Leg l = a.GetLeg();
l.kick();
実際に呼び出されるメソッドは Dog.GetLeg(); です。DogLeg.Kick() (メソッド Leg.kick() が存在すると仮定します) の場合、DogLeg である宣言された戻り値の型は不要です。Dog.GetLeg() の戻り値の型が脚。
次のような適切な制約を持つジェネリックを使用することで、目的を達成できます。
abstract class Animal<LegType> where LegType : Leg
{
public abstract LegType GetLeg();
}
abstract class Leg { }
class Dog : Animal<DogLeg>
{
public override DogLeg GetLeg()
{
return new DogLeg();
}
}
class DogLeg : Leg { }