¿Es “Parent x = new Child ();” en lugar de “Child x = new Child ();” una mala práctica si podemos usar el último?


32

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?


66
Si hacemos lo último, ya no tiene sentido la abstracción / generalización. Por supuesto que hay excepciones ...
Walfrat

2
En un proyecto en el que estoy trabajando, mis funciones esperan el tipo primario y ejecuta métodos de tipo primario. Puedo aceptar cualquiera de varios hijos, y el código detrás de esos métodos (heredados y anulados) es diferente, pero quiero ejecutar ese código sin importar qué hijo esté usando.
Stephen S

44
@Walfrat De hecho, hay un pequeño punto en la abstracción cuando ya está creando instancias del tipo concreto, pero aún puede devolver el tipo abstracto para que el código del cliente sea felizmente ignorante.
Jacob Raihle

44
No use padre / hijo. Usar interfaz / clase (para Java).
Thorbjørn Ravn Andersen

44
Google "programa a una interfaz" para obtener un montón de explicaciones sobre por qué usar Parent x = new Child()es una buena idea.
Kevin Workman

Respuestas:


69

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 LinkedListy del ArrayListcual ambos descienden List. 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.


24
Exactamente. Para agregar un ejemplo, piense 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.
Maybe_Factor 01 de

19
buena respuesta, pero debería expandir que el tipo más abstracto posible significa que el código en el alcance no debe ser devuelto al niño para realizar alguna operación específica del niño.
Mike

10
@ Mike: Espero que sea evidente, de lo contrario, todas las variables, parámetros y campos se declararían como Object.
JacquesB

3
@JacquesB: como esta pregunta y respuesta recibieron tanta atención, pensé que ser explícito sería útil para personas con diferentes niveles de habilidad.
Mike

1
Depende del contexto no es una respuesta.
Billal Begueradj

11

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 newpalabra 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.


18
"aunque un poco abstracto". Badumtish
rosuav 01 de

1
¿No podría haber sido una edición?
beppe9000

6

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: ToStringes una llamada virtual, pero Stringestá sellada. On String, ToStringes un no-op, por lo que si declara objectque es una llamada virtual, si declara un, Stringel 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 a ArrayListfrente LinkedList.

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.


44
En la mayoría de los casos, las diferencias de rendimiento son minúsculas. Y en la mayoría de los casos (he oído decir que esto es aproximadamente el 90% del tiempo) debemos olvidarnos de esas pequeñas diferencias. Solo cuando sepamos (preferiblemente a través de la medición) que envejecemos como una sección de código de rendimiento crítico, deberíamos preocuparnos por tales optimizaciones.
Jules

Y el compilador sabe que "new Child ()" devuelve un Child, incluso cuando está asignado a un Parent, y mientras lo sepa, puede usar ese conocimiento y llamar al código Child ().
gnasher729

+1. También robé tu ejemplo en mi respuesta. = P
Nat

1
Tenga en cuenta que hay optimizaciones de rendimiento que hacen que todo sea discutible. HotSpot, por ejemplo, notará que un parámetro pasado a una función siempre es de una clase específica y se optimizará en consecuencia. Si esa suposición resulta ser falsa, el método se desoptimiza. Pero esto depende en gran medida del entorno del compilador / tiempo de ejecución. La última vez que verifiqué que el CLR ni siquiera podía hacer la optimización bastante trivial de notar que p de Parent p = new Child()siempre es un niño ..
Voo

4

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 Birdy Dog. Ahora, estos animales tienen unos comportamientos comunes - move(), eat()y speak(). 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 Listno. En particular, trimToSize(). Ahora, en el 99% de los casos, no nos importa esto. El Listes lo suficientemente grande para que casi nunca nos preocupamos. Pero hay momentos en que lo es. Ocasionalmente, debemos solicitar específicamente ArrayListque se ejecute trimToSize()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.


Confundido por qué esto podría tener un voto negativo. Parece canónico, personalmente. Excepto por la idea de un perro guardián programable ...
corsiKa

¿Por qué tuve que leer páginas para obtener la mejor respuesta? La calificación apesta.
Basilevs

Gracias por las palabras amables. Hay ventajas y desventajas en los sistemas de calificación, y una de ellas es que las respuestas posteriores a veces pueden quedar en el polvo. ¡Sucede!
corsiKa

2

En general, debes usar

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

o

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

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).


1
En C ++, las variables se crean principalmente sin la nueva palabra clave: stackoverflow.com/questions/6500313/…
Marian Spanik

1
La pregunta no es sobre C # / C ++ sino sobre un problema general orientado a objetos en un lenguaje que tiene al menos alguna forma de tipeo estático (es decir, sería absurdo preguntar en algo como Ruby). Además, incluso en esos dos idiomas, realmente no veo una buena razón por la que deberíamos hacerlo como usted dice.
AnoE

1

Bueno porque es fácil de entender y fácil de modificar este código:

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

Bueno porque comunica intención:

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

Ok, porque es común y fácil de entender:

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

La única opción que es realmente mala es usarla Parentsi solo Childtiene un método DoSomething(). Es malo porque comunica intenciones erróneamente:

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

Ahora elaboremos un poco más sobre el caso, la pregunta específicamente se refiere a:

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

Formateemos esto de manera diferente, haciendo que la segunda mitad sea una llamada de función:

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

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

Esto se puede acortar a:

FunctionCall(new Child());

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

Aquí, supongo que es ampliamente aceptado que WhichTypedeberí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ía Parent, o algo se Parentderiva de.

Esta línea de razonamiento explica por qué usar el tipo Parentes una buena opción, pero no entra en el clima, las otras opciones son malas (no lo son).


1

tl; dr : es preferible utilizarChildoverParenten 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,

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

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:

  1. Proporciona más información al compilador.
  2. Proporciona más información al lector.
  3. Código más limpio y estandarizado.
  4. Hace que la lógica del programa sea más mutable.

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

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

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

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

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

En el ejemplo anterior, ambas .Foo()llamadas finalmente llaman al mismo overridemétodo que devuelve 2. 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étodo sealed.

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();, y child.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 Childde Parent.

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 de 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

Ver una vardeclaració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.

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

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 Childen 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 Parentdesordenado 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 vares 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 sobreParentvs.Childen el ámbito local de un método. El problema deParentvs.Childes muy diferente para los tipos de retorno, argumentos y campos de clase.


2
Añadiría que eso 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)
crizzis

"tl; dr Usar niño sobre padre es preferible en el ámbito local. No solo ayuda con la legibilidad", no necesariamente ... si no hace uso de los detalles del niño, simplemente agregará más detalles de los necesarios. "pero también es necesario garantizar que la resolución del método sobrecargado funcione correctamente y ayude a permitir una compilación eficiente". - Eso depende mucho del lenguaje de programación. Hay muchos que no tienen ningún problema derivado de esto.
AnoE

1
¿Tiene una fuente que Parent p = new Child(); p.doSomething();y Child 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.
corsiKa

@corsiKa Sí, es posible de varias maneras. Dos ejemplos rápidos serían donde se sobrescriben las firmas de métodos, por ejemplo, con la newpalabra clave en C # , y cuando hay varias definiciones, por ejemplo, con herencia múltiple en C ++ .
Nat

@corsiKa Solo un tercer ejemplo rápido (porque creo que es un buen contraste entre C ++ y C #), C # tiene un problema como el de C ++, donde también puede obtener este comportamiento de los métodos de interfaz explícita . Es divertido porque los lenguajes como C # usan interfaces en lugar de herencia múltiple en parte para evitar estos problemas, pero al adoptar interfaces, todavía tienen parte del problema, pero no es tan malo porque, en ese escenario, solo se aplica a cuando Parentes un interface.
Nat

0

Dado parentType foo = new childType1();, el código se limitará al uso de métodos de tipo primario foo, pero podrá almacenarse en foouna referencia a cualquier objeto cuyo tipo se derive de, no parentsolo instancias de childType1. Si la nueva childType1instancia es lo único a lo fooque alguna vez tendrá una referencia, entonces puede ser mejor declarar fooel tipo como childType1. Por otro lado, si es fooposible que necesite contener referencias a otros tipos, tenerlo declarado como parentTypelo hará posible.


0

Sugiero usar

Child x = new Child();

en lugar de

Parent x = new Child();

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 usar Baseo DerivedL1.

Puedes usar

Base x = new DerivedL2();

o

DerivedL1 x = new DerivedL2();

No veo ninguna forma lógica de preferir uno sobre el otro.

Si utiliza

DerivedL2 x = new DerivedL2();

No hay nada que discutir.


3
Poder hacer menos si no necesitas hacer más es algo bueno, ¿no?
Peter

1
@ Peter, la capacidad de hacer menos no está limitada. Siempre es posible. Si la capacidad de hacer más está limitada, podría ser un punto doloroso.
R Sahu

Pero perder información a veces es algo bueno. Con respecto a su jerarquía, la mayoría de las personas probablemente votarían 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.
maaartinus

@maaartinus, me sorprende que la mayoría de los usuarios prefieran perder información. No veo los beneficios de perder información tan claramente como los demás.
R Sahu

Cuando declaras 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 que void f(Base)es mejor que void f(DerivedL2)?
maaartinus

0

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

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

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> , no Map<K, V>. Si alguien quiere asignar el resultado de este método a a Map<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 un LinkedHashMap<K, V>, tendrían que hacer una verificación de tipo, conversión y manejo de errores, lo cual es realmente engorroso.


La misma lógica que lo obliga a aceptar un tipo amplio como parámetro también debería aplicarse al tipo de retorno también. Si el objetivo de la función es asignar claves a valores, debería devolver un Mapno a LinkedHashMap, solo querría devolver un LinkedHashMappara una función como keyValuePairsToFifoMapo algo así. Más amplio es mejor hasta que tenga una razón específica para no hacerlo.
corsiKa

@corsiKa Hmm. Estoy de acuerdo en que es un nombre mejor, pero este es solo un ejemplo. No estoy de acuerdo con que (en general) ampliar su tipo de devolución sea mejor. Su información de tipo de eliminación artificial, sin ninguna justificación. Si imagina que su usuario necesitará razonablemente rechazar el tipo amplio que le ha proporcionado, entonces le ha hecho un mal servicio.
Alexander - Restablece a Mónica el

"sin ninguna justificación", ¡pero hay justificación! Si puedo salir con la devolución de un tipo más amplio, facilita la refactorización más adelante. Si alguien NECESITA el tipo específico debido a algún comportamiento, estoy dispuesto a devolver el tipo apropiado. Pero no quiero estar encerrado en un tipo específico si no tengo que hacerlo conmigo. Me gustaría tener la libertad de alterar las características específicas de mi método sin dañar a los que consumen, y si lo cambio de, por ejemplo LinkedHashMapa TreeMappor cualquier razón, ahora tengo un montón de cosas a cambio.
corsiKa

En realidad, estoy de acuerdo con eso hasta la última oración. ¿Ha cambiado desde LinkedHashMapque TreeMapno es un cambio trivial, como el cambio de ArrayLista LinkedList. 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.
Alexander - Restablece a Monica el

Pero puede ser un cambio trivial si lo único que le importa es el comportamiento del mapa (que, si puede, debería hacerlo). Si el contrato es "obtendrá un mapa de valores clave" y el orden no importa (lo cual es cierto para la mayoría de los mapas), entonces no importa si es un árbol o un mapa hash. Por ejemplo, Thread#getAllStackTraces()- devuelve un en Map<Thread,StackTraceElement[]>lugar de un HashMapespecíficamente para que en el futuro puedan cambiarlo al tipo Mapque deseen.
corsiKa
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.