Por ejemplo, había visto algunos códigos que crean un fragmento como este:
Fragment myFragment=new MyFragment();
que declara una variable como Fragment en lugar de MyFragment, que MyFragment es una clase secundaria de Fragment. No estoy satisfecho con esta línea de códigos porque creo que este código debería ser:
MyFragment myFragment=new MyFragment();
que es más específico, ¿es eso cierto?
O en generalización de la pregunta, ¿es una mala práctica usar:
Parent x=new Child();
en lugar de
Child x=new Child();
si podemos cambiar el primero en el último sin error de compilación?
Parent x = new Child()
es una buena idea.Respuestas:
Depende del contexto, pero diría que debe declarar el tipo más abstracto posible. De esa manera, su código será lo más general posible y no dependerá de detalles irrelevantes.
Un ejemplo sería tener un
LinkedList
y delArrayList
cual ambos desciendenList
. Si el código funcionaría igualmente bien con cualquier tipo de lista, entonces no hay razón para restringirlo arbitrariamente a una de las subclases.fuente
List list = new ArrayList()
, al código que usa la lista no le importa qué tipo de lista es, solo que cumple con el contrato de la interfaz de la Lista. En el futuro, podríamos cambiar fácilmente a una implementación de Lista diferente y el código subyacente no necesitaría cambiar o actualizarse en absoluto.Object
.La respuesta de JacquesB es correcta, aunque un poco abstracta. Permítanme enfatizar algunos aspectos más claramente:
En su fragmento de código, utiliza explícitamente la
new
palabra clave, y tal vez le gustaría continuar con otros pasos de inicialización que probablemente sean específicos para el tipo concreto: entonces debe declarar la variable con ese tipo concreto concreto.La situación es diferente cuando obtienes ese artículo de otro lugar, por ejemplo
IFragment fragment = SomeFactory.GiveMeFragments(...);
. Aquí, la fábrica normalmente debería devolver el tipo más abstracto posible, y el código descendente no debería depender de los detalles de implementación.fuente
Hay potencialmente diferencias de rendimiento.
Si la clase derivada está sellada, el compilador puede en línea y optimizar o eliminar lo que de otro modo serían llamadas virtuales. Por lo tanto, puede haber una diferencia de rendimiento significativa.
Ejemplo:
ToString
es una llamada virtual, peroString
está sellada. OnString
,ToString
es un no-op, por lo que si declaraobject
que es una llamada virtual, si declara un,String
el compilador sabe que ninguna clase derivada ha anulado el método porque la clase está sellada, por lo que es un no-op. Consideraciones similares se aplican aArrayList
frenteLinkedList
.Por lo tanto, si conoce el tipo concreto del objeto (y no hay ninguna razón de encapsulación para ocultarlo), debe declararlo como ese tipo. Como acaba de crear el objeto, conoce el tipo concreto.
fuente
Parent p = new Child()
siempre es un niño ..La diferencia clave es el nivel de acceso requerido. Y toda buena respuesta sobre la abstracción involucra animales, o eso me han dicho.
Supongamos que tiene algunos animales,
Cat
Bird
yDog
. Ahora, estos animales tienen unos comportamientos comunes -move()
,eat()
yspeak()
. Todos comen de manera diferente y hablan de manera diferente, pero si necesito que mi animal coma o hable, realmente no me importa cómo lo hacen.Pero a veces lo hago. Nunca he tenido un gato guardián o un pájaro guardián, pero tengo un perro guardián. Entonces, cuando alguien irrumpe en mi casa, no puedo confiar en hablar para ahuyentar al intruso. Realmente necesito que mi perro ladre, esto es diferente a su habla, lo cual es solo un engaño.
Entonces, en el código que requiere un intruso, realmente necesito hacer
Dog fido = getDog(); fido.barkMenancingly();
Pero la mayoría de las veces, felizmente puedo hacer que cualquier animal haga trucos donde teje, miau o tuitee para recibir golosinas.
Animal pet = getAnimal(); pet.speak();
Para ser más concreto, ArrayList tiene algunos métodos que
List
no. En particular,trimToSize()
. Ahora, en el 99% de los casos, no nos importa esto. ElList
es lo suficientemente grande para que casi nunca nos preocupamos. Pero hay momentos en que lo es. Ocasionalmente, debemos solicitar específicamenteArrayList
que se ejecutetrimToSize()
y que su matriz de respaldo sea más pequeña.Tenga en cuenta que esto no tiene que hacerse en la construcción, podría hacerse a través de un método de devolución o incluso un yeso si estamos absolutamente seguros.
fuente
En general, debes usar
o
para variables locales a menos que haya una buena razón para que la variable tenga un tipo diferente al que se inicializa.
(Aquí estoy ignorando el hecho de que el código C ++ probablemente debería estar usando un puntero inteligente. Hay casos en los que no hay y sin más de una línea que realmente no puedo conocer).
La idea general aquí es con lenguajes que admiten la detección automática de tipos de variables, su uso hace que el código sea más fácil de leer y hace lo correcto (es decir, el sistema de tipos del compilador puede optimizar lo más posible y cambiar la inicialización para que sea un nuevo el tipo funciona igual si se hubiera utilizado la mayoría de los resúmenes).
fuente
Bueno porque es fácil de entender y fácil de modificar este código:
Bueno porque comunica intención:
Ok, porque es común y fácil de entender:
La única opción que es realmente mala es usarla
Parent
si soloChild
tiene un métodoDoSomething()
. Es malo porque comunica intenciones erróneamente:Ahora elaboremos un poco más sobre el caso, la pregunta específicamente se refiere a:
Formateemos esto de manera diferente, haciendo que la segunda mitad sea una llamada de función:
Esto se puede acortar a:
Aquí, supongo que es ampliamente aceptado que
WhichType
debería ser el tipo más básico / abstracto que permite que la función funcione (a menos que haya problemas de rendimiento). En este caso, el tipo apropiado seríaParent
, o algo seParent
deriva de.Esta línea de razonamiento explica por qué usar el tipo
Parent
es una buena opción, pero no entra en el clima, las otras opciones son malas (no lo son).fuente
tl; dr : es preferible utilizar
Child
overParent
en el ámbito local. No solo ayuda con la legibilidad, sino que también es necesario garantizar que la resolución del método sobrecargado funcione correctamente y ayuda a permitir una compilación eficiente.En el ámbito local,
Conceptualmente, se trata de mantener la mayor cantidad de información posible. Si bajamos a
Parent
, esencialmente estamos eliminando información de tipo que podría haber sido útil.Retener la información de tipo completa tiene cuatro ventajas principales:
Ventaja 1: más información para el compilador
El tipo aparente se usa en la resolución de métodos sobrecargados y en la optimización.
Ejemplo: resolución de método sobrecargado
En el ejemplo anterior, ambas
foo()
llamadas funcionan , pero en un caso, obtenemos la mejor resolución del método sobrecargado.Ejemplo: optimización del compilador
En el ejemplo anterior, ambas
.Foo()
llamadas finalmente llaman al mismooverride
método que devuelve2
. Simplemente, en el primer caso, hay una búsqueda de método virtual para encontrar el método correcto; esta búsqueda de método virtual no es necesaria en el segundo caso, ya que la de ese métodosealed
.Gracias a @Ben, quien proporcionó un ejemplo similar en su respuesta .
Ventaja 2: más información para el lector
Conocer el tipo exacto, es decir
Child
, proporciona más información a quien lea el código, lo que facilita ver lo que está haciendo el programa.Claro, tal vez no importa que el código real, ya que ambos
parent.Foo();
, ychild.Foo();
tiene sentido, pero para alguien ver el código por primera vez, más información es simplemente útil.Además, dependiendo de su entorno de desarrollo, el IDE puede ser capaz de proporcionar información sobre herramientas más útiles y metadatos acerca
Child
deParent
.Ventaja 3: código más limpio y estandarizado
La mayoría de los ejemplos de código C # que he visto últimamente usan
var
, que es básicamente una forma abreviada deChild
.Ver una
var
declaración de no declaración simplemente se ve mal; si hay una razón situacional para ello, increíble, pero de lo contrario parece antipatrón.Ventaja 4: lógica de programa más mutable para la creación de prototipos
Las tres primeras ventajas fueron las grandes; este es mucho más situacional.
Aún así, para las personas que usan código como otros usan Excel, estamos cambiando constantemente nuestro código. Tal vez no es necesario llamar a un método único para
Child
en esta versión del código, pero que podríamos reutilizar o reelaborar el código más tarde.La ventaja de un sistema de tipo fuerte es que nos proporciona ciertos metadatos sobre la lógica de nuestro programa, lo que hace que las posibilidades sean más evidentes. Esto es increíblemente útil en la creación de prototipos, por lo que es mejor mantenerlo donde sea posible.
Resumen
El uso
Parent
desordenado de la resolución del método sobrecargado, inhibe algunas optimizaciones del compilador, elimina la información del lector y hace que el código sea más feo.Usar
var
es realmente el camino a seguir. Es rápido, limpio, está diseñado y ayuda al compilador y al IDE a hacer su trabajo correctamente.Importante : Esta respuesta es sobre
Parent
vs.Child
en el ámbito local de un método. El problema deParent
vs.Child
es muy diferente para los tipos de retorno, argumentos y campos de clase.fuente
Parent list = new Child()
no tiene mucho sentido. El código que crea directamente la instancia concreta de un objeto (a diferencia de la indirecta a través de fábricas, métodos de fábrica, etc.) tiene información perfecta sobre el tipo que se está creando, puede que necesite interactuar con la interfaz concreta para configurar el objeto recién creado, etc. El alcance local no es donde se logra flexibilidad. La flexibilidad se logra cuando expones ese objeto recién creado a un cliente de tu clase usando una interfaz más abstracta (las fábricas y los métodos de fábrica son un ejemplo perfecto)Parent p = new Child(); p.doSomething();
yChild c = new Child; c.doSomething();
tiene un comportamiento diferente? Tal vez sea una peculiaridad en algún lenguaje, pero se supone que el objeto controla el comportamiento, no la referencia. Esa es la belleza de la herencia! Mientras pueda confiar en las implementaciones secundarias para cumplir con el contrato de la implementación principal, estará listo.new
palabra clave en C # , y cuando hay varias definiciones, por ejemplo, con herencia múltiple en C ++ .Parent
es uninterface
.Dado
parentType foo = new childType1();
, el código se limitará al uso de métodos de tipo primariofoo
, pero podrá almacenarse enfoo
una referencia a cualquier objeto cuyo tipo se derive de, noparent
solo instancias dechildType1
. Si la nuevachildType1
instancia es lo único a lofoo
que alguna vez tendrá una referencia, entonces puede ser mejor declararfoo
el tipo comochildType1
. Por otro lado, si esfoo
posible que necesite contener referencias a otros tipos, tenerlo declarado comoparentType
lo hará posible.fuente
Sugiero usar
en lugar de
Este último pierde información mientras que el primero no.
Puedes hacer todo con el primero que puedes hacer con el segundo pero no al revés.
Para elaborar más el puntero, tengamos múltiples niveles de herencia.
Base
->DerivedL1
->DerivedL2
Y desea inicializar el resultado de
new DerivedL2()
una variable. Usar un tipo base te deja la opción de usarBase
oDerivedL1
.Puedes usar
o
No veo ninguna forma lógica de preferir uno sobre el otro.
Si utiliza
No hay nada que discutir.
fuente
Base
(suponiendo que funcione). Prefieres el más específico, AFAIK la mayoría prefiere el menos específico. Yo diría que para las variables locales apenas importa.Base x = new DerivedL2();
, estás más limitado y esto te permite cambiar a otro (gran) hijo con el mínimo esfuerzo. Además, ves de inmediato que es posible. +++ ¿Estamos de acuerdo en quevoid f(Base)
es mejor quevoid f(DerivedL2)
?Sugeriría siempre devolver o almacenar variables como el tipo más específico, pero aceptando parámetros como los tipos más amplios.
P.ej
Los parámetros son
List<T>
, un tipo muy general.ArrayList<T>
,LinkedList<T>
y otros podrían ser aceptados por este método.Es importante destacar que el tipo de retorno es
LinkedHashMap<K, V>
, noMap<K, V>
. Si alguien quiere asignar el resultado de este método a aMap<K, V>
, puede hacerlo. Comunica claramente que este resultado es más que un simple mapa, sino un mapa con un orden definido.Supongamos que este método devuelto
Map<K, V>
. Si la persona que llama quería unLinkedHashMap<K, V>
, tendrían que hacer una verificación de tipo, conversión y manejo de errores, lo cual es realmente engorroso.fuente
Map
no aLinkedHashMap
, solo querría devolver unLinkedHashMap
para una función comokeyValuePairsToFifoMap
o algo así. Más amplio es mejor hasta que tenga una razón específica para no hacerlo.LinkedHashMap
aTreeMap
por cualquier razón, ahora tengo un montón de cosas a cambio.LinkedHashMap
queTreeMap
no es un cambio trivial, como el cambio deArrayList
aLinkedList
. Es un cambio con importancia semántica. En ese caso, desea que el compilador arroje un error, para obligar a los consumidores del valor de retorno a notar el cambio y tomar las medidas apropiadas.Thread#getAllStackTraces()
- devuelve un enMap<Thread,StackTraceElement[]>
lugar de unHashMap
específicamente para que en el futuro puedan cambiarlo al tipoMap
que deseen.