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.
Hay varias razones. Uno es el rendimiento . Podemos observar que a medida que las personas escriben código en Java, se olvidan de marcar sus métodos como finales. Por lo tanto, esos métodos son virtuales. Como son virtuales, no funcionan tan bien. Solo hay una sobrecarga de rendimiento asociada con ser un método virtual. Ese es un problema.
Un tema más importante es el versionado . Hay dos escuelas de pensamiento sobre los métodos virtuales. La escuela de pensamiento académica dice: "Todo debería ser virtual, porque algún día querré anularlo". La escuela pragmática de pensamiento, que proviene de la construcción de aplicaciones reales que se ejecutan en el mundo real, dice: "Tenemos que tener mucho cuidado con lo que hacemos virtual".
Cuando hacemos algo virtual en una plataforma, hacemos muchas promesas sobre cómo evolucionará en el futuro. Para un método no virtual, prometemos que cuando llame a este método, x e y sucederán. Cuando publicamos un método virtual en una API, no solo prometemos que cuando llame a este método, x e y sucederán. También prometemos que cuando anule este método, lo llamaremos en esta secuencia particular con respecto a estos otros y el estado estará en esto y aquello invariante.
Cada vez que dices virtual en una API, estás creando un enlace de devolución de llamada. Como diseñador de marcos de SO o API, debes tener mucho cuidado con eso. No desea que los usuarios anulen y enganchen en ningún punto arbitrario en una API, porque no necesariamente puede hacer esas promesas. Y las personas pueden no entender completamente las promesas que están haciendo cuando hacen algo virtual.
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:
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 que BaseClass y definir un nuevo comportamiento, pero en el polimorfismo en tiempo de ejecución, ¡se invocará el método BaseClass! (que no está anulando pero lógicamente debería estarlo).
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
y override
en C #, consulte esta página de MSDN ).
Un escenario muy práctico es este:
- Creó una API, que tiene una clase llamada
Vehicle
.
- Empecé a usar su API y derivado
Vehicle
.
- Tu
Vehicle
clase no tenía ningún método PerformEngineCheck()
.
- En mi
Car
clase, agrego un método PerformEngineCheck()
.
- Lanzaste una nueva versión de tu API y agregaste un
PerformEngineCheck()
.
- No puedo cambiar el nombre de mi método porque mis clientes dependen de mi API y eso los rompería.
Entonces, cuando vuelvo a compilar contra su nueva API, C # me advierte sobre este problema, por ejemplo
Si la base PerformEngineCheck()
no era virtual
:
app2.cs(15,17): warning CS0108: 'Car.PerformEngineCheck()' hides inherited member 'Vehicle.PerformEngineCheck()'.
Use the new keyword if hiding was intended.
Y si la base PerformEngineCheck()
era virtual
:
app2.cs(15,17): warning CS0114: 'Car.PerformEngineCheck()' hides inherited member 'Vehicle.PerformEngineCheck()'.
To make the current member override that implementation, add the override keyword. Otherwise add the new keyword.
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.
- Al hacerlo
new
, no rompo a mis clientes si la funcionalidad del método base era diferente del método derivado. Cualquier código al que se Vehicle
haga referencia no se verá Car.PerformEngineCheck()
llamado, pero el código al que hizo referencia Car
continuará viendo la misma funcionalidad que yo había ofrecido PerformEngineCheck()
.
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 la PerformEngineCheck()
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 la virtual
palabra clave) como en la clase derivada (a través de las palabras clave new
y override
).
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
Pero en el caso de C # si SpaceShip no anula la aceleración de la clase Vehículo y usa una nueva, entonces la lógica de mi código se romperá. ¿No es eso una desventaja?
¿Quién decide si SpaceShip
realmente está anulando Vehicle.accelerate()
o si es diferente? Tiene que ser el SpaceShip
desarrollador. Entonces, si el SpaceShip
desarrollador decide que no está cumpliendo con el contrato de la clase base, entonces su llamada a Vehicle.accelerate()
no debería ir SpaceShip.accelerate()
, ¿o debería? Es entonces cuando lo marcarán como new
. Sin embargo, si deciden que efectivamente mantiene el contrato, lo marcarán override
. 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 si SpaceShip.accelerate()
realmente está anulando Vehicle.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 de Vehicle.accelerate()
, la llamada al método todavía ir a SpaceShip.accelerate()
.