En Java no existen virtual, new, overridepalabras 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 newo virtual+deriveo nueva + Virtual de anulación está trabajando.
No puedo entender por qué en el mundo voy a agregar un método en mi DerivedClasscon el mismo nombre y la misma firma BaseClassy definir un nuevo comportamiento, pero en el polimorfismo en tiempo de ejecución, ¡ BaseClassse invocará el método! (que no está anulando pero lógicamente debería estarlo).
En caso de virtual + overrideque 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 + overrideen lugar de newy también el uso de newen 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 SpaceShipderivada de Vehicle. Entonces quiero cambiar todo cara un SpaceShipobjeto, 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 SpaceShipno anula la Vehicleclase acceleratey el uso, newentonces 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
newyoverrideen C #, consulte esta página de MSDN ).Un escenario muy práctico es este:
Vehicle.Vehicle.Vehicleclase no tenía ningún métodoPerformEngineCheck().Carclase, 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 seVehiclehaga referencia no se veráCar.PerformEngineCheck()llamado, pero el código al que hizo referenciaCarcontinuará 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 lavirtualpalabra clave) como en la clase derivada (a través de las palabras clavenewyoverride).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
newdeberí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
SpaceShiprealmente está anulandoVehicle.accelerate()o si es diferente? Tiene que ser elSpaceShipdesarrollador. Entonces, si elSpaceShipdesarrollador 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
finalmodificador, 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.@Overridese 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
newvs.virtual+override, va más o menos así:abstractyoverrideen la subclase.virtualyoverrideen la subclase.newen la subclase.sealeden 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
OrderProcessorque tenía la mayor parte de la lógica, con ciertos métodosabstract/virtualpara 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) Agregarvirtualal método base, yoverrideen el niño; o 2) Agregarnewal 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
newen 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
newesta 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 senewcomportan los métodos ed.newes en WebViewPage <TModel> en el marco MVC. Pero también he sido arrojado por un error que involucranewhoras, 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
GrafBaseque no implementavoid DrawParallelogram(int x1,int y1, int x2,int y2, int x3,int y3), pero loGrafDerivedimplementa 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 deGrafBaseimplementa un método público con la misma firma, pero su cuarto punto calculado frente al segundo. Los clientes que reciben esperan unGrafBasepero reciben una referencia a unGrafDerivedesperaránDrawParallelogramcalcular el cuarto punto en la forma del nuevoGrafBasemétodo, pero los clientes que han estado usandoGrafDerived.DrawParallelogramantes de que se cambiara el método base esperarán el comportamiento queGrafDerivedse implementó originalmente.En Java, no habría forma de que el autor
GrafDerivedhaga que esa clase coexista con clientes que usan el nuevoGrafBase.DrawParallelogrammétodo (y pueden no darse cuenta de queGrafDerivedincluso existe) sin romper la compatibilidad con el código de cliente existente que usóGrafDerived.DrawParallelogramantes deGrafBasedefinirlo. DadoDrawParallelogramque 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 deGrafDerivedevitar violar las expectativas legítimas de uno de ellos (es decir, romper el código de cliente legítimo).En C #, si
GrafDerivedno se vuelve a compilar, el tiempo de ejecución asumirá que el código que invoca elDrawParallelogrammétodo sobre referencias de tipoGrafDerivedesperará el comportamiento queGrafDerived.DrawParallelogram()tuvo cuando se compiló por última vez, pero el código que invoca el método sobre referencias de tipoGrafBaseesperaráGrafBase.DrawParallelogram(el comportamiento que fue añadido). SiGrafDerivedluego 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
GrafDerivedhacer algo diferente de un miembroGrafBaseque 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 usingGrafDerived`?GrafDerived. Empecé a usarDerivedGraphics, pero sentí que era un poco largo. EvenGrafDerivedaú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