web-dev-qa-db-ja.com

継承とインターフェイスがインスタンスメンバーに制限されているのはなぜですか?

免責事項:ルールはほとんどのOO言語でほぼ同じだと思いますが、私はC#に最も精通しているので、この特定の言語に関連します。

静的メンバーがOOインスタンスと同じ原則を適用する場合、属性とリフレクションの使用を大幅に減らすことができると思います。

静的プロパティとメソッドは、newキーワードを使用して子孫によって「オーバーライド」できますが、インターフェイスを介して強制的に実装することはできません。また、「base」キーワードを使用することもできません(ベースタイプの明示的な命名のみがサポートされます)。さらに悪いことに、メンバーはインスタンスを介して参照することはできませんが、常に正確な型で特別に修飾する必要があります。

コンストラクターの「継承」サポートも制限されており、呼び出す基本コンストラクターを選択できますが、インターフェイスで定義することにより、特定のパラメーターを受け取るコンストラクターをオブジェクトにサポートさせることはできません。

これまでのところ、この質問に答えるために私が見つけることができる最良のヒントは スタックオーバーフローに関するこれ (別の質問に答える)です。

彼の答えの中で、イェルクはオブジェクト指向に適用される3つの原則を指摘しています。

  • メッセージング、
  • ローカルの保持と保護、および状態プロセスの非表示、
  • すべてのものの極端な遅延バインディング。

あるメソッドから別のメソッドに状態フルクラスをオブジェクトとして渡したい場合、これらの原則が理にかなっていることを完全に理解しました。しかし、私の意見では、継承の原則、つまり基本メソッドを代替または追加の機能でオーバーライドすることも、ステートレスなビジネスロジックにとって有益である可能性があります。

オブジェクトに特定のコンストラクターを実装するように強制することが役立つ1つの例は、逆シリアル化です。たとえば、コンストラクターがXMLノードを受け入れる必要がある場合。 C#では、リフレクションを使用して動的に実装できますが、コンパイル時エラーが発生し、リフレクションボイラープレートを回避すると、多くの価値が追加されます。

静的メソッド/メンバーインターフェイスは、値の一部の側面がインスタンスとは関係なく、型固有である状況で役立ちます。たとえば、クラスには、インスタンス値に関係なく、特定のタイプのすべてのインスタンスで同じであるDisplayName、Unit、Weight、Factorまたはその他の側面を含めることができます。属性(またはアノテーション)を使用すると、インスタンスを作成せずにこれらを活用できますが、ここでも追加の定型コードがあり、コンパイル時エラーで実装を強制する方法はありません。

これらの点を回避する簡単な方法はたくさんあることを認めます。回避策を探しているわけではありません。インスタンスプロパティで静的な値を返すことは非常に一般的であり、タイプ固有の側面を読み取るためだけにオブジェクトのインスタンスを作成することはそれほどオーバーヘッドではありませんが、静的な理由の背景を知りたいです物事は体系的に除外されています。 「仮想スタック」については以前聞いたことがありますが、技術的にどのように機能するのかよくわかりません(ネット上で簡単な説明を見つけることができません)。

技術的または哲学的な性質の理由はありますか?


編集:静的インターフェースの詳細:

静的インターフェースが、リフレクションの代わりに、強く型付けされ、コンパイル時にチェックされる方法を提供できると私が考える例:

_public staticInterface MyStaticInterface
{
    constructor(XmlNode node);
    string DisplayName { get; }
    int Weight { get; }
}
_

使用法:

_public void FillNodesToolBox() 
{
    MyStaticInterface[] nodes = 
        Assembly.GetTypes().Where(t => t.Implements<MyStaticInterface>());

    for (int i = nodes.Length - 1; i >= 0 ; i--)
    {
        Console.WriteLine(string.Concat(
            nodes[i].DisplayName, " (", nodes[i].Weight.ToString(), ")"));
    }
}
_

振り返ってみると、これは実際には私のTypeオブジェクトをSystem.Typeから継承させ、静的メンバーとは異なるいくつかのプロパティを追加しているように見えます...

string.Empty != typeof(string).Emty(後者はコンパイルされません)。

答えを見つけ始めたのかもしれませんが、逆説的な円を歩いているような気がします。まだ「スナップ」していません。

3
Louis Somers

それは単に機能しません。

「仮想」静的メソッドを定義するクラスAがあるとします。次に、Aから派生したクラスBとCがあり、どちらもこの静的メソッドをオーバーライドします。次に、A.StaticMethod()を呼び出します。 BとCのどちらを呼び出す必要がありますか?

仮想メソッドがインスタンスに関連付けられている理由は、インスタンスのタイプによって、呼び出される仮想メソッドの実装が指定されるためです。この情報がない場合は、適切なメソッドを呼び出すことができません。

また、実装されている型(B.StaticMethod()やC.StaticMethod()など)で静的メソッドを呼び出す必要があると主張しようとした場合、それは仮想メソッド呼び出しではなく、すでに実装できます。問題なくC#で。

あるメソッドから別のメソッドに状態フルクラスをオブジェクトとして渡したい場合、これらの原則が理にかなっていることを完全に理解しました。しかし、私の意見では、継承の原則、つまり基本メソッドを代替または追加の機能でオーバーライドすることも、ステートレスなビジネスロジックにとって有益である可能性があります。

抽象化の観点からは、ビジネスロジックはステートレスに見えるかもしれません。したがって、インターフェイスまたはその周りの抽象クラスを設計します。しかし、その後、実行時に特定の動作をパラメーター化する必要があることに気付きます。インスタンス仮想メソッドを使用すると、これは簡単です。このパラメータ化を指定する状態を含む新しいサブタイプを作成するだけです。メソッドだけでステートレスインスタンスを作成することを妨げるものはありませんが、抽象化を変更せずに状態を追加できることは、OOPの大きな利点です。メソッドを静的にすることで、この利点を取り除くことができます。

「ステートレス」プログラミングがプロジェクトに適していると本当に思う場合は、関数型プログラミングを検討することをお勧めします。関数型プログラミングとは、最小限の状態保持でメソッドを作成することです。

2
Euphoric

インターフェイスがコンストラクターの署名や静的メソッドの存在を指示できない理由は、主に技術的なものです。

インターフェイスIのメソッドI::fooを呼び出す場合、ランタイム環境では、呼び出す必要があるのが実際にX::fooであるかY::fooであるかを知るためにメタデータが必要です。このメタデータは、メソッドが呼び出されてそのオブジェクトにアタッチされているオブジェクトに固有です。

コンストラクターと静的メソッドの問題は、使用可能なオブジェクトが(まだ)ないときに呼び出されることです。これは、コンストラクターまたは静的メソッドを呼び出すクラスを決定するためにランタイム環境で使用できるメタデータがないことを意味します。

インターフェイスが「抽象静的メンバー」を宣言できない概念的な理由は、インターフェイスの目的がオブジェクトで実行できることを指定することであり、 typeを使用します。たとえば、.NETインターフェイスIList<T>は、リストとして動作するオブジェクトを表すタイプです。

ここで、doesは、オブジェクトではなく型に対して実行できる操作のリストを指定することに意味があります。しかし、そのようなものはtypeclassと呼ばれ、インターフェースではありません。

型クラス

C#が型クラスをサポートしている場合、型クラス宣言は次のようになります。

typeclass CMonoidalList<T> of TList : IList<T>
{
    TList(IEnumerable<T> contents); // a constructor
    TList Append(TList suffix); // an instance method
}

この型クラスを実装するクラスは次のようになります。

class MyIntList : CMonoidalList<int>
{
    public MyIntList(IEnumerable<int> contents) { ... }
    public MyIntList Append(MyIntList suffix) { ... }
    ...
}

そして、この型クラスを使用するメソッドは次のようになります。

public static TList AppendString<TList>(TList list, string suffix)
    where TList : CMonoidalList<char>
{
    TList suffixList = new TList(suffix);
    return list.Append(suffixList);
}

型クラスの問題

インターフェイスと比較した場合の型クラスの欠点は、型クラスがインターフェイスのように型として意味をなさないことです。 notCMonoidalListを型として扱うコードを書くのは理にかなっています。

public static CMonoidalList<T> AppendBackwards<T>(
        CMonoidalList<T> suffix, CMonoidalList<T> prefix)
{
    return prefix.Append(suffix);
}

Appendではprefixsuffixの両方が同じである必要があるため、このコードは意味がありません。 )CMonoidalList<T>を実装するタイプですが、AppendBackwardsの引数リストはこれを表現できません。

これは、どこでもwhereを使用する必要があるため、型クラスはインターフェイスよりも使い勝手が悪いことを意味します。さらに、インターフェイスはオブジェクト指向プログラミングの重要な概念ですが、型クラスはそうではありません。

インターフェイスは型クラスよりも優れている可能性があります

あなたが考えることができるすべての型クラス対応するインターフェースを持っています。 MyStaticInterfaceに対応するインターフェースは次のようになります。

public interface MyClassyInterface<T>
{
    T New(XmlNode node);
    string DisplayName { get; }
    int Weight { get; }
}

このインターフェースには、実際にはタイプクラスよりも優れています。1つの「実装タイプ」Tに対して複数の「実装」を持つことができます。場合によっては、それぞれに個別のクラスを作成せずに、さまざまなDisplayNamesWeightsを使用したい場合があります。その場合、型クラスの代わりにインターフェースを使用すると便利です。

これの1つの欠点は、実装タイプから自動的に取得するのではなく、実際にインターフェイスの実装を渡す必要があることです。もう1つの欠点は、1つの「実装タイプ」に対して複数の「実装」を持つことができるという事実です。タイプごとに1つの実装に制限したい場合があります。

他の言語

ほとんどの言語には型クラスがありません。おそらく、実装にコストがかかると考えられているためです(一般に、ほとんどの機能の実装にはコストがかかるため)それほど多くのメリットはありません。

Haskellには、すべての長所と短所とともに、本格的な型クラスがあります。 Haskellプログラマーは型クラスを頻繁に使用し、一般的に不利な点をそれほど大きな問題とは考えていません。

Scalaには型クラスはありませんが、「偽造」できる機能があります。 Scalaで「型クラス」を記述したい場合は、それをインターフェースに変換し(前述のとおり)、thatと記述します。 「型クラスの実装」が必要な場合は、インターフェースの実装を作成し、その実装を「暗黙的」として宣言します。そうすれば、インターフェースの実装を明示的に渡す必要はありません。 Scalaコンパイラーはそれを完全に自動的に起こそうとします。それは通常は機能します。

1
Tanner Swett

コンストラクターの「継承」サポートも制限されており、呼び出す基本コンストラクターを選択できますが、インターフェイスで定義することにより、特定のパラメーターを受け取るコンストラクターをオブジェクトにサポートさせることはできません。

定義上、インターフェースはその基礎となる型についての知識を持っていません。技術的には、基になる型である必要はありません。インターフェイスは単に関数のコレクションです。主流のOOP言語にはインターフェース用の組み込みメカニズムがあり、それらをクラスに「アタッチ」することを期待していますが、既知のシグネチャを持つデリゲート/ラムダ/関数の束をに取り込むこともできますいくつかのコンテナタイプとそれもインターフェイスになります。

オブジェクトに特定のコンストラクターを実装するように強制することが役立つ1つの例は、逆シリアル化です。たとえば、コンストラクターがXMLノードを受け入れる必要がある場合。

私の意見では、これは悪い考えです。クラスを別の方法でシリアル化/逆シリアル化する場合はどうなりますか?または一度に複数の方法?クラスには変更する理由が1つだけあるはずであり、それ自体をシリアル化する方法を知っていると、2番目の理由が得られます。確かに、シリアル化はカプセル化を破る必要があるため興味深いケースですが、それでも外部で処理するのが最善だと思います。

静的メソッド/メンバーインターフェイスは、値の一部の側面がインスタンスとは関係なく、型固有である状況で役立ちます。たとえば、クラスには、インスタンス値に関係なく、特定のタイプのすべてのインスタンスで同じであるDisplayName、Unit、Weight、Factorまたはその他の側面を含めることができます。

静的フィールドは、定義上、正確に1つのことを指します。そのため、クラスの1つのインスタンスに関連付けられていません。静的であると同時にオーバーライドできるものはありません。何かをオーバーライドすることの意味を考えてください。アクセスしようとしているフィールドを見つける方法を知っている何かがあります。これにより、インスタンスフィールドを取得しようとしているものは何でも作成されます。クラスのインスタンスのインスタンスフィールドではないかもしれませんが、クラス自体をオブジェクトに変えることになります。

最後に、継承はすでに問題のあるメカニズムです。一度に1つのものからしか継承できないため(そうでない場合は、別のワームの缶を開く)、オーバーライドできるものに制限を設けない場合は、基本クラスに変更を加えると、本質的に構成可能ではありません。単独で検討すると、サブクラスが破損する可能性があります。静的フィールドをオーバーライドできることは、非常に疑わしい値になります。

0
Doval