「親x = new Child();」ではなく「親x = new Child();」は、後者を使用できる場合の悪い習慣ですか?


32

たとえば、次のようなフラグメントを作成するコードを見たことがあります。

Fragment myFragment=new MyFragment();

MyFragmentではなくFragmentとして変数を宣言します。MyFragmentはFragmentの子クラスです。このコードは次のようにすべきだと思うので、このコード行を満足していません

MyFragment myFragment=new MyFragment();

より具体的ですが、本当ですか?

または質問の一般化では、使用するのは悪い習慣ですか?

Parent x=new Child();

の代わりに

Child x=new Child();

前者をコンパイルエラーなしで後者に変更できるとしたら?


6
後者を行う場合、抽象化/一般化はもはや意味がありません。もちろん例外もあります
...-ウォルフラット

2
私が取り組んでいるプロジェクトでは、私の関数は親型を期待し、親型のメソッドを実行します。複数の子を受け入れることができ、それらの(継承およびオーバーライドされた)メソッドの背後にあるコードは異なりますが、使用している子に関係なくそのコードを実行したいと思います。
スティーブンS

4
@Walfrat実際に、具体的な型を既にインスタンス化している場合、抽象化のポイントはほとんどありませんが、抽象型を返すことができるので、クライアントコードは非常に無知です。
ジェイコブライレ

4
親/子を使用しないでください。インターフェイス/クラス(Java用)を使用します。
トールビョールンラヴンアンデルセン

4
Googleが「インターフェースへのプログラミング」で、なぜ使用するのParent x = new Child()が良いアイデアなのかについての説明を集めています。
ケビンワークマン

回答:


69

コンテキストに依存しますが、可能な限り最も抽象的な型を宣言する必要があると思います。そうすれば、コードは可能な限り一般的になり、無関係な詳細に依存しません。

例としては、LinkedListとのArrayList両方のがありListます。コードがどの種類のリストでも同じように機能する場合、サブクラスの1つに任意に制限する理由はありません。


24
まさに。例を考えList list = new ArrayList()てみてください。リストを使用するコードは、リストの種類を気にせず、リストインターフェイスの規約を満たしているだけです。将来、別のList実装に簡単に変更でき、基礎となるコードはまったく変更または更新する必要がありません。
Maybe_Factor

19
良い答えですが、可能な限り最も抽象的な型を拡張する必要があるということは、スコープ内のコードが子にキャストバックして、子固有の操作を実行しないことを意味します。
マイク

10
@Mike:言うまでもないが、そうでなければ、すべての変数、パラメーター、およびフィールドがObject
ジャックB

3
@JacquesB:この質問と回答は非常に注目されたので、明示的であることは、すべての異なるスキルレベルを持つ人々に役立つと考えていました。
マイク

1
文脈に依存するのは答えではありません。
ビラルBegueradj

11

JacquesBの答えは正しいですが、少し抽象的です。いくつかの側面をより明確に強調させてください。

コードスニペットでは、newキーワードを明示的に使用します。具体的な型に固有の可能性が高い初期化手順をさらに続行したい場合は、その特定の型で変数を宣言する必要があります。

他のどこかからアイテムを入手する場合、状況は異なりますIFragment fragment = SomeFactory.GiveMeFragments(...);。ここで、ファクトリは通常、可能な限り最も抽象的な型を返す必要があり、下位コードは実装の詳細に依存するべきではありません。


18
「少し抽象的」。バダムティシュ。
rosuav

1
これは編集できませんでしたか?
beppe9000

6

潜在的にパフォーマンスの違いがあります。

派生クラスが封印されている場合、コンパイラは、そうでなければ仮想呼び出しになるものをインライン化し、最適化または削除できます。そのため、パフォーマンスに大きな違いが生じる可能性があります。

例:ToStringは仮想呼び出しですがString、封印されています。でStringToStringとしてあなたが宣言しそうならば、何もしませんobjectそれは仮想呼び出しだあなたが宣言した場合、Stringクラスが封入されているので、コンパイラは知っていない派生クラスでメソッドをオーバーライドしたことが何-OPませんので、。同様の考慮事項がArrayListvsに適用されますLinkedList

したがって、オブジェクトの具体的なタイプがわかっている場合(そして、それを隠すカプセル化の理由がない場合)、そのタイプとして宣言する必要があります。オブジェクトを作成したばかりなので、具象型はわかっています。


4
ほとんどの場合、パフォーマンスの違いはごくわずかです。そして、ほとんどの場合(これは時間の約90%であると言われたと聞きました)、これらの小さな違いは忘れてください。このような最適化を気にする必要があるのは、コードのパフォーマンスクリティカルセクションとしてエージングすることを(できれば測定によって)知っている場合のみです。
ジュール

コンパイラは、「new Child()」が親に割り当てられた場合でも子を返すことを認識しており、知っている限り、その知識を使用してChild()コードを呼び出すことができます。
gnasher729

+1。また、私の答えであなたの例を盗みました。= P-
ナット

1
全体を意味のないパフォーマンス最適化があることに注意してください。たとえば、HotSpotは、関数に渡されるパラメーターが常に1つの特定のクラスであることに気付き、それに応じて最適化します。その仮定が間違っていることが判明した場合、メソッドは最適化されません。ただし、これはコンパイラ/ランタイム環境に大きく依存します。前回CLRをチェックしたとき、p from Parent p = new Child()は常に子であることに気付くというささいな最適化さえ行えませんでした
。– Voo

4

主な違いは、必要なアクセスのレベルです。そして、抽象化に関するすべての良い答えには動物が関係している、またはそう言われています。

あなたが少数の動物を持っているとしましょう- Cat BirdそしてDog。-今、これらの動物は、いくつかの一般的な行動を持っているmove()eat()speak()。彼らはすべて異なった食事をし、異なった話し方をしますが、私の動物が食べたり話したりする必要がある場合、私は彼らがそれをどうするかをあまり気にしません。

しかし、時々そうします。番犬や番鳥を一生懸命にしたことはありませんが、番犬は飼っています。だから誰かが私の家に侵入したとき、私は話すだけで侵入者を追い払うことはできません。私は本当に私の犬がbarえる必要があります-これは彼の話すこととは異なります。

したがって、侵入者を必要とするコードでは、実際に行う必要があります Dog fido = getDog(); fido.barkMenancingly();

しかし、ほとんどの場合、動物がうそをつく、鳴き声を出す、おやつをつぶやくなどのトリックを喜んで行うことができます。 Animal pet = getAnimal(); pet.speak();

より具体的には、ArrayListにはないメソッドがいくつかありますList。注目すべきは、trimToSize()。現在、99%のケースで、私たちはこれを気にしません。これListは、私たちが気にするほど大きなものではありません。しかし、そうなることもあります。そのため、ときどき、ArrayList実行するように具体的に要求し、trimToSize()そのバッキング配列を小さくする必要があります。

これは構築時に行う必要はないことに注意してください-確実な場合は、returnメソッドまたはキャストを介して行うこともできます。


なぜこれが下票を得るのか混乱している。個人的には標準的なようです。プログラム可能な番犬のアイデアを除いて
...-corsiKa

最良の答えを得るために、なぜページを読む必要があったのですか?評価が悪い。
バシレフス

優しい言葉をありがとう。評価システムには長所と短所がありますが、そのうちの1つは、後の回答がときどきほこりの中に残ることがあるということです。それが起こる!
corsiKa

2

一般的に、使用する必要があります

var x = new Child(); // C#

または

auto x = new Child(); // C++

ローカル変数の場合は、変数が初期化されたものと異なる型を持つ適切な理由がない限り。

(ここでは、C ++コードがスマートポインターを使用している可能性が高いという事実を無視しています。実際にはわからない複数の行がない場合とない場合があります。)

ここでの一般的な考え方は、変数の自動型検出をサポートする言語であり、それを使用するとコードが読みやすくなり、正しいことを行います(つまり、コンパイラの型システムは可能な限り最適化し、初期化を新しいものに変更できますほとんどのアブストラクトが使用されていれば、タイプも機能します)。


1
:C ++では、変数は、主に新しいキーワードを指定せずに作成されstackoverflow.com/questions/6500313/...
マリアンSpanik

1
質問はC#/ C ++ではなく、少なくとも何らかの静的型付けを持つ言語の一般的なオブジェクト指向の問題に関するものです(つまり、Rubyのようなもので尋ねるのは無意味です)。それに、これらの2つの言語でさえ、あなたが言うようにそれをするべき理由について、私は本当に見当がつきません。
AnoE

1

このコードは理解しやすく、このコードを簡単に変更できるため、優れています。

var x = new Child(); 
x.DoSomething();

意図を伝えるので良い:

Parent x = new Child(); 
x.DoSomething(); 

わかりやすいので、わかりました。

Child x = new Child(); 
x.DoSomething(); 

本当に悪いオプションの1つは、メソッドがあるParent場合にのみ使用することです。意図を誤って伝えるため、悪いです。ChildDoSomething()

Parent x = new Child(); 
(x as Child).DoSomething(); // DON'T DO THIS! IF YOU WANT x AS CHILD, STORE x AS CHILD

次に、質問で具体的に尋ねられるケースについてもう少し詳しく説明しましょう。

Parent x = new Child(); 
x.DoSomething(); 

後半を関数呼び出しにして、これを異なる形式にしましょう。

WhichType x = new Child(); 
FunctionCall(x);

void FunctionCall(WhichType x)
    x.DoSomething(); 

これは次のように短縮できます。

FunctionCall(new Child());

void FunctionCall(WhichType x)
    x.DoSomething();

ここでWhichTypeは、関数が機能することを可能にする最も基本的な/抽象的なタイプであることが広く受け入れられていると仮定します(パフォーマンスの懸念がない限り)。この場合、適切なタイプはParent、または何かParentから派生します。

この推論の行は、なぜ型を使用するのParentが良い選択であるかを説明しますが、他の選択が悪い(彼らはそうではない)天気にならない。


1

TL; DR - 使用Child上のParentローカルスコープで好ましいです。読みやすさだけでなく、オーバーロードされたメソッド解決が適切に機能し、効率的なコンパイルを可能にすることも必要です。


ローカルスコープでは、

Parent obj = new Child();  // Works
Child  obj = new Child();  // Better
var    obj = new Child();  // Best

概念的には、可能な限り多くの型情報を維持することです。にダウングレードする場合Parent、基本的には有用である可能性のある型情報を取り除くだけです。

完全な型情報を保持することには、主に4つの利点があります。

  1. コンパイラーに詳細情報を提供します。
  2. 読者に詳細情報を提供します。
  3. よりクリーンで、より標準化されたコード。
  4. プログラムロジックをより可変にします。

利点1:コンパイラーへの詳細情報

見かけの型は、オーバーロードされたメソッド解決および最適化で使用されます。

例:オーバーロードされたメソッド解決

main()
{
    Parent parent = new Child();
    foo(parent);

    Child  child  = new Child();
    foo(child);
}
foo(Parent arg) { /* ... */ }  // More general
foo(Child  arg) { /* ... */ }  // Case-specific optimizations

上記の例では、両方のfoo()呼び出しが動作しますが、ある場合には、より優れたオーバーロードメソッド解決を取得します。

例:コンパイラーの最適化

main()
{
    Parent parent = new Child();
    var x = parent.Foo();

    Child  child  = new Child();
    var y = child .Foo();
}
class Parent
{
    virtual         int Foo() { return 1; }
}
class Child : Parent
{
    sealed override int Foo() { return 2; }
}

上記の例では、両方の.Foo()呼び出しが最終的にoverrideを返す同じメソッドを呼び出します2。ちょうど、最初のケースでは、正しいメソッドを見つけるための仮想メソッドルックアップがあります。この仮想メソッドのルックアップは、2番目のケースではそのメソッドのなので必要ありませんsealed

彼の答えで同様の例を提供した@Benに感謝します

利点2:読者への詳細情報

正確なタイプを知っている、つまりChild、コードを読んでいる人にもっと多くの情報を提供し、プログラムが何をしているのかを見やすくします。

確かに、多分それは両方以来の実際のコードには問題ではないparent.Foo();child.Foo();意味をなさないが、最初の時間のためのコードを見て誰かのために、より多くの情報は、単なる便利です。

さらに、開発環境によっては、IDEはについてChildより役立つツールチップとメタデータを提供できる場合がありますParent

利点3:よりクリーンで標準化されたコード

私が最近見たほとんどのC#コード例varは、基本的には略記ですChild

Parent obj = new Child();  // Sub-optimal
Child  obj = new Child();  // Optimal, but anti-pattern syntax
var    obj = new Child();  // Optimal, clean, patterned syntax "everyone" uses now

var宣言文を見ると、ただ見えます。状況的な理由がある場合は素晴らしいですが、そうでない場合はアンチパターンに見えます。

// Clean:
var foo1 = new Person();
var foo2 = new Job();
var foo3 = new Residence();

// Staggered:
Person foo1 = new Person();
Job foo2 = new Job();
Residence foo3 = new Residence();   

利点4:プロトタイピングのためのより可変的なプログラムロジック

最初の3つの利点は大きなものでした。これははるかに状況的です。

それでも、他の人がExcelを使用しているようにコードを使用する場合、コードを常に変更しています。多分私達はに固有のメソッドを呼び出す必要はありませんChild、このコードのバージョンが、我々は、後でコードを再利用したり手直しすることがあります。

強力な型システムの利点は、プログラムロジックに関する特定のメタデータを提供し、可能性をより簡単に明らかにすることです。これは、プロトタイピングに非常に役立つため、可能な限り保管することをお勧めします。

概要

Parentオーバーロードされたメソッド解決を台無しにして、いくつかのコンパイラの最適化を禁止し、リーダーから情報を削除し、コードをくします。

var実際に使用する方法です。素早く、きれいで、パターン化されており、コンパイラとIDEが適切に仕事をするのに役立ちます。


重要:この回答は、ParentvsChild。メソッドのローカルスコープに関するものです。Parentvsの問題はChild、戻り値の型、引数、およびクラスフィールドによって大きく異なります。


2
それはParent list = new Child()あまり意味がないと付け加えます。オブジェクトの具体的なインスタンスを直接作成するコード(ファクトリー、ファクトリーメソッドなどを介した間接化とは対照的に)は、作成されるタイプに関する完全な情報を持っています。ローカルスコープは、柔軟性を実現する場所ではありません。より抽象的なインターフェイスを使用して、新しく作成されたオブジェクトをクラスのクライアントに公開すると、柔軟性が得られます(ファクトリーとファクトリーメソッドが完璧な例です)
-crizzis

「tl; dr親よりも子を使用することはローカルスコープで望ましい。読みやすくするだけでなく」-必ずしも...子の詳細を使用しない場合は、必要以上の詳細を追加するだけです。「しかし、オーバーロードされたメソッド解決が適切に機能し、効率的なコンパイルを可能にすることを保証することも必要です。」-それはプログラミング言語に大きく依存します。これに起因する問題はまったくありませんがたくさんあります。
-AnoE

1
異なる動作をするソースがParent p = new Child(); p.doSomething();ありChild c = new Child; c.doSomething();ますか?たぶんそれはある言語では奇妙なことかもしれませんが、それはオブジェクトが参照ではなく振る舞いを制御することになっているはずです。それが相続の美しさです!子実装を信頼して、親実装の契約を履行できる限り、あなたは準備ができています。
corsiKa

@corsiKaええ、いくつかの方法で可能です。2つの簡単な例は、たとえばnewC#キーワードを使用してメソッドシグネチャを上書きする場合と、C ++の多重継承などを使用して複数の定義がある場合です。
ナット

@corsiKa簡単な3番目の例(C ++とC#の間のきちんとした対比だと思うので)、C#にはC ++のような問題があり、明示的なインターフェイスメソッドからこの動作を取得することもできます。C#のような言語では、これらの問題を回避するために部分的に多重継承の代わりにインターフェイスを使用しているため、面白いのですが、インターフェイスを採用することで、問題の一部を解決できませんでした-しかし、それほど悪いことではありません。いつParentですかinterface
ナット

0

与えられた場合parentType foo = new childType1();、コードは上の親型メソッドの使用に制限されますが、のインスタンスだけでなく、型が派生するオブジェクトfoofooの参照に格納できparentますchildType1。新しいchildType1インスタンスがfoo参照を保持する唯一のものである場合、fooの型をとして宣言する方が適切な場合がありますchildType1。一方、foo他の型への参照を保持する必要がある場合は、宣言parentTypeすることで可能になります。


0

を使用することをお勧めします

Child x = new Child();

の代わりに

Parent x = new Child();

前者は情報を失いますが、後者は情報を失います。

前者でできることはすべてできますが、後者ではできますが、その逆はできません。


ポインターをさらに詳しく説明するために、複数レベルの継承を考えてみましょう。

Base-> DerivedL1->DerivedL2

そして、結果をnew DerivedL2()変数に初期化します。基本タイプを使用すると、Baseまたはを使用するオプションが残りますDerivedL1

使用できます

Base x = new DerivedL2();

または

DerivedL1 x = new DerivedL2();

どちらかを好む論理的な方法は見当たりません。

使用する場合

DerivedL2 x = new DerivedL2();

議論することは何もありません。


3
あなたがもっとする必要がないなら、より少なくすることができるのは良いことですよね?
ピーター

1
@Peter、より少ないことをする能力に制約はありません。常に可能です。より多くのことを行う能力が制限されている場合、それは痛い点かもしれません。
R Sahu

しかし、情報を失うことは時々良いことです。あなたのヒエラルキーに関しては、ほとんどの人がおそらく投票するでしょうBase(それが機能すると仮定して)。最も具体的で、大部分が最も具体的でないことを好みます。ローカル変数の場合、それはほとんど問題ではないと思います。
-maaartinus

@maaartinus、ユーザーの大半が情報を失うことを好むことに驚いています。他の人ほど明確に情報を失うことの利点を見ていません。
R Sahu

宣言するとBase x = new DerivedL2();、最も制約を受け、これにより、最小限の労力で別の(グランド)子を切り替えることができます。さらに、それが可能であることがすぐにわかります。+++私たちはそれvoid f(Base)がより良いことに同意しvoid f(DerivedL2)ますか?
-maaartinus

0

変数を常に最も具体的な型として返すか格納することをお勧めしますが、最も広い型としてパラメーターを受け入れます。

例えば

<K, V> LinkedHashMap<K, V> keyValuePairsToMap(List<K> keys, List<V> values) {
   //...
}

パラメータはList<T>、非常に一般的なタイプです。ArrayList<T>LinkedList<T>その他はすべてこの方法で受け入れられます。

重要なのは、戻り値の型がLinkedHashMap<K, V>でなくであることMap<K, V>です。誰かがこのメソッドの結果をに割り当てたい場合Map<K, V>、それを行うことができます。この結果は単なるマップではなく、順序が定義されたマップであることを明確に伝えています。

このメソッドが返されたとしMap<K, V>ます。呼び出し元LinkedHashMap<K, V>がを必要とする場合、型チェック、キャスト、エラー処理を行う必要があり、これは非常に面倒です。


パラメータとして幅広い型を受け入れるように強制する同じロジックは、戻り型にも同様に適用する必要があります。関数の目的がキーを値にマッピングすることである場合、a Mapではなくa LinkedHashMapを返す必要があります- LinkedHashMapなどの関数に対してのみを返しkeyValuePairsToFifoMapます。特別な理由がない限り、より広い方が良いです。
corsiKa

@corsiKaうーん、それはもっといい名前だと思うが、これはほんの一例だ。(一般的に)戻り値の型を広げる方が良いことに同意しません。正当化せずに、人為的に型情報を削除します。ユーザーが提供したブロードタイプを合理的にキャストバックする必要があることを想定している場合は、ユーザーに損害を与えています。
アレクサンダー-モニカーの復活

「正当化なし」-しかし正当化があります!より広範な型を返すことで問題がなければ、リファクタリングが将来容易になります。誰かが特定の型を必要とする場合、その型の動作が原因で、適切な型を返すのは私だけです。しかし、私が必要ない場合、特定のタイプにロックされたくありません。私はそれを消費するものを傷つけることなく、私の方法の詳細を変更して自由になりたい、と私はからそれを変更した場合、と言うだろうLinkedHashMapTreeMap、何らかの理由で、今私は変更に多くのものを持っています。
corsiKa

実際、最後の文までずっとそれに同意します。あなたはから変更LinkedHashMapするTreeMapなどの変更などの些細な変更ではないArrayListLinkedList。これは、意味的に重要な変更です。その場合、コンパイラーにエラーをスローさて、戻り値のコンシューマーに変更を通知させ、適切なアクションを実行させます。
アレクサンダー-モニカーの復活

しかし、もしあなたが気にするのがマップの振る舞いだけなら、それ些細な変更かもしれません(もし可能なら、あなたはそうするべきです)。ほとんどのマップ)、それがツリーまたはハッシュマップであるかどうかは関係ありません。たとえば、Thread#getAllStackTraces()-は、具体的Map<Thread,StackTraceElement[]>にではなくを返すHashMapため、将来的にはMap、必要なタイプに変更できます。
corsiKa
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.