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?
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.
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.
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.
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 - Cat
Bird
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.
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).
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).
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:
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 .
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
.
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();
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.
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.
É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.
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.
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.