私はC#に多重継承を含めることに反対する多くの議論に出くわしました。そのうちのいくつかは(哲学的議論は別として)以下を含みます:
私はC++のバックグラウンドを持っており、多重継承のパワーとエレガンスが恋しいです。すべてのソフトウェア設計に適しているわけではありませんが、インターフェース、構成、および同様のOO手法)での有用性を否定するのが難しい状況があります。
多重継承の除外は、開発者がそれらを賢く使用するのに十分賢くなく、複雑さが発生したときに対処できないことを示していますか?
私は個人的に、C#(おそらくC ##)への多重継承の導入を歓迎します。
補遺:単一(または手続き的背景)と多重継承の背景から来る応答から知りたいと思います。多重継承の経験がない開発者は、パラダイムの経験がないという理由だけで、デフォルトで多重継承は不要な引数になることがよくあります。
私は一度もそれを見逃したことはありません。はい、それは[MI]複雑になります、そしてはい、インターフェースは多くの方法で同様の仕事をします-しかしそれは最大のポイントではありません:一般的な意味で、それは単にほとんどの場合必要ではありません。多くの場合、単一の継承でさえ使いすぎです。
継承よりも集約を優先してください!
class foo : bar, baz
多くの場合、より適切に処理されます
class foo : Ibarrable, Ibazzable
{
...
public Bar TheBar{ set }
public Baz TheBaz{ set }
public void BarFunction()
{
TheBar.doSomething();
}
public Thing BazFunction( object param )
{
return TheBaz.doSomethingComplex(param);
}
}
このようにして、IBarrableとIBazzableのさまざまな実装を入れ替えて、さらに別のクラスを作成しなくても、アプリの複数のバージョンを作成できます。
依存性注入はこれに大いに役立ちます。
多重継承を処理する際の問題の1つは、インターフェース継承と実装継承の違いです。
C#には、純粋なインターフェイスを使用したインターフェイス継承のクリーンな実装(暗黙的または明示的な実装の選択を含む)がすでにあります。
C++を見ると、class
宣言のコロンの後に指定するクラスごとに、取得する継承の種類はアクセス修飾子(private
、protected
)によって決定されます。 、またはpublic
)。 public
継承を使用すると、多重継承の完全な混乱を得ることができます。複数のインターフェイスが複数の実装と混合されます。 private
継承を使用すると、実装を取得できます。 「_class Foo : private Bar
_」のオブジェクトは、Bar
クラスが実際にはプライベートFoo
フィールドを持っているかのようであるため、Bar
を期待する関数に渡されることはありません。および自動的に実装される delegationpattern 。
純粋な複数実装の継承(実際には単なる自動委任)は問題を引き起こさず、C#で持つのは素晴らしいことです。
クラスからの複数のインターフェイスの継承に関しては、機能を実装するためのさまざまな可能な設計があります。多重継承を持つすべての言語には、メソッドが複数の基本クラスで同じ名前で呼び出されたときに何が起こるかについて独自のルールがあります。 Common LISP(特にCLOSオブジェクトシステム)やPythonなどの一部の言語には、基本クラスの優先順位を指定できるメタオブジェクトプロトコルがあります。
1つの可能性があります:
_abstract class Gun
{
public void Shoot(object target) {}
public void Shoot() {}
public abstract void Reload();
public void Cock() { Console.Write("Gun cocked."); }
}
class Camera
{
public void Shoot(object subject) {}
public virtual void Reload() {}
public virtual void Focus() {}
}
//this is great for taking pictures of targets!
class PhotoPistol : Gun, Camera
{
public override void Reload() { Console.Write("Gun reloaded."); }
public override void Camera.Reload() { Console.Write("Camera reloaded."); }
public override void Focus() {}
}
var pp = new PhotoPistol();
Gun gun = pp;
Camera camera = pp;
pp.Shoot(); //Gun.Shoot()
pp.Reload(); //writes "Gun reloaded"
camera.Reload(); //writes "Camera reloaded"
pp.Cock(); //writes "Gun cocked."
camera.Cock(); //error: Camera.Cock() not found
((PhotoPistol) camera).Cock(); //writes "Gun cocked."
camera.Shoot(); //error: Camera.Shoot() not found
((PhotoPistol) camera).Shoot();//Gun.Shoot()
pp.Shoot(target); //Gun.Shoot(target)
camera.Shoot(target); //Camera.Shoot(target)
_
この場合、競合が発生した場合、最初にリストされたクラスの実装のみが暗黙的に継承されます。他の基本タイプのクラスは、それらの実装を取得するために明示的に指定する必要があります。それをよりばかげたものにするために、コンパイラーは競合の場合に暗黙の継承を禁止することができます(競合するメソッドは常にキャストを必要とします)。
また、暗黙の変換演算子を使用して、今日C#で多重継承を実装できます。
_public class PhotoPistol : Gun /* ,Camera */
{
PhotoPistolCamera camera;
public PhotoPistol() {
camera = new PhotoPistolCamera();
}
public void Focus() { camera.Focus(); }
class PhotoPistolCamera : Camera
{
public override Focus() { }
}
public static Camera implicit operator(PhotoPistol p)
{
return p.camera;
}
}
_
ただし、is
およびas
演算子、およびSystem.Type.IsSubClassOf()
。
これは、私がいつも遭遇する多重継承の非常に便利なケースです。
ツールキットベンダーとして、公開されているAPIを変更することはできません。そうしないと、下位互換性が失われます。その結果として生じることの1つは、インターフェイスをリリースすると、実装者のコンパイルが中断されるため、インターフェイスに追加できないことです。唯一のオプションは、インターフェイスを拡張することです。
これは既存の顧客にとっては問題ありませんが、新しい顧客はこの階層を不必要に複雑であると見なし、最初から設計した場合、この方法で実装することを選択しません。そうしないと、下位互換性が失われます。 。インターフェイスが内部の場合は、インターフェイスに追加して実装者を修正するだけです。
多くの場合、インターフェイスへの新しいメソッドには、明白で小さなデフォルトの実装がありますが、私はそれを提供できません。
抽象クラスを使用したいのですが、メソッドを追加する必要がある場合は、デフォルトの実装で仮想クラスを追加します。これを行う場合もあります。
もちろん、問題は、このクラスがすでに何かを拡張しているものに混在する可能性がある場合です。その場合、インターフェイスを使用して拡張インターフェイスを処理する以外に選択肢はありません。
この問題が大きく発生していると思われる場合は、代わりにリッチイベントモデルを選択します。これはおそらくC#では正しい答えだと思いますが、すべての問題がこの方法で解決されるわけではありません。単純なパブリックインターフェイスが必要な場合があります。 、およびエクステンダー用のより豊富なもの。
C#は、単一の継承、インターフェイス、および拡張メソッドをサポートしています。それらの間では、多重継承がもたらす頭痛の種なしに、多重継承が提供するほぼすべてのものを提供します。
多重継承は、私が知っている方法ではCLRでサポートされていないため、C++(またはEiffel、言語が特別に設計されている場合はより適切にサポートされる可能性があります)のように効率的な方法でサポートできるとは思えません。 MIの場合)。
多重継承の優れた代替手段は、特性と呼ばれます。これにより、さまざまな動作単位を1つのクラスにまとめることができます。コンパイラーは、単一継承型システムのコンパイル時拡張としてトレイトをサポートできます。クラスXに特性A、B、およびCが含まれていることを宣言するだけで、コンパイラーは要求された特性を組み合わせてXの実装を形成します。
たとえば、IList(of T)を実装しようとしているとします。 IList(of T)のさまざまな実装を見ると、まったく同じコードのいくつかを共有していることがよくあります。トレイトが登場しました。共通コードを含むトレイトを宣言するだけで、実装に他の基本クラスがすでにある場合でも、IList(of T)の任意の実装でその共通コードを使用できます。構文は次のようになります。
/// This trait declares default methods of IList<T>
public trait DefaultListMethods<T> : IList<T>
{
// Methods without bodies must be implemented by another
// trait or by the class
public void Insert(int index, T item);
public void RemoveAt(int index);
public T this[int index] { get; set; }
public int Count { get; }
public int IndexOf(T item)
{
EqualityComparer<T> comparer = EqualityComparer<T>.Default;
for (int i = 0; i < Count; i++)
if (comparer.Equals(this[i], item))
return i;
return -1;
}
public void Add(T item)
{
Insert(Count, item);
}
public void Clear()
{ // Note: the class would be allowed to override the trait
// with a better implementation, or select an
// implementation from a different trait.
for (int i = Count - 1; i >= 0; i--)
RemoveAt(i);
}
public bool Contains(T item)
{
return IndexOf(item) != -1;
}
public void CopyTo(T[] array, int arrayIndex)
{
foreach (T item in this)
array[arrayIndex++] = item;
}
public bool IsReadOnly
{
get { return false; }
}
public bool Remove(T item)
{
int i = IndexOf(item);
if (i == -1)
return false;
RemoveAt(i);
return true;
}
System.Collections.IEnumerator
System.Collections.IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
IEnumerator<T> GetEnumerator()
{
for (int i = 0; i < Count; i++)
yield return this[i];
}
}
そして、あなたはこのような特性を使用します:
class MyList<T> : MyBaseClass, DefaultListMethods<T>
{
public void Insert(int index, T item) { ... }
public void RemoveAt(int index) { ... }
public T this[int index] {
get { ... }
set { ... }
}
public int Count {
get { ... }
}
}
もちろん、私はここで表面を引っ掻いているだけです。より完全な説明については、論文 特性:構成可能な行動単位 (PDF)を参照してください。
Rust言語(Mozillaから)は興味深い方法でトレイトを実装しました:トレイトがデフォルトのインターフェース実装に似ていることに気づいたので、インターフェースとトレイトを単一の機能(トレイトと呼びます)に統合しました)トレイトとデフォルトのインターフェース実装(Java現在))の主な違いは、パブリックでなければならない従来のインターフェースメソッドとは異なり、トレイトにはプライベートメソッドまたは保護されたメソッドを含めることができることです。 notが単一の機能に統合されていない場合、別の違いは、インターフェースへの参照はできるが、トレイトへの参照はできないことです。 ;特性自体はタイプではありません。
私は実際に1つの特定の理由で多重継承を見逃しています...破棄パターン。
ディスポーズパターンを実装する必要があるたびに、「いくつかの仮想オーバーライドを使用してディスポーズパターンを実装するクラスから派生できればいいのにと思います」と自分に言い聞かせます。 IDisposeを実装するすべてのクラスに同じ定型コードをコピーして貼り付けますが、嫌いです。
はい!はい!はい!
真剣に、私は私のキャリア全体でGUIライブラリを開発してきました、そしてMI(多重継承)はこのFARをSI(単一継承)より簡単にします
最初にC++で SmartWin ++ (MIが頻繁に使用されます)を実行し、次にGaia Ajaxを実行し、最後に Ra-Ajax を実行しました。それらの場所の1つはGUIライブラリです...
そして、MIが「複雑すぎる」などと主張する議論は、ほとんどの場合、言語戦争を構築しようとする人々によって行われ、たまたま「現在MIを持っていない」キャンプに属しています...
関数型プログラミング言語(LISPなど)が(「非Lispers」によって)非関数型プログラミング言語の支持者によって「複雑すぎる」と教えられてきたように...
人々は未知のものを恐れています...
MIルール!
私はあなたが述べている理由だけで多重継承に反対します。開発者はそれを誤用します。ユーティリティクラスから継承するすべてのクラスで十分な問題が発生しているので、あまり入力しなくてもすべてのクラスから関数を呼び出すことができます。多重継承は多くの状況で不正なコードにつながることを知っています。 GoToについても同じことが言えます。これは、GoToの使用が非常に嫌われている理由の1つです。多重継承は、GoToのように、いくつかの良い用途があると思います。両方が適切な場合にのみ使用される理想的な世界では、問題はありません。しかし、世界は理想的ではないので、悪いプログラマーを自分たちから守る必要があります。
便利な場合もありますが、C#に多重継承がないことを嬉しく思います。代わりに私が見たいのは、インターフェイスメソッドのデフォルトの実装を提供する機能です。あれは:
_interface I
{
void F();
void G();
}
class DefaultI : I
{
void F() { ... }
void G() { ... }
}
class C : I = DefaultI
{
public void F() { ... } // implements I.F
}
_
この場合、_((I)new C()).F()
_はC
のI.F()
の実装を呼び出し、_((I)new C()).G()
_はDefaultI
のI.G()
の実装を呼び出します。 。
これを言語に追加する前に言語設計者が解決しなければならない問題はたくさんありますが、どれも非常に難しい問題ではなく、結果は多重継承を望ましいものにする多くのニーズをカバーします。
更新
私を投票するすべての人に、単一継承の言語に簡単に移植できない多重継承の例を見せてくれるように挑戦します。誰かがそのようなサンプルを見せることができない限り、私はそれが存在しないと主張します。大量のC++コード(MH)をJava(no-MH)に移植しましたが、C++コードがどれだけMHを使用しても、それは問題にはなりませんでした。
これまでのところ、多重継承が 任意の利点 投稿で言及した他の手法(インターフェイスとデリゲートを使用すると、コードやオーバーヘッドなしでまったく同じ結果を得ることができます)よりも優れていることを証明することはできませんでしたよく知られているいくつかの欠点( ダイヤモンドの問題 最も厄介なものです)。
実際、多重継承は通常悪用されます。 OO設計を使用して、現実世界をクラスにモデル化する場合、多重継承が実際に意味をなすポイントに到達することは決してありません。多重継承の有用な例を提供できますか?私がこれまで見てきた例は、実際には「間違っている」ものであり、サブクラスを作成します。これは、実際には単なる追加のプロパティであり、したがって実際にはインターフェイスです。
Sather を見てください。これはプログラミング言語であり、インターフェイスには多重継承がありますが(ダイヤモンドの問題は発生しません)、インターフェイスではないクラスには継承がまったくありません。それらはインターフェースを実装することしかできず、他のオブジェクトを「含める」ことができるため、これらの他のオブジェクトはそれらの固定部分になりますが、それは継承と同じではなく、むしろ委任の形式です(オブジェクトを含めることによって「継承」されるメソッド呼び出しは実際、オブジェクトにカプセル化されたこれらのオブジェクトのインスタンスに転送されるだけです)。この概念は非常に興味深いものだと思います。実装の継承がまったくなくても、完全にクリーンなOO言語)を使用できることを示しています。
番号
(投票用)
多重継承の代わりに、 mixins を使用できます。これはより良い解決策です。
C#が最初にアルファ/ベータリリースとして利用可能になって以来、私はC#を使用しており、多重継承を見逃したことはありません。 MIはいくつかの点で優れていますが、同じ結果を達成する方法はほとんど常に他にもあります(実際には、より単純な方法や、より理解しやすい実装を作成する方法もあります)。
dataFlex 4GL v3 +に関する本当に素晴らしく(当時)斬新なものの1つ(私は知っています、私は知っています、データwhat?)はそのサポートでしたfor mixin継承-他のクラスのメソッドをクラスで再利用できます。クラスがこれらのメソッドが使用するプロパティを提供している限り、それは問題なく機能し、「ダイヤモンドの問題」やその他の多重継承の「落とし穴」について心配する必要はありませんでした。
特定の種類の抽象化と構築の問題を単純化するので、C#でこのようなものを見たいと思います
一般に多重継承は有用であり、多くのOO言語はそれを何らかの方法で実装します(C++、Eiffel、CLOS、Python ...)。それは不可欠ですか?いいえ。持っているのはいいことですか? ? はい。
私はC++が好きです。私はJava、C#などを使用しました。このようなOO環境でプログラムが洗練されるにつれて、多重継承が失われることに気付きます。これは私の主観的な経験です。
それは驚くべきスパゲッティコードを作ることができます...それは驚くほどエレガントなコードを作ることができます。
C#のような言語は、プログラマーに選択肢を与えるべきだと思います。 多分複雑すぎるからといって、それがそうなる複雑すぎるという意味ではありません。プログラミング言語は、プログラマーが望むものを構築するためのツールを開発者に提供する必要があります。
開発者がすでに作成したAPIを使用することを選択しますが、使用することもありません。
C# implicits を指定すると、多重継承、またはその問題に関する継承を見逃すことはありません。
同僚が、動的コンパイルを使用してC#で多重継承のようなものを取得する方法についてこのブログを書きました。
いいえ、私はしません。私は他のすべてのOO機能を使用して、必要なものを開発しています。インターフェイスとオブジェクトのカプセル化を使用しており、実行したいことに制限されることはありません。
継承を使わないようにしています。毎回できることが少なくなります。
本当にシンプルだと思います。他の複雑なプログラミングパラダイムと同じように、それを誤用して自分を傷つける可能性があります。オブジェクトを誤用することはできますか(そうです!)、それはOOそれ自体が悪いという意味ではありません。
MIと同様に。継承されたクラスの大きな「ツリー」がない場合、または同じ名前のメソッドを提供するクラスが多数ない場合は、MIで完全に問題ありません。実際、MIが実装を提供するため、委任されたオブジェクトにメソッドを再コーディングまたはカットアンドペーストする必要があるSI実装よりも優れていることがよくあります。このような場合は、コードが少ない方がよいでしょう。インターフェイスの継承を通じてオブジェクトを再利用しようとすると、コードの共有を大混乱にする可能性があります。そして、そのような回避策は正しいにおいがしません。
.NETの単一継承モデルには欠陥があると思います。インターフェースのみ、またはMIのみを使用する必要がありました。 「半分と半分」(つまり、単一の実装の継承と複数のインターフェースの継承)を持つことは、本来あるべきよりも混乱を招き、まったくエレガントではありません。
私はMIのバックグラウンドを持っており、それを恐れたり、やけどを負ったりすることはありません。
私はこれを数回ここに投稿しましたが、それは本当にクールだと思います。 MIを偽造する方法を学ぶことができます ここ 。また、この記事では、意図していなくてもMIがなぜそんなに苦痛なのかを強調していると思います。
私はそれを見逃したり必要としたりしません。私は目的を達成するためにオブジェクトの構成を使用することを好みます。それは本当に記事のポイントでもあります。
私自身もC++で複数の継承を使用しましたが、特に祖父母を共有する2つの基本クラスがある場合は、問題が発生しないように、何をしているのかを本当に知っておく必要があります。次に、仮想継承の問題が発生する可能性があります。チェーンを呼び出すすべてのコンストラクターを宣言する必要があります(これにより、バイナリの再利用がはるかに困難になります)... itcan混乱する。
さらに重要なことに、CLIが現在構築されている方法では、MIを簡単に実装できません。彼らが望むなら彼らはそれを行うことができると確信していますが、私は複数の継承よりもCLIで見たいと思う他のことを持っています。
私が見たいのは、null許容でない参照型のような Spec# のいくつかの機能が含まれています。また、パラメーターをconstとして宣言できるようにし、関数constを宣言できるようにすることで、オブジェクトの安全性を高めたいと思います(つまり、オブジェクトの内部状態がメソッドとコンパイラはあなたを二重にチェックします)。
単一継承、複数インターフェース継承、ジェネリックス、拡張メソッドの間で、必要なことはほとんど何でもできると思います。 MIを望んでいる人のために何かが改善できるとしたら、より簡単な集約と構成を可能にするある種の言語構造が必要だと思います。そうすれば、共有インターフェースを持つことができますが、その後、通常は継承するクラスのプライベートインスタンスに実装を委任します。今のところ、これには多くの定型コードが必要です。そのためのより自動化された言語機能があると、非常に役立ちます。
いいえ、私たちはそれから離れました。あなたは今それを必要としています。
十分なROIを提供しないと、事態が複雑になりすぎると思います。継承ツリーが深すぎる.NETコードを食い物にする人はすでにいます。人々が多重継承を行う力を持っていれば、私は残虐行為を想像することができます。
私はそれが可能性を秘めていることを否定しませんが、私はただ十分な利益を見ていません。
ダイヤモンドの問題が解決されない限り、いいえ。これが解決されなくなるまで、合成を使用できます。
確かに役に立つ場合もありますが、必要だと思うときはほとんど必要ないことがわかりました。
C#での多重継承を見逃すことはほとんどありません。
複数の継承を使用して実際のドメインモデルを定義している場合、私の経験では、単一の継承といくつかの優れたデザインパターンを使用して、同じように優れたデザインを作成できることがよくあります。
多重継承が本当に価値があると私が思う場所のほとんどは、それがドメイン自体ではなく、MIを使用する必要があるいくつかのテクノロジー/フレームワークの制約である場所です。 ATLを使用したCOMオブジェクトの実装は、多重継承を使用してCOMオブジェクトに必要なすべての必要なインターフェイスを実装する非常に良い例であり、醜い(.NETと比較して)テクノロジに対する洗練されたソリューションです。 C++フレームワークであるという観点からはエレガントです!
しかし、今は多重継承を使用できる状況にあります。他のドメインオブジェクトから機能を派生させる必要があるクラスが1つありますが、クロスAppDomain呼び出しでもアクセスできる必要があります。これは、MarshalByRefObjectから継承する必要があることを意味します。したがって、この特定のケースでは、MarshalByRefObjectと特定のドメインオブジェクトの両方から派生させたいと思います。ただし、これは不可能であるため、クラスはドメインオブジェクトと同じインターフェイスを実装し、呼び出しを集約インスタンスに転送する必要があります。
しかし、これは、私が言ったように、テクノロジー/フレームワークが制約を課す場合であり、ドメインモデル自体ではありません。
インターフェイスに実装があれば、コードが少なくて簡単になります。しかし、それらは適切に使用されるよりも誤用される可能性がありますか?それがどのように適切に行われるかを理解するまで、あなたはそれを見逃すことはないと思います。多重継承は安全ではないという議論に悩まされます。そうではない。しかし、ほとんどすべての言語実装はそうです。しかし、私たちはそれが必要ですか?、私は知りません。
私は戻り値の型の共分散を好みます。これは簡単に追加でき(オーバーライドされたメソッドの戻り値の型でルールを緩和するだけです)、常に安全です。
例:
class shape {}
class circle : shape {}
interface part {
shape Form();
}
interface wheel : part {
circle Form();
}
ダイヤモンド問題(クラスでの多重継承が悪い大きな理由)が適切に解決され、その解決策がインターフェイスを使用するのと同じくらい良い場合まで、私はノーと言います。一言で言えば、ダイヤモンド問題は基本的に、クラスを介した多重継承によるクラス内のデータ、メソッド、およびイベントの潜在的なあいまいさに関係しています。
P/S難しいという理由だけで、ひどく必要なプログラミングソリューションを「めったに」避けません。 「難しさ」は、複数の継承がないことの言い訳にはなりません。マルチスレッドは難しいですが、C#で利用できます。多重継承がないことの大きな理由は、ダイヤモンドの問題によるものです( http://en.wikipedia.org/wiki/Diamond_problem )。より良い代替案(人々はさまざまな方法で解決し、考えるので、1つの解決策が悪い考えである場合があります)についてのコメントには、より巧妙な推論が適用され、適切なオプションがすでに存在します(ADO.NETがトリックを実行して成熟しているときにLINQを作成する理由...お互いの強さに。)
多重継承を導入すると、C++の古いダイヤモンドの問題に再び直面します...
しかし、それが避けられないと思う人のために、合成によって複数の継承効果を導入することができます(オブジェクト内の複数のオブジェクトを合成し、合成されたオブジェクトに責任を委任して返すパブリックメソッドを公開します)...
では、なぜわざわざ多重継承を持ち、コードを避けられない例外に対して脆弱にするのか...