java - puro - ¿Por qué las funciones virtuales no deben ser utilizadas en exceso?
polimorfismo puro c++ (9)
Cada dependencia aumenta la complejidad del código y lo hace más difícil de mantener. Cuando define su función como virtual, crea dependencia de su clase en algún otro código, que podría no existir en este momento.
Por ejemplo, en C, puede encontrar fácilmente lo que hace foo (): solo hay un foo (). En C ++ sin funciones virtuales, es un poco más complicado: necesita explorar su clase y sus clases base para encontrar qué foo () necesitamos. Pero al menos puedes hacerlo de manera determinista por adelantado, no en tiempo de ejecución. Con funciones virtuales, no podemos decir qué foo () se ejecuta, ya que se puede definir en una las subclases.
(Otra cosa es el problema de rendimiento que mencionaste, debido a v-table).
Acabo de leer que no debemos usar la función virtual en exceso. La gente sentía que las funciones menos virtuales tienden a tener menos errores y reduce el mantenimiento.
¿Qué tipo de errores y desventajas pueden aparecer debido a las funciones virtuales?
Me interesa el contexto de C ++ o Java.
Una razón por la que puedo pensar es que la función virtual puede ser más lenta que las funciones normales debido a la búsqueda en la tabla v.
En C ++: -
Las funciones virtuales tienen una leve penalización de rendimiento. Normalmente es demasiado pequeño para hacer cualquier diferencia, pero en un circuito cerrado puede ser significativo.
Una función virtual aumenta el tamaño de cada objeto con un puntero. De nuevo, esto es típicamente insignificante, pero si creas millones de objetos pequeños podría ser un factor.
Las clases con funciones virtuales generalmente están destinadas a ser heredadas de. Las clases derivadas pueden reemplazar algunas, todas o ninguna de las funciones virtuales. Esto puede crear una complejidad adicional y la complejidad es el enemigo mortal de los programadores. Por ejemplo, una clase derivada puede implementar deficientemente una función virtual. Esto puede romper una parte de la clase base que se basa en la función virtual.
Ahora déjenme ser claro: no estoy diciendo "no use funciones virtuales". Son una parte vital e importante de C ++. Solo tenga en cuenta el potencial de complejidad.
La tabla virtual se crea para cada clase, teniendo funciones virtuales o derivadas de una clase que contiene funciones virtuales. Esto consume más espacio que el habitual.
El compilador debe insertar código extra silenciosamente para garantizar que se realice la vinculación tardía en lugar de la vinculación anticipada. Esto consume más que el tiempo habitual.
Las funciones virtuales son un poco más lentas que las funciones normales. Pero esa diferencia es tan pequeña que no hace la diferencia en todas las circunstancias excepto en las más extremas.
Creo que la mejor razón para evitar las funciones virtuales es protegerse contra el uso indebido de la interfaz.
Es una buena idea escribir clases para estar abierto para la extensión, pero hay tal cosa como demasiado abierto . Al planear cuidadosamente qué funciones son virtuales, puede controlar (y proteger) cómo se puede extender una clase.
Los errores y problemas de mantenimiento aparecen cuando una clase se extiende de modo que rompe el contrato de la clase base. Aquí hay un ejemplo:
class Widget
{
private WidgetThing _thing;
public virtual void Initialize()
{
_thing = new WidgetThing();
}
}
class DoubleWidget : Widget
{
private WidgetThing _double;
public override void Initialize()
{
// Whoops! Forgot to call base.Initalize()
_double = new WidgetThing();
}
}
Aquí, DoubleWidget rompió la clase primaria porque Widget._thing
es nulo. Hay una forma bastante estándar de solucionar esto:
class Widget
{
private WidgetThing _thing;
public void Initialize()
{
_thing = new WidgetThing();
OnInitialize();
}
protected virtual void OnInitialize() { }
}
class DoubleWidget : Widget
{
private WidgetThing _double;
protected override void OnInitialize()
{
_double = new WidgetThing();
}
}
Ahora Widget no se ejecutará en una NullReferenceException
más tarde.
No sé dónde lo leíste, pero no se trata de rendimiento en absoluto.
Tal vez se trata más de "preferir la composición sobre la herencia" y los problemas que pueden ocurrir si tus clases / métodos no son definitivos (estoy hablando principalmente de Java aquí), pero en realidad no están diseñados para su reutilización. Hay muchas cosas que pueden ir realmente mal:
Tal vez use métodos virtuales en su constructor: una vez que se reemplazan, su clase base llama al método reemplazado, que puede usar recursos inicializados en el constructor de la subclase, que se ejecuta más tarde (se levanta NPE).
Imagine un método add y addAll en una clase de lista. addAll llamadas se agregan muchas veces y ambas son virtuales. Alguien puede anularlos para contar cuántos elementos se han agregado. Si no documenta que addAll calls add, el desarrollador puede (y lo hará) anular add y addAll (y agregar algunas cosas de counter ++). Pero ahora, si usted agrega addAll, cada elemento se cuenta dos veces (agregar y agregar todo) lo que conduce a resultados incorrectos y errores difíciles de encontrar.
En resumen, si no diseña su clase para ser extendida (proporcionar ganchos, documentar algunas de las cosas importantes de implementación), no debe permitir la herencia en absoluto, ya que esto puede conducir a errores graves. También es fácil eliminar un modificador final (y tal vez rediseñarlo para volver a usarlo) de una de sus clases si es necesario, pero es imposible hacer una clase no final (donde la subclasificación conduzca a errores) final porque otros pueden haberlo subclasificado.
Tal vez fue realmente sobre el rendimiento, entonces al menos fuera del tema. Pero si no fue así, hay algunas buenas razones para no extender las clases si realmente no lo necesita.
Más información sobre este tipo de cosas en Blochs Effective Java (esta publicación en particular fue escrita unos días después de leer el elemento 16 ("preferir la composición sobre la herencia") y 17 ("diseñar y documentar por herencia o prohibirlo") - libro increíble .
Recientemente tuvimos un ejemplo perfecto de cómo el mal uso de las funciones virtuales introduce errores.
Hay una biblioteca compartida que cuenta con un manejador de mensajes:
class CMessageHandler {
public:
virtual void OnException( std::exception& e );
///other irrelevant stuff
};
la intención es que pueda heredar de esa clase y usarla para el manejo de errores personalizados:
class YourMessageHandler : public CMessageHandler {
public:
virtual void OnException( std::exception& e ) { //custom reaction here }
};
El mecanismo de manejo de errores utiliza un puntero CMessageHandler*
, por lo que no le importa el tipo real del objeto. La función es virtual, por lo que cada vez que existe una versión sobrecargada, se llama a la última.
Genial, ¿verdad? Sí, fue hasta que los desarrolladores de la biblioteca compartida cambiaron la clase base:
class CMessageHandler {
public:
virtual void OnException( const std::exception& e ); //<-- notice const here
///other irrelevant stuff
};
... y las sobrecargas simplemente dejaron de funcionar.
¿Ves lo que pasó? Después de que se cambió la clase base, las sobrecargas dejaron de ser las sobrecargas desde el punto de vista de C ++: se convirtieron en funciones nuevas, otras no relacionadas .
La clase base tenía la implementación predeterminada no marcada como puramente virtual, por lo que las clases derivadas no fueron forzadas a sobrecargar la implementación predeterminada. Y finalmente solo se llamó al functon en caso de manejo de error que no se utiliza aquí y allá. Entonces el error fue introducido silenciosamente y pasó inadvertido por un período bastante largo.
La única forma de eliminarlo de una vez por todas fue hacer una búsqueda en toda la base de código y editar todas las piezas relevantes de código.
Sospecho que malinterpretaste la declaración.
Excesivamente es un término muy subjetivo, creo que en este caso significaba "cuando no lo necesitas", no es que debas evitarlo cuando puede ser útil.
En mi experiencia, algunos estudiantes, cuando aprenden acerca de las funciones virtuales y se queman por primera vez al olvidarse de hacer una función virtual, piensan que es prudente simplemente hacer cada función virtual .
Dado que las funciones virtuales tienen un costo en cada invocación de método (que en C ++ normalmente no se puede evitar debido a la compilación por separado), esencialmente se está pagando ahora por cada llamada a un método y también se está evitando la creación de línea. Muchos instructores desalientan a los estudiantes a hacer esto, aunque el término "excesivo" es una opción muy pobre.
En Java, un comportamiento "virtual" (envío dinámico) es el predeterminado. Sin embargo, la JVM puede optimizar cosas sobre la marcha, y teóricamente podría eliminar algunas de las llamadas virtuales cuando la identidad del objetivo es clara. Además, los métodos o métodos finales en las clases finales a menudo se pueden resolver a un único objetivo también en tiempo de compilación.
Trabajé esporádicamente como consultor en el mismo sistema C ++ durante un período de aproximadamente 7 años, verificando el trabajo de aproximadamente 4-5 programadores. Cada vez que volví, el sistema había empeorado. En algún momento, alguien decidió eliminar todas las funciones virtuales y reemplazarlas por un sistema obtuso de fábrica / basado en RTTI que esencialmente hacía todo lo que las funciones virtuales ya estaban haciendo, pero peor, más costoso, miles de líneas más de código, mucho trabajo, muchas pruebas, ... Completamente sin sentido, y claramente miedo de lo desconocido.
También habían escrito a mano docenas de constructores de copia, con errores, cuando el compilador los habría producido automáticamente, sin errores, con aproximadamente tres excepciones donde se requería una versión manuscrita.
Moraleja: no luches contra el lenguaje. Te da cosas: úsalas.
Usted ha publicado algunas declaraciones generales que creo que la mayoría de los programadores pragmáticos ignorarían como mal informados o malinterpretados. Pero, existen fanáticos anti-virtuales, y su código puede ser tan malo para el rendimiento y el mantenimiento.
En Java, todo es virtual por defecto. Decir que no debes usar funciones virtuales excesivamente es bastante fuerte.
En C ++, debe declarar una función virtual, pero es perfectamente aceptable utilizarla cuando corresponda.
Acabo de leer que no debemos usar la función virtual en exceso.
Es difícil definir "excesivamente" ... ciertamente "usar funciones virtuales cuando sea apropiado" es un buen consejo.
La gente sentía que las funciones menos virtuales tienden a tener menos errores y reduce el mantenimiento. No consigo saber qué tipo de errores y desventajas pueden aparecer debido a las funciones virtuales.
El código mal diseñado es difícil de mantener. Período.
Si es un mantenedor de biblioteca, un código de depuración enterrado en una jerarquía de clase alta, puede ser difícil rastrear dónde se está ejecutando realmente el código, sin el beneficio de un IDE poderoso, a menudo es difícil decir qué clase anula el comportamiento. Puede conducir a muchos saltos entre los archivos que trazan árboles de herencia.
Entonces, hay algunas reglas generales, todas con excepciones:
- Mantenga sus jerarquías superficiales. Los árboles altos crean clases confusas.
- En c ++, si su clase tiene funciones virtuales, use un destructor virtual (si no, probablemente sea un error)
- Como con cualquier jerarquía, manténgase en una relación ''is-a'' entre las clases derivadas y las básicas.
- Debe tener en cuenta que una función virtual puede no llamarse en absoluto ... así que no agregue expectativas implícitas.
- Es difícil argumentar que las funciones virtuales son más lentas. Está vinculado dinámicamente, por lo que a menudo es el caso. Si importa en la mayoría de los casos que su citado es ciertamente discutible. Perfil y optimizar en su lugar :)
- En C ++, no use virtual cuando no sea necesario. Hay un significado semántico involucrado en marcar una función virtual, no abusar de ella. Deje que el lector sepa que "sí, esto puede ser anulado".
- Prefiere las interfaces virtuales puras a una jerarquía que mezcle la implementación. Es más limpio y mucho más fácil de entender.
La realidad de la situación es que las funciones virtuales son increíblemente útiles, y estas sombras de duda son poco probables provenientes de fuentes equilibradas: las funciones virtuales se han utilizado ampliamente durante mucho tiempo. Los idiomas más nuevos los están adoptando como predeterminados.