web-dev-qa-db-fra.com

Est "Parent x = new Child ();" au lieu de "Child x = new Child ();" une mauvaise pratique si nous pouvons utiliser ce dernier?

Par exemple, j'avais vu des codes qui créent un fragment comme celui-ci:

Fragment myFragment=new MyFragment();

qui déclare une variable comme Fragment au lieu de MyFragment, qui MyFragment est une classe enfant de Fragment. Je ne suis pas satisfait de cette ligne de codes car je pense que ce code devrait être:

MyFragment myFragment=new MyFragment();

qui est plus spécifique, est-ce vrai?

Ou dans la généralisation de la question, est-ce une mauvaise pratique d'utiliser:

Parent x=new Child();

au lieu de

Child x=new Child();

si nous pouvons changer l'ancien en dernier sans erreur de compilation?

32
ggrr

Cela dépend du contexte, mais je dirais que vous devriez déclarer le type le plus abstrait possible. De cette façon, votre code sera aussi général que possible et ne dépendra pas de détails non pertinents.

Un exemple serait d'avoir un LinkedList et ArrayList qui descendent tous les deux de List. Si le code fonctionnait aussi bien avec n'importe quel type de liste, il n'y a aucune raison de le restreindre arbitrairement à l'une des sous-classes.

69
JacquesB

La réponse de JacquesB est correcte, quoique un peu abstraite. Permettez-moi de souligner certains aspects plus clairement:

Dans votre extrait de code, vous utilisez explicitement le mot clé new, et peut-être souhaitez-vous poursuivre d'autres étapes d'initialisation qui sont probablement spécifiques au type concret: vous devez ensuite déclarer la variable avec ce type concret spécifique.

La situation est différente lorsque vous obtenez cet article ailleurs, par exemple IFragment fragment = SomeFactory.GiveMeFragments(...);. Ici, la fabrique doit normalement retourner le type le plus abstrait possible, et le code descendant ne doit pas dépendre des détails d'implémentation.

11
Bernhard Hiller

Il existe potentiellement des différences de performances.

Si la classe dérivée est scellée, le compilateur peut aligner et optimiser ou éliminer ce qui serait autrement des appels virtuels. Il peut donc y avoir une différence de performances significative.

Exemple: ToString est un appel virtuel, mais String est scellé. Sur String, ToString est un no-op, donc si vous déclarez comme object c'est un appel virtuel, si vous déclarez un String le compilateur ne sait pas la classe dérivée a remplacé la méthode car la classe est scellée, c'est donc un no-op. Des considérations similaires s'appliquent à ArrayList vs LinkedList.

Par conséquent, si vous connaissez le type concret de l'objet (et qu'il n'y a aucune raison d'encapsulation pour le masquer), vous devez déclarer ce type. Puisque vous venez de créer l'objet, vous connaissez le type de béton.

6
Ben

La principale différence est le niveau d'accès requis. Et toute bonne réponse à propos de l'abstraction implique des animaux, du moins c'est ce qu'on me dit.

Supposons que vous ayez quelques animaux - CatBird et Dog. Maintenant, ces animaux ont quelques comportements communs - move(), eat() et speak(). Ils mangent tous différemment et parlent différemment, mais si j'ai besoin que mon animal mange ou parle, peu m'importe comment ils le font.

Mais parfois je le fais. Je n'ai jamais endurci un chat ou un oiseau de garde, mais j'ai un chien de garde. Donc, quand quelqu'un s'introduit dans ma maison, je ne peux pas me contenter de parler pour effrayer l'intrus. J'ai vraiment besoin que mon chien aboie - c'est différent de son discours, qui est juste une trame.

Donc, dans le code qui nécessite un intrus, je dois vraiment faire Dog fido = getDog(); fido.barkMenancingly();

Mais la plupart du temps, je peux heureusement demander à n'importe quel animal de faire des tours où il woof, miaule ou Tweet pour des friandises. Animal pet = getAnimal(); pet.speak();

Donc, pour être plus concret, ArrayList a certaines méthodes que List n'a pas. Notamment, trimToSize(). Maintenant, dans 99% des cas, cela nous importe peu. Le List n'est presque jamais assez grand pour que nous nous en soucions. Mais il y a des moments où c'est le cas. Donc, de temps en temps, nous devons spécifiquement demander un ArrayList pour exécuter trimToSize() et rendre son tableau de support plus petit.

Notez que cela ne doit pas être fait lors de la construction - cela pourrait être fait via une méthode de retour ou même un cast si nous sommes absolument sûrs.

4
corsiKa

D'une manière générale, vous devez utiliser

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

ou

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

pour les variables locales, sauf s'il existe une bonne raison pour que la variable ait un type différent de celui avec lequel elle est initialisée.

(Ici, j'ignore le fait que le code C++ devrait très probablement utiliser un pointeur intelligent. Il y a des cas où il n'y a pas et sans plus d'une ligne que je ne peux pas vraiment savoir.)

L'idée générale ici est avec les langages qui prennent en charge la détection automatique de type de variables, son utilisation rend le code plus facile à lire et fait la bonne chose (c'est-à-dire que le système de type du compilateur peut optimiser autant que possible et changer l'initialisation pour être un nouveau fonctionne également si la plupart des résumés ont été utilisés).

2
Joshua

Bien car il est facile à comprendre et à modifier ce code:

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

Bon car il communique l'intention:

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

Ok, car c'est courant et facile à comprendre:

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

La seule option vraiment mauvaise est d'utiliser Parent si seulement Child a une méthode DoSomething(). C'est mauvais parce qu'il communique mal l'intention:

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

Maintenant, développons un peu plus le cas que la question pose spécifiquement sur:

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

Formaterons cela différemment, en faisant la deuxième moitié un appel de fonction:

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

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

Cela peut être raccourci comme suit:

FunctionCall(new Child());

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

Ici, je suppose qu'il est largement admis que WhichType devrait être le type le plus basique/abstrait qui permet à la fonction de fonctionner (sauf en cas de problèmes de performances). Dans ce cas, le type approprié serait Parent, ou quelque chose Parent dérive de.

Ce raisonnement explique pourquoi l'utilisation du type Parent est un bon choix, mais il ne va pas dans le temps les autres choix sont mauvais (ils ne le sont pas).

1
Peter

tl; dr - Utiliser Child plutôt que Parent est préférable dans le local portée. Cela aide non seulement à la lisibilité, mais il est également nécessaire de s'assurer que la résolution de méthode surchargée fonctionne correctement et permet une compilation efficace.


Au niveau local,

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

Conceptuellement, il s'agit de conserver le plus d'informations de type possible. Si nous rétrogradons vers Parent, nous supprimons essentiellement les informations de type qui auraient pu être utiles.

La conservation des informations de type complètes présente quatre avantages principaux:

  1. Fournit plus d'informations au compilateur.
  2. Fournit plus d'informations au lecteur.
  3. Code plus propre et plus standardisé.
  4. Rend la logique du programme plus modifiable.

Avantage 1: Plus d'informations pour le compilateur

Le type apparent est utilisé dans la résolution de méthode surchargée et dans l'optimisation.

Exemple: résolution de méthode surchargée

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

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

Dans l'exemple ci-dessus, les deux foo() appels travail, mais dans un cas, nous obtenons la meilleure résolution de méthode surchargée.

Exemple: optimisation du compilateur

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; }
}

Dans l'exemple ci-dessus, les deux appels .Foo() appellent finalement la même méthode override qui renvoie 2. Juste, dans le premier cas, il y a une recherche de méthode virtuelle pour trouver la bonne méthode; cette recherche de méthode virtuelle n'est pas nécessaire dans le second cas puisque sealed de cette méthode.

Nous remercions @Ben qui a fourni un exemple similaire dans sa réponse .

Avantage 2: Plus d'informations pour le lecteur

Connaître le type exact, c'est-à-dire Child, fournit plus d'informations à celui qui lit le code, ce qui permet de voir plus facilement ce que fait le programme.

Bien sûr, cela n'a peut-être pas d'importance pour le code réel car parent.Foo(); et child.Foo(); ont du sens, mais pour quelqu'un qui voit le code pour la première fois, plus d'informations sont tout simplement utiles.

De plus, selon votre environnement de développement, le IDE peut être en mesure de fournir des info-bulles et des métadonnées plus utiles sur Child que Parent.

Avantage 3: code plus propre et plus standardisé

La plupart des exemples de code C # que j'ai vus récemment utilisent var, qui est essentiellement un raccourci pour 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

Voir une instruction de déclaration nonvar est tout simplement désactivé; s'il y a une raison situationnelle, génial, mais sinon cela a l'air anti-modèle.

// 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();   

Avantage 4: Logique de programme plus modifiable pour le prototypage

Les trois premiers avantages étaient les grands; celui-ci est beaucoup plus situationnel.

Pourtant, pour les personnes qui utilisent du code comme d'autres utilisent Excel, nous changeons constamment notre code. Peut-être que nous n'avons pas besoin d'appeler une méthode unique à Child dans la version ceci du code, mais nous pourrions réutiliser ou retravailler le code plus tard.

L'avantage d'un système de type fort est qu'il nous fournit certaines métadonnées sur la logique de notre programme, ce qui rend les possibilités plus évidentes. Ceci est incroyablement utile dans le prototypage, il est donc préférable de le conserver autant que possible.

Sommaire

L'utilisation de Parent perturbe la résolution des méthodes surchargées, inhibe certaines optimisations du compilateur, supprime les informations du lecteur et rend le code plus laid.

Utiliser var est vraiment la voie à suivre. C'est rapide, propre, structuré et aide le compilateur et IDE à faire son travail correctement.


Important: Cette réponse est d'environ Parent vs. Child dans une méthode portée locale. Le problème entre Parent et Child est très différent pour les types de retour, les arguments et les champs de classe.

1
Nat

Étant donné parentType foo = new childType1();, le code sera limité à l'utilisation de méthodes de type parent sur foo, mais pourra stocker dans foo une référence à tout objet dont le type dérive de parent-- pas seulement des instances de childType1. Si le nouveau childType1 l'instance est la seule chose à laquelle foo contiendra une référence, alors il peut être préférable de déclarer le type de foo comme childType1. D'un autre côté, si foo peut avoir besoin de contenir des références à d'autres types, le déclarer comme parentType rendra cela possible.

0
supercat

Je suggère d'utiliser

Child x = new Child();

au lieu de

Parent x = new Child();

Ce dernier perd des informations tandis que l'ancien ne le fait pas.

Vous pouvez tout faire avec les premiers comme vous pouvez le faire avec les seconds mais pas l'inverse.


Pour élaborer davantage le pointeur, nous vous proposons plusieurs niveaux d'héritage.

Base -> DerivedL1 -> DerivedL2

Et vous voulez initialiser le résultat de new DerivedL2() en une variable. L'utilisation d'un type de base vous laisse la possibilité d'utiliser Base ou DerivedL1.

Vous pouvez utiliser

Base x = new DerivedL2();

ou

DerivedL1 x = new DerivedL2();

Je ne vois aucun moyen logique de préférer l'un à l'autre.

Si tu utilises

DerivedL2 x = new DerivedL2();

il n'y a rien à discuter.

0
R Sahu

Je suggérerais toujours de retourner ou de stocker des variables comme type le plus spécifique, mais d'accepter les paramètres comme types les plus larges.

Par exemple.

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

Les paramètres sont List<T>, un type très général. ArrayList<T>, LinkedList<T> et d'autres pourraient tous être acceptés par cette méthode.

Surtout, le type de retour est LinkedHashMap<K, V>, ne pas Map<K, V>. Si quelqu'un veut affecter le résultat de cette méthode à un Map<K, V>, ils peuvent le faire. Il indique clairement que ce résultat est plus qu'une simple carte, mais une carte avec un ordre défini.

Supposons que cette méthode renvoie Map<K, V>. Si l'appelant voulait un LinkedHashMap<K, V>, ils devraient effectuer une vérification de type, un cast et une gestion des erreurs, ce qui est vraiment lourd.