En Java no existen virtual
, new
, override
palabras clave para la definición del método. Entonces, el funcionamiento de un método es fácil de entender. Porque si DerivedClass extiende BaseClass y tiene un método con el mismo nombre y la misma firma de BaseClass , la anulación tendrá lugar en el polimorfismo en tiempo de ejecución (siempre que el método no lo sea static
).
BaseClass bcdc = new DerivedClass();
bcdc.doSomething() // will invoke DerivedClass's doSomething method.
Ahora vienen a C # no puede haber tanta confusión y difícil de entender cómo el new
o virtual+derive
o nueva + Virtual de anulación está trabajando.
No puedo entender por qué en el mundo voy a agregar un método en mi DerivedClass
con el mismo nombre y la misma firma BaseClass
y definir un nuevo comportamiento, pero en el polimorfismo en tiempo de ejecución, ¡ BaseClass
se invocará el método! (que no está anulando pero lógicamente debería estarlo).
En caso de virtual + override
que la implementación lógica sea correcta, pero el programador debe pensar qué método debe dar permiso al usuario para anular en el momento de la codificación. Que tiene algo de pro-con (no vayamos allí ahora).
Entonces, ¿por qué en C # hay tanto espacio para el razonamiento no lógico y la confusión? Así que puedo replantear mi pregunta en qué contexto del mundo real debería pensar en su uso virtual + override
en lugar de new
y también el uso de new
en lugar de virtual + override
?
Después de algunas muy buenas respuestas, especialmente de Omar , entiendo que los diseñadores de C # le dieron más énfasis a los programadores que deberían pensar antes de hacer un método, lo cual es bueno y maneja algunos errores de novatos de Java.
Ahora tengo una pregunta en mente. Como en Java si tuviera un código como
Vehicle vehicle = new Car();
vehicle.accelerate();
y luego hago una nueva clase SpaceShip
derivada de Vehicle
. Entonces quiero cambiar todo car
a un SpaceShip
objeto, solo tengo que cambiar una sola línea de código
Vehicle vehicle = new SpaceShip();
vehicle.accelerate();
Esto no romperá nada de mi lógica en ningún punto del código.
Pero en el caso de C # si SpaceShip
no anula la Vehicle
clase accelerate
y el uso, new
entonces la lógica de mi código se romperá. ¿No es eso una desventaja?
fuente
final
; es todo lo contrario).@Override
.Respuestas:
Dado que usted preguntó por qué C # lo hizo de esta manera, es mejor preguntar a los creadores de C #. Anders Hejlsberg, el arquitecto principal de C #, respondió por qué eligieron no usar el virtual por defecto (como en Java) en una entrevista , los fragmentos pertinentes están a continuación.
Tenga en cuenta que Java tiene virtual de forma predeterminada con la palabra clave final para marcar un método como no virtual. Todavía hay dos conceptos que aprender, pero muchas personas no conocen la palabra clave final o no la usan de manera proactiva. C # obliga a uno a usar virtual y new / override para tomar conscientemente esas decisiones.
La entrevista tiene más discusión sobre cómo piensan los desarrolladores sobre el diseño de herencia de clase, y cómo eso llevó a su decisión.
Ahora a la siguiente pregunta:
Esto sería cuando una clase derivada quiere declarar que no cumple con el contrato de la clase base, sino que tiene un método con el mismo nombre. (Para cualquiera que no sepa la diferencia entre
new
yoverride
en C #, consulte esta página de MSDN ).Un escenario muy práctico es este:
Vehicle
.Vehicle
.Vehicle
clase no tenía ningún métodoPerformEngineCheck()
.Car
clase, agrego un métodoPerformEngineCheck()
.PerformEngineCheck()
.Entonces, cuando vuelvo a compilar contra su nueva API, C # me advierte sobre este problema, por ejemplo
Si la base
PerformEngineCheck()
no eravirtual
:Y si la base
PerformEngineCheck()
eravirtual
:Ahora, debo tomar una decisión explícita si mi clase realmente está extendiendo el contrato de la clase base, o si es un contrato diferente pero resulta ser el mismo nombre.
new
, no rompo a mis clientes si la funcionalidad del método base era diferente del método derivado. Cualquier código al que seVehicle
haga referencia no se veráCar.PerformEngineCheck()
llamado, pero el código al que hizo referenciaCar
continuará viendo la misma funcionalidad que yo había ofrecidoPerformEngineCheck()
.Un ejemplo similar es cuando otro método en la clase base podría estar llamando
PerformEngineCheck()
(especialmente en la versión más reciente), ¿cómo se evita que llame a laPerformEngineCheck()
clase derivada? En Java, esa decisión dependería de la clase base, pero no sabe nada sobre la clase derivada. En C #, esa decisión se basa tanto en la clase base (a través de lavirtual
palabra clave) como en la clase derivada (a través de las palabras clavenew
yoverride
).Por supuesto, los errores que arroja el compilador también proporcionan una herramienta útil para que los programadores no cometan errores inesperadamente (es decir, anular o proporcionar una nueva funcionalidad sin darse cuenta).
Como dijo Anders, el mundo real nos obliga a tales problemas que, si comenzáramos desde cero, nunca quisiéramos entrar.
EDITAR: se agregó un ejemplo de dónde
new
debería usarse para garantizar la compatibilidad de la interfaz.EDITAR: Mientras revisaba los comentarios, también encontré un artículo escrito por Eric Lippert (entonces uno de los miembros del comité de diseño de C #) sobre otros escenarios de ejemplo (mencionados por Brian).
PARTE 2: Basado en una pregunta actualizada
¿Quién decide si
SpaceShip
realmente está anulandoVehicle.accelerate()
o si es diferente? Tiene que ser elSpaceShip
desarrollador. Entonces, si elSpaceShip
desarrollador decide que no está cumpliendo con el contrato de la clase base, entonces su llamada aVehicle.accelerate()
no debería irSpaceShip.accelerate()
, ¿o debería? Es entonces cuando lo marcarán comonew
. Sin embargo, si deciden que efectivamente mantiene el contrato, lo marcaránoverride
. En cualquier caso, su código se comportará correctamente llamando al método correcto según el contrato . ¿Cómo puede su código decidir siSpaceShip.accelerate()
realmente está anulandoVehicle.accelerate()
o si es una colisión de nombres? (Ver mi ejemplo arriba).Sin embargo, en el caso de la herencia implícita, aunque
SpaceShip.accelerate()
no se mantenga el contrato deVehicle.accelerate()
, la llamada al método todavía ir aSpaceShip.accelerate()
.fuente
Se hizo porque es lo correcto. El hecho es que permitir que se anulen todos los métodos es incorrecto; conduce al problema de la clase base frágil, donde no tienes forma de saber si un cambio en la clase base romperá las subclases. Por lo tanto, debe incluir en la lista negra los métodos que no se deben anular o incluir en la lista blanca los que se pueden anular. De los dos, la lista blanca no solo es más segura (ya que no puede crear una clase base frágil accidentalmente ), sino que también requiere menos trabajo ya que debe evitar la herencia a favor de la composición.
fuente
final
modificador, entonces, ¿cuál es el problema?HashMap
¿ recuerda ?). Diseñe para la herencia y aclare el contrato o no, y asegúrese de que nadie pueda heredar la clase.@Override
se agregó como bandaid porque ya era demasiado tarde para cambiar el comportamiento, pero incluso los desarrolladores originales de Java (particularmente Josh Bloch) están de acuerdo en que esta fue una mala decisión.Como dijo Robert Harvey, todo está en lo que estás acostumbrado. Me parece extraña la falta de flexibilidad de Java .
Dicho esto, ¿por qué tener esto en primer lugar? Por la misma razón por la que C # tiene
public
,internal
(también "nada"),protected
,protected internal
, yprivate
, aunque apenas tiene Javapublic
,protected
, nada, yprivate
. Proporciona un control de grano más fino sobre el comportamiento de lo que está codificando, a expensas de tener más términos y palabras clave para realizar un seguimiento.En el caso de
new
vs.virtual+override
, va más o menos así:abstract
yoverride
en la subclase.virtual
yoverride
en la subclase.new
en la subclase.sealed
en la clase base.Para un ejemplo del mundo real: un proyecto en el que trabajé en pedidos de comercio electrónico procesados de muchas fuentes diferentes. Había una base
OrderProcessor
que tenía la mayor parte de la lógica, con ciertos métodosabstract
/virtual
para anular la clase secundaria de cada fuente. Esto funcionó bien, hasta que obtuvimos una nueva fuente que tenía una forma completamente diferente de procesar pedidos, de modo que tuvimos que reemplazar una función central. Teníamos dos opciones en este punto: 1) Agregarvirtual
al método base, yoverride
en el niño; o 2) Agregarnew
al niño.Si bien cualquiera de los dos podría funcionar, el primero haría que sea muy fácil anular ese método en particular nuevamente en el futuro. Aparecería en autocompletar, por ejemplo. Sin embargo, este fue un caso excepcional, por lo que elegimos usarlo
new
en su lugar. Eso preservó el estándar de "este método no necesita ser anulado", al tiempo que permite el caso especial donde lo hizo. Es una diferencia semántica que hace la vida más fácil.Ten en cuenta, sin embargo, que no es una diferencia de comportamiento asociado con esto, no sólo una diferencia semántica. Vea este artículo para más detalles. Sin embargo, nunca me he encontrado con una situación en la que necesitaba aprovechar este comportamiento.
fuente
new
esta manera es un error que espera suceder. Si llamo a un método en alguna instancia, espero que siempre haga lo mismo, incluso si lanzo la instancia a su clase base. Pero no es así como senew
comportan los métodos ed.new
es en WebViewPage <TModel> en el marco MVC. Pero también he sido arrojado por un error que involucranew
horas, por lo que no creo que sea una pregunta irrazonable.El diseño de Java es tal que, dada cualquier referencia a un objeto, una llamada a un nombre de método particular con tipos de parámetros particulares, si está permitido, siempre invocará el mismo método. Es posible que las conversiones implícitas de tipo de parámetro puedan verse afectadas por el tipo de referencia, pero una vez que se han resuelto todas esas conversiones, el tipo de referencia es irrelevante.
Esto simplifica el tiempo de ejecución, pero puede causar algunos problemas desafortunados. Supongamos
GrafBase
que no implementavoid DrawParallelogram(int x1,int y1, int x2,int y2, int x3,int y3)
, pero loGrafDerived
implementa como un método público que dibuja un paralelogramo cuyo cuarto punto calculado es opuesto al primero. Supongamos además que una versión posterior deGrafBase
implementa un método público con la misma firma, pero su cuarto punto calculado frente al segundo. Los clientes que reciben esperan unGrafBase
pero reciben una referencia a unGrafDerived
esperaránDrawParallelogram
calcular el cuarto punto en la forma del nuevoGrafBase
método, pero los clientes que han estado usandoGrafDerived.DrawParallelogram
antes de que se cambiara el método base esperarán el comportamiento queGrafDerived
se implementó originalmente.En Java, no habría forma de que el autor
GrafDerived
haga que esa clase coexista con clientes que usan el nuevoGrafBase.DrawParallelogram
método (y pueden no darse cuenta de queGrafDerived
incluso existe) sin romper la compatibilidad con el código de cliente existente que usóGrafDerived.DrawParallelogram
antes deGrafBase
definirlo. DadoDrawParallelogram
que no se puede saber qué tipo de cliente lo invoca, debe comportarse de manera idéntica cuando se invoca mediante tipos de código de cliente. Dado que los dos tipos de código de cliente tienen expectativas diferentes sobre cómo debería comportarse, no hay forma deGrafDerived
evitar violar las expectativas legítimas de uno de ellos (es decir, romper el código de cliente legítimo).En C #, si
GrafDerived
no se vuelve a compilar, el tiempo de ejecución asumirá que el código que invoca elDrawParallelogram
método sobre referencias de tipoGrafDerived
esperará el comportamiento queGrafDerived.DrawParallelogram()
tuvo cuando se compiló por última vez, pero el código que invoca el método sobre referencias de tipoGrafBase
esperaráGrafBase.DrawParallelogram
(el comportamiento que fue añadido). SiGrafDerived
luego se vuelve a compilar en presencia del mejoradoGrafBase
, el compilador graznará hasta que el programador especifique si su método fue un reemplazo válido para el miembro heredadoGrafBase
, o si su comportamiento debe estar vinculado a referencias de tipoGrafDerived
, pero no debe reemplazar el comportamiento de referencias de tipoGrafBase
.Se podría argumentar razonablemente que tener un método para
GrafDerived
hacer algo diferente de un miembroGrafBase
que tiene la misma firma indicaría un mal diseño y, como tal, no debería ser compatible. Desafortunadamente, dado que el autor de un tipo base no tiene forma de saber qué métodos pueden agregarse a los tipos derivados, ni viceversa, la situación en la que los clientes de clase base y clase derivada tienen expectativas diferentes para métodos con nombres similares es esencialmente inevitable a menos que nadie tiene permitido agregar ningún nombre que alguien más pueda agregar. La pregunta no es si tal duplicación de nombre debería ocurrir, sino cómo minimizar el daño cuando ocurre.fuente
DerivedGraphics? A class using
GrafDerived`?GrafDerived
. Empecé a usarDerivedGraphics
, pero sentí que era un poco largo. EvenGrafDerived
aún es un poco largo, pero no sabía cómo nombrar mejor los tipos de renderizador de gráficos que deberían tener una relación clara de base / derivada.Situación estándar:
Eres el propietario de una clase base que utilizan varios proyectos. Desea realizar un cambio en dicha clase base que dividirá entre 1 e innumerables clases derivadas, que se encuentran en proyectos que brindan valor en el mundo real (un marco proporciona valor en el mejor de los casos, un ser humano real quiere un marco, ellos quieren lo que se ejecuta en el Marco). Buena suerte para los ocupados propietarios de las clases derivadas; "bueno, tienes que cambiar, no deberías haber anulado ese método" sin obtener un representante como "Marco: Retraso de proyectos y Causador de errores" para las personas que tienen que aprobar decisiones.
Especialmente porque, al no declarar que no se puede anular, implícitamente has declarado que estaban bien hacer lo que ahora impide tu cambio.
Y si no tiene un número significativo de clases derivadas que brinden valor real al anular su clase base, ¿por qué es una clase base en primer lugar? La esperanza es un poderoso motivador, pero también una muy buena manera de terminar con un código sin referencia.
Resultado final: su código de clase base de framework se vuelve increíblemente frágil y estático, y realmente no puede hacer los cambios necesarios para mantenerse actualizado / eficiente. Alternativamente, su marco de trabajo obtiene un representante de la inestabilidad (las clases derivadas se siguen rompiendo) y la gente no lo usará en absoluto, ya que la razón principal para usar un marco es hacer que la codificación sea más rápida y confiable.
En pocas palabras, no puede pedirles a los propietarios de proyectos ocupados que retrasen su proyecto para corregir los errores que está presentando y esperar algo mejor que un "desaparecer" a menos que les brinde beneficios significativos, incluso si la "falla" original era de ellos, lo cual es discutible en el mejor de los casos.
Es mejor no dejar que hagan lo incorrecto en primer lugar, que es donde entra en juego "no virtual por defecto". Y cuando alguien se acerca a usted con una razón muy clara de por qué necesitan que este método en particular sea reemplazable, y por qué debe ser seguro, puede "desbloquearlo" sin correr el riesgo de romper el código de otra persona.
fuente
Por defecto a no virtual se supone que el desarrollador de la clase base es perfecto. En mi experiencia, los desarrolladores no son perfectos. Si el desarrollador de una clase base no puede imaginar un caso de uso en el que un método podría ser anulado u olvida agregar virtual, entonces no puedo aprovechar el polimorfismo al extender la clase base sin modificar la clase base. En el mundo real, modificar la clase base a menudo no es una opción.
En C #, el desarrollador de la clase base no confía en el desarrollador de la subclase. En Java, el desarrollador de la subclase no confía en el desarrollador de la clase base. El desarrollador de la subclase es responsable de la subclase y debería (en mi humilde opinión) tener el poder de extender la clase base como mejor le parezca (salvo la negación explícita, y en Java incluso pueden equivocarse).
Es una propiedad fundamental de la definición del lenguaje. No es correcto o incorrecto, es lo que es y no puede cambiar.
fuente