c# ジェネリック 引数 - List <T>から継承しないのはなぜですか?




13 Answers

最後に、リストに何かをラップすることを提案します:

それが正しい方法です。 「不必要に言葉遣い」は、これを見る悪い方法です。 my_team.Players.Countを記述すると、明示的な意味があります。 あなたは選手を数えたいと思う。

my_team.Count

..何も意味しない。 何を数えますか?

チームはリストではなく、単なるプレーヤーリスト以上のもので構成されています。 チームが選手を所有しているので、選手は選手の一員でなければなりません(メンバー)。

あなたが過度に冗長であることを本当に心配しているなら、いつでもチームからプロパティを公開することができます:

public int PlayerCount {
    get {
        return Players.Count;
    }
}

..されるもの:

my_team.PlayerCount

これは今や意味を持ち、 デメテルの法則を遵守しています。

複合再利用の原則を遵守することも考慮する必要があります。 List<T>継承することで、チームプレーヤーのリストであり、不要なメソッドを公開しているということになります。 これは間違っています - あなたが述べたように、チームは選手のリスト以上のものです:名前、マネージャー、ボードメンバー、トレーナー、医療スタッフ、給料キャップなどがあります。チームクラスに選手のリストが含まれていると、 「チームに選手のリストがある 」と言っているが、他のものも持つことができる。

格納 初期化 順番

私のプログラムを計画するとき、私はしばしば以下のような思考の連鎖から始めます:

サッカーチームは、サッカー選手のリストに過ぎません。 したがって、私はそれを次のように表現すべきです:

var football_team = new List<FootballPlayer>();

このリストの発注は、選手が選出された順番を表します。

しかし、後でチームには、記録されなければならない単なる選手のリストの他に、他の特性もあることが分かっています。 たとえば、今シーズンの得点の合計、現在の予算、統一色、チーム名を表すstringなど。

だから私は思う:

さて、サッカーチームは選手のリストに似ていますが、さらに名前( string )とランニング合計( int )を持っています。 .NETはフットボールチームを格納するためのクラスを提供しないので、私は自分のクラスを作成します。 最も似て関連性の高い既存の構造はList<FootballPlayer>ですので、私はそれを継承します:

class FootballTeam : List<FootballPlayer> 
{ 
    public string TeamName; 
    public int RunningTotal 
}

しかし、 List<T>から継承すべきではないというガイドラインがあることが判明しました。 私はこのガイドラインに2つの点で徹底的に混乱しています。

何故なの?

明らかに、 Listはパフォーマンスのために何らかの形で最適化されています 。 どうして? Listを拡張すると、どのようなパフォーマンス上の問題が発生しますか? 何が壊れますか?

私が見たもう1つの理由は、 ListがMicrosoftによって提供されていることです。私はそれを制御できないため、後で「公開API」を公開した後で変更することはできません 。 しかし、私はこれを理解するのに苦労します。 パブリックAPIとは何ですか、なぜ私は気にする必要がありますか? 私の現在のプロジェクトでこのパブリックAPIを使用することができない場合、このガイドラインを無視しても問題ありませんか? 私がListから継承しそれが公開されたAPIが必要な場合は、どのような困難がありますか?

それはなぜ重要なのでしょうか? リストはリストです。 何が変わる可能性がありますか? 私はおそらく何を変えたいのですか?

最後に、MicrosoftがListから継承しないようにしたら、どうしてクラスをsealedないのですか?

私は他に何を使うべきですか?

どうやら、カスタムコレクションの場合、MicrosoftではListクラスではなくCollectionクラスを提供しています。 しかし、このクラスは非常に裸であり、 例えばAddRangeような多くの有用なものはありません。 jvitor83の答えは、その特定のメソッドのパフォーマンスの理論的根拠を提供しますが、遅いAddRangeAddRangeなしより優れていませAddRangeか?

Collectionから継承するのは、 List継承するよりももっと仕事であり、私は何の利益も見ません。 確かにマイクロソフトでは何の理由もなく余計な作業をするようには言わなかったので、何かを誤解しているような気持ちにはならず、 Collectionを継承することは私の問題の正しい解決策ではありません。

私はIList実装などの提案を見てきました。 ちょうどいいえ。 これは何も得られない定型コードの数十行です。

最後に、 Listに何かをラップすることを提案します:

class FootballTeam 
{ 
    public List<FootballPlayer> Players; 
}

これには2つの問題があります。

  1. それは私のコードを不必要に冗長にします。 my_team.Players.Countではなくmy_team.Players.Countを呼び出す必要があります。 ありがたいことに、C#ではインデクシングを透明にするためのインデクサーを定義し、内部Listすべてのメソッドを転送することができます...しかし、それはたくさんのコードです! そのすべての仕事のために私は何を得ますか?

  2. それはまったく平凡ではありません。 サッカーチームは選手のリストを持っていません。 それ選手のリストです。 あなたは「John McFootballerがSomeTeamの選手に加わった」とは言わない。 あなたは「ジョンはSomeTeamに加わった」と言う。 "文字列の文字"に文字を追加しないで、文字列に文字を追加します。 あなたは図書を図書館の本に追加せず、図書を図書館に追加します。

「ボンネットの下で」何が起こるかは「Yを内部リストに追加する」と言うことができますが、これは世界を考える直感的な方法のように思えます。

私の質問(要約)

「論理的に」(つまり「人間の心に」)というデータ構造を表現する正しいC#の方法は、いくつかの鐘や笛のあるlistです。

List<T>から継承することは常に容認できませんか? いつ受け入れられますか? なぜ/なぜですか? List<T>から継承するかどうかを決定する際に、プログラマが考慮する必要があるのは何ですか?




class FootballTeam : List<FootballPlayer> 
{ 
    public string TeamName; 
    public int RunningTotal;
}

これまでのコードは、路上でフットボールをしている人たちの集まりを意味しています。 何かのようなもの:

とにかく、このコード(私の答えから)

public class FootballTeam
{
    // Football team rosters are generally 53 total players.
    private readonly List<T> _roster = new List<T>(53);

    public IList<T> Roster
    {
        get { return _roster; }
    }

    public int PlayerCount
    {
    get { return _roster.Count(); }
    }

    // Any additional members you want to expose/wrap.
}

意味:これは管理、選手、管理者などがいるサッカーチームです。

これはあなたのロジックが写真にどのように表示されるかです...




誰もが指摘しているように、選手のチームは選手のリストではありません。 このミスは世界中の多くの人々によって、おそらく様々なレベルの専門知識によって作られています。 この問題のように、問題は微妙で時には非常に重大です。 そのようなデザインは、 Liskov Substitution Principleに違反するため、悪いものです。 インターネットには、この概念を説明する多くの良い記事があります。たとえば、 http://en.wikipedia.org/wiki/Liskov_substitution_principle

要約すると、クラス間の親子関係には2つのルールが保持されます。

  • 子供は親を完全に定義するものよりも特性を要求してはならない。
  • 親は子を完全に定義するものに加えて特性を要求してはならない。

言い換えると、Parentは子供の必要な定義であり、子供はParentの十分な定義です。

ここでは、1つの解決策を考え、そのような間違いを避けるために役立つ上記の原則を適用する方法があります。 親クラスのすべての操作が派生クラスに対して構造的および意味的に有効であるかどうかを検証することによって、仮説をテストする必要があります。

  • サッカーチームはサッカー選手のリストですか? (リストのすべてのプロパティは同じ意味のチームに適用されます)
    • チームは同種の団体の集まりですか? はい、チームは選手の集まりです
    • チームの状態を説明するプレイヤーを含む順番であり、明示的に変更されない限り、そのシーケンスが保持されることをチームは保証していますか? いいえ、いいえ
    • チームのシーケンシャルポジションに基づいてプレーヤーのインクルード/ドロップが予想されますか? いいえ

ご覧のように、リストの最初の特徴だけがチームに適用されます。 したがって、チームはリストではありません。 リストは、チームをどのように管理するかに関する実装の詳細なので、プレーヤオブジェクトを格納し、Teamクラスのメソッドで操作する必要があります。

この時点では、チームクラスは、私の意見では、Listを使って実装すべきではないことを述べたいと思います。 ほとんどの場合、Setデータ構造(たとえば、HashSet)を使用して実装する必要があります。




まず第一に、使いやすさと関係しています。 継承を使用する場合、 Teamクラスはオブジェクト操作用に設計された動作(メソッド)を公開します。 たとえば、 AsReadOnly()またはCopyTo(obj)メソッドは、チームオブジェクトに意味をなさない。 AddRange(items)メソッドの代わりに、おそらくよりわかりやすいAddPlayers(players)メソッドが必要です。

LINQを使用する場合は、 ICollection<T>IEnumerable<T>などの汎用インターフェイスを実装する方が理にかなっています。

言及したように、構図は正しいことです。 プライベート変数として選手のリストを実装するだけです。




サッカーチームサッカー選手のリストではありません。サッカーチーム、サッカー選手のリストで構成されています!

これは論理的に間違っています:

class FootballTeam : List<FootballPlayer> 
{ 
    public string TeamName; 
    public int RunningTotal 
}

これは正しいです:

class FootballTeam 
{ 
    public List<FootballPlayer> players
    public string TeamName; 
    public int RunningTotal 
}



あなたの質問を書き直しましょう。異なる視点からの主題を見るかもしれません。

私がサッカーチームを代表する必要があるとき、私はそれが基本的に名前であることを理解する。「イーグルス」のように

string team = new string();

その後、チームにも選手がいることが分かりました。

なぜ私はストリング型を拡張して、選手のリストも保持できないのですか?

問題へのあなたの入り口は任意です。チーム何を持っているのか(プロパティ)が何でないかを考えてみてください。

その後、他のクラスとプロパティを共有するかどうかを確認できます。そして相続について考える。




人々が言えるようにするか

myTeam.subList(3, 5);

全く意味をなさない?そうでなければ、それはリストであってはならない。




ここではすばらしい答えがたくさんありますが、私が触れていないことに触れたいと思います。オブジェクト指向設計とは、オブジェクトに力を与えることです

すべてのルール、追加の作業、および内部の詳細を適切なオブジェクトにカプセル化したいとします。このようにして、このオブジェクトとやりとりする他のオブジェクトは、すべてそれについて心配する必要はありません。実際には、一歩一歩進んで、他のオブジェクトがこれらの内部をバイパスすることを積極的に防止する必要があります。

あなたが継承するとList、他のすべてのオブジェクトがあなたをリストとして見ることができます。彼らはプレーヤーの追加と削除の方法に直接アクセスできます。そしてあなたはあなたのコントロールを失ってしまいます。例えば:

プレイヤーが退職したか、辞任したのか、解雇されたのかを知ることによって、プレーヤーが離れるときを区別したいとします。あなたRemovePlayerは、適切な入力enumを取るメソッドを実装することができます。しかし、継承によってList、あなたはへの直接アクセスを防止することができないであろうRemoveRemoveAllでも、とClear。その結果、あなたは実際にあなたのクラスを拒否しましたFootballTeam

カプセル化に関する追加の考え方...あなたは以下の懸念を提起しました:

それは私のコードを不必要に冗長にします。my_team.Countではなくmy_team.Players.Countを呼び出す必要があります。

あなたは間違いなく、すべてのクライアントがチームを使用するために不必要に冗長になります。しかし、その問題はあなたがList Playersすべてのものや雑貨に晒されていることに比べて非常に小さいので、彼らはあなたの同意なしにあなたのチームと仲良くすることができます。

あなたは言うことを続けます:

それはまったく平凡ではありません。サッカーチームは選手のリストを持っていません。それは選手のリストです。あなたは「John McFootballerがSomeTeamの選手に加わった」とは言わない。あなたは「ジョンはSomeTeamに加わった」と言う。

あなたは最初のビットについて間違っている:単語 'リスト'をドロップし、チームが選手を持っていることは実際に明らかです。
しかし、あなたは頭の爪を2番目の頭で打つ。あなたはクライアントに電話をかけたくありませんateam.Players.Add(...)。あなたは彼らに電話してほしいateam.AddPlayer(...)。そして、あなたの実装は(おそらく他のものの中で)Players.Add(...)内部的に呼び出すでしょう。

うまくいけば、カプセル化があなたのオブジェクトに力を与える目的にとってどれほど重要かを知ることができます。あなたは、各クラスが他のオブジェクトからの干渉を恐れることなく、その仕事をうまくやり遂げることを許可したいと考えています。




これは私に "Is a"対 "a has"のトレードオフを思い出させる。スーパークラスから直接継承する方が簡単な場合もあります。それ以外の場合は、スタンドアロンクラスを作成し、メンバ変数として継承したクラスを追加する方が理にかなっています。それでもクラスの機能にアクセスすることはできますが、インタフェースまたはクラスから継承した他の制約にはバインドされません。

あなたはどちらですか?多くのものと同様に、それは文脈にもよります。私が使用するガイドは、別のクラスから継承するためには、本当に "is a"の関係でなければならないということです。したがって、BMWと呼ばれるクラスを書くと、BMWは本当に車なので、Carから継承することができます。馬は実際には哺乳類であり、哺乳類の機能性は馬に関連しているはずであるため、馬のクラスは哺乳動物のクラスを継承することができます。しかし、チームがリストであると言うことができますか?私が言うことから、チームのようには見えません。本当に「リスト」です。したがって、この場合、Listをメンバー変数として使用します。




ガイドラインで言うことは、パブリックAPIが、リスト、セット、ディクショナリ、ツリーなどを使用しているかどうかの内部設計の決定を明らかにしてはいけないということです。「チーム」は必ずしもリストではありません。それをリストとして実装することもできますが、パブリックAPIのユーザーは、基礎を知る必要性に応じてクラスを使用する必要があります。これにより、決定を変更し、パブリックインターフェイスに影響を与えずに別のデータ構造を使用することができます。




私はこれらの答えのほとんどが複雑な比較をしていませんが、私はこの状況を処理する方法を共有したいと思います。拡張IEnumerable<T>することで、Teamクラスのすべてのメソッドとプロパティを公開することなく、クラスがLinqクエリ拡張をサポートできるようにすることができますList<T>

class Team : IEnumerable<Player>
{
    private readonly List<Player> playerList;

    public Team()
    {
        playerList = new List<Player>();
    }

    public Enumerator GetEnumerator()
    {
        return playerList.GetEnumerator();
    }

    ...
}

class Player
{
    ...
}



私はちょうどEiffelの発明者であり、契約によるデザインのBertrand Meyerがまぶたを打つことなしにTeam継承するだろうと付け加えましたList<Player>

オブジェクト指向のソフトウェア構築の彼の著書では、長方形のウィンドウに子ウィンドウを持つことができるGUIシステムの実装について議論しています。彼は単に持っているWindow両方を継承Rectangleし、Tree<Window>実装を再利用します。

しかし、C#はエッフェルではありません。後者は、複数の継承と機能の名前の変更をサポートしています。C#では、サブクラス化すると、インタフェースと実装の両方を継承します。実装をオーバーライドできますが、呼び出し規約はスーパークラスから直接コピーされます。エッフェル塔では、しかし、あなたは、publicメソッドの名前を変更することができますので、あなたが名前を変更することができますAddし、RemoveHireし、FireあなたにTeam。インスタンスTeamがアップキャストされているList<Player>場合、呼び出し元はそれを使用AddRemoveて変更しますが、仮想メソッドHireFireが呼び出されます。




クラスよりも優先するインタフェース

クラスはクラスから派生することを避け、代わりに必要な最小限のインタフェースを実装する必要があります。

継承がカプセル化を破る

クラスから派生すると、カプセル化が中断されます

  • あなたのコレクションの実装方法に関する内部の詳細を公開します
  • 適切でないかもしれないインタフェース(パブリック関数とプロパティの集合)を宣言します

とりわけ、これはあなたのコードをリファクタリングすることをより困難にします。

クラスは実装の詳細です

クラスは、コードの他の部分から隠す必要のある実装の詳細です。ショートA中System.List又は現在および将来において適切であってもなくてもよい抽象データ・タイプの特定の実装です。

概念的には、System.Listデータ型が「リスト」と呼ばれるという事実は、赤ちゃんのビットです。A System.List<T>は、要素の追加、挿入、および削除のための償却されたO(1)操作をサポートする可変順序付けられたコレクションであり、要素の数を取得するまたはインデックスごとに要素を取得および設定するO(1)

インターフェイスが小さくなるほどコードが柔軟になります

データ構造を設計する際には、インターフェースが簡単になればなるほど、コードはより柔軟になります。これを実証するためにはLINQの強力さを見てください。

インターフェイスの選択方法

あなたが "リスト"と考えるとき、あなたは自分自身に "野球選手の集まりを代表する必要がある"と言って始めてください。ですから、これをクラスでモデル化することにしましょう。最初にすべきことは、このクラスが公開する必要のあるインタフェースの最小量を決定することです。

このプロセスを導くのに役立ついくつかの質問:

  • カウントが必要ですか?実装しない場合IEnumerable<T>
  • このコレクションは、初期化された後に変更される予定ですか?考慮しない場合IReadonlyList<T>
  • インデックスでアイテムにアクセスできることは重要ですか?検討するICollection<T>
  • コレクションにアイテムを追加する順序は重要ですか?多分それはISet<T>
  • あなたが本当にこれらのことをしたい場合は、先に進んで実装してくださいIList<T>

この方法では、コードの他の部分を野球選手コレクションの実装の詳細に結合することはなく、インターフェイスを尊重する限り、実装方法を自由に変更できます。

このアプローチをとることで、コードの読み込み、リファクタリング、再利用が容易になります。

ボイラープレートの回避に関する注意

最新のIDEにインターフェイスを実装するのは簡単です。右クリックし、 "Implement Interface"を選択します。その後、必要に応じて、すべての実装をメンバークラスに転送します。

それは、あなたが定型文をたくさん書いているのを見つけたら、あなたが必要とするよりも多くの機能を公開している可能性があるということです。あなたがクラスから継承してはならないのと同じ理由です。

あなたのアプリケーションに合ったより小さいインターフェイスを設計することもできます。また、必要なインターフェイスにこれらのインターフェイスをマップするヘルパー拡張機能をいくつか設計することもできます。これは私がLinqArrayライブラリのIArrayために自分のインタフェースで取ったアプローチです。




Related