java - metodo virtual analisis estructural
Llamando al método virtual en el constructor de la clase base (9)
(Esta respuesta se aplica a C # y Java. Creo que C ++ funciona de manera diferente en este asunto).
Llamar a un método virtual en un constructor es realmente peligroso, pero a veces puede terminar con el código más limpio.
Intentaré evitarlo siempre que sea posible, pero sin doblar el diseño enormemente . (Por ejemplo, la opción "inicializar más tarde" prohíbe la inmutabilidad). Si utiliza un método virtual en el constructor, documéntelo con mucha fuerza. Mientras todos los involucrados estén al tanto de lo que está haciendo, no debería causar demasiados problemas. Sin embargo, trataría de limitar la visibilidad, como lo ha hecho en su primer ejemplo.
EDITAR: Una cosa que es importante aquí es que hay una diferencia entre C # y Java en orden de inicialización. Si tienes una clase como:
public class Child : Parent
{
private int foo = 10;
protected override void ShowFoo()
{
Console.WriteLine(foo);
}
}
donde el constructor principal llama a ShowFoo
, en C # se mostrará 10. El programa equivalente en Java mostraría 0.
Sé que llamar a un método virtual desde un constructor de clase base puede ser peligroso ya que la clase secundaria puede no estar en un estado válido. (al menos en C #)
Mi pregunta es ¿qué ocurre si el método virtual es el que inicializa el estado del objeto? ¿Es una buena práctica o debe ser un proceso de dos pasos, primero para crear el objeto y luego cargar el estado?
Primera opción: (usando el constructor para inicializar el estado)
public class BaseObject {
public BaseObject(XElement definition) {
this.LoadState(definition);
}
protected abstract LoadState(XElement definition);
}
Segunda opción: (usando un proceso de dos pasos)
public class BaseObject {
public void LoadState(XElement definition) {
this.LoadStateCore(definition);
}
protected abstract LoadStateCore(XElement definition);
}
En el primer método, el consumidor del código puede crear e inicializar el objeto con una declaración:
// The base class will call the virtual method to load the state.
ChildObject o = new ChildObject(definition)
En el segundo método, el consumidor tendrá que crear el objeto y luego cargar el estado:
ChildObject o = new ChildObject();
o.LoadState(definition);
Con C ++, los métodos virtuales se enrutan a través del vtable para la clase que se está construyendo. Por lo tanto, en su ejemplo, generaría una excepción de método virtual puro, ya que mientras se está construyendo BaseObject, simplemente no hay un método LoadStateCore para invocar.
Si la función no es abstracta, pero simplemente no hace nada, a menudo el programador se rasca la cabeza por un rato tratando de recordar por qué la función no se llama.
Por esta razón, simplemente no puedes hacerlo de esta manera en C ++ ...
En C ++ es perfectamente seguro llamar funciones virtuales desde dentro de la clase base, siempre que no sean puras , con algunas restricciones. Sin embargo, no deberías hacerlo. Inicialice mejor los objetos utilizando funciones no virtuales, que están marcadas explícitamente como tales funciones de inicialización mediante comentarios y un nombre apropiado (como initialize
). Si incluso se declara como puro-virtual en la clase que lo llama, el comportamiento no está definido.
La versión que se llama es la de la clase que lo llama desde el constructor, y no un overrider en alguna clase derivada. Esto no tiene mucho que ver con las tablas de funciones virtuales, pero más con el hecho de que la anulación de esa función podría pertenecer a una clase que aún no se ha inicializado. Entonces esto está prohibido.
En C # y Java, eso no es un problema, porque no existe una inicialización por defecto que se realice justo antes de ingresar al cuerpo del constructor. En C #, lo único que se hace fuera del cuerpo es llamar a los constructores de clase base o hermanos, creo. En C ++, sin embargo, las inicializaciones hechas a los miembros de las clases derivadas por el overrider de esa función se desharían al construir esos miembros mientras se procesa la lista de inicializadores del constructor justo antes de ingresar al cuerpo de constructores de la clase derivada.
Editar : Debido a un comentario, creo que se necesita un poco de aclaración. Aquí hay un ejemplo (artificial), supongamos que se le permitiría llamar a los virtuales, y la llamada daría como resultado una activación del overrider final:
struct base {
base() { init(); }
virtual void init() = 0;
};
struct derived : base {
derived() {
// we would expect str to be "called it", but actually the
// default constructor of it initialized it to an empty string
}
virtual void init() {
// note, str not yet constructed, but we can''t know this, because
// we could have called from derived''s constructors body too
str = "called it";
}
private:
string str;
};
Ese problema puede resolverse cambiando el estándar de C ++ y permitiéndolo, ajustando la definición de constructores, la duración del objeto y demás. Deben establecerse reglas para definir qué str = ...;
significa para un objeto aún no construido. Y observe cómo el efecto depende entonces de quién llamó a init
. La característica que obtenemos no justifica los problemas que tenemos que resolver en ese momento. Entonces C ++ simplemente prohíbe el despacho dinámico mientras se construye el objeto.
En C ++, llamar a un método virtual en un constructor de clase base simplemente llamará al método como si la clase derivada aún no existiera (porque no lo hace). Entonces eso significa que la llamada se resuelve en tiempo de compilación a cualquier método al que deba llamar en la clase base (o clases de las que se deriva).
Probado con GCC, le permite llamar a una función virtual pura desde un constructor, pero le da una advertencia y da como resultado un error de tiempo de enlace. Parece que este comportamiento no está definido por el estándar:
"Las funciones miembro se pueden llamar desde un constructor (o destructor) de una clase abstracta: el efecto de realizar una llamada virtual ( clase.virtual ) a una función virtual pura directa o indirectamente para el objeto que se crea (o destruye) de tal constructor (o destructor) no está definido. "
Para C ++, el constructor base se llama antes del constructor derivado, lo que significa que la tabla virtual (que contiene las direcciones de las funciones virtuales anuladas de la clase derivada) aún no existe. Por esta razón, se considera algo MUY peligroso de hacer (especialmente si las funciones son puramente virtuales en la clase base ... esto causará una excepción pura virtual).
Hay dos formas de evitar esto:
- Haz un proceso de construcción en dos pasos + inicialización
- Mueva las funciones virtuales a una clase interna que pueda controlar más de cerca (puede usar el enfoque anterior, vea ejemplos para más detalles)
Un ejemplo de (1) es:
class base
{
public:
base()
{
// only initialize base''s members
}
virtual ~base()
{
// only release base''s members
}
virtual bool initialize(/* whatever goes here */) = 0;
};
class derived : public base
{
public:
derived ()
{
// only initialize derived ''s members
}
virtual ~derived ()
{
// only release derived ''s members
}
virtual bool initialize(/* whatever goes here */)
{
// do your further initialization here
// return success/failure
}
};
Un ejemplo de (2) es:
class accessible
{
private:
class accessible_impl
{
protected:
accessible_impl()
{
// only initialize accessible_impl''s members
}
public:
static accessible_impl* create_impl(/* params for this factory func */);
virtual ~accessible_impl()
{
// only release accessible_impl''s members
}
virtual bool initialize(/* whatever goes here */) = 0;
};
accessible_impl* m_impl;
public:
accessible()
{
m_impl = accessible_impl::create_impl(/* params to determine the exact type needed */);
if (m_impl)
{
m_impl->initialize(/* ... */); // add any initialization checking you need
}
}
virtual ~accessible()
{
if (m_impl)
{
delete m_impl;
}
}
/* Other functionality of accessible, which may or may not use the impl class */
};
Enfoque (2) utiliza el patrón de fábrica para proporcionar la implementación adecuada para la clase accessible
(que proporcionará la misma interfaz que su clase base
). Uno de los principales beneficios aquí es que obtiene la inicialización durante la construcción de accessible
que puede hacer uso de miembros virtuales de accessible_impl
forma segura.
Para C ++, lea el artículo correspondiente de Scott Meyer:
Nunca llame a funciones virtuales durante la construcción o destrucción
PD: preste atención a esta excepción en el artículo:
El problema casi con certeza se hará evidente antes del tiempo de ejecución, porque la función logTransaction es puramente virtual en Transaction. A menos que se haya definido ( poco probable, pero posible ), el programa no vincularía: el vinculador no podría encontrar la implementación necesaria de Transaction :: logTransaction.
Para C ++, sección 12.7, el párrafo 3 de la Norma cubre este caso.
Para resumir, esto es legal. Resolverá la función correcta según el tipo de constructor que se está ejecutando. Por lo tanto, al adaptar su ejemplo a la sintaxis de C ++, estaría llamando a BaseObject::LoadState()
. No puede acceder a ChildObject::LoadState()
, e intenta hacerlo especificando la clase y los resultados de la función en un comportamiento indefinido.
Los constructores de clases abstractas se tratan en la sección 10.4, párrafo 6. En resumen, pueden llamar a funciones miembro, pero llamar a una función virtual pura en el constructor es un comportamiento indefinido. No hagas eso.
Por lo general, puede evitar estos problemas teniendo un constructor base más codicioso. En su ejemplo, está pasando un XElement a LoadState. Si permite que el estado se establezca directamente en su constructor base, entonces su clase secundaria puede analizar el XElement antes de llamar a su constructor.
public abstract class BaseObject {
public BaseObject(int state1, string state2, /* blah, blah */) {
this.State1 = state1;
this.State2 = state2;
/* blah, blah */
}
}
public class ChildObject : BaseObject {
public ChildObject(XElement definition) :
base(int.Parse(definition["state1"]), definition["state2"], /* blah, blah */) {
}
}
Si la clase de niños necesita hacer un buen trabajo, puede descargar a un método estático.
Si tiene una clase como se muestra en su publicación, que toma un elemento XElement
en el constructor, entonces el único lugar del que podría proceder XElement
es la clase derivada. Entonces, ¿por qué no simplemente cargar el estado en la clase derivada que ya tiene XElement
?
O a su ejemplo le falta alguna información fundamental que cambia la situación, o simplemente no hay necesidad de volver a la clase derivada con la información de la clase base, porque acaba de decirle esa información exacta.
es decir
public class BaseClass
{
public BaseClass(XElement defintion)
{
// base class loads state here
}
}
public class DerivedClass : BaseClass
{
public DerivedClass (XElement defintion)
: base(definition)
{
// derived class loads state here
}
}
Entonces su código es realmente simple, y no tiene ninguno de los problemas de llamada de método virtual.