Herencia

Al definir una clase, un desarrollador puede heredarla de otra clase, incorporando así el concepto de herencia. Para ello, el nombre de la clase va seguido de un signo de dos puntos, un modificador opcional de derechos de acceso (una de las palabras clave public, protected, private) y el nombre de la clase padre. Por ejemplo, así es como podemos definir una clase Rectangle que derive de Shape:

class Rectangle : public Shape
{
};

Los modificadores de acceso de la cabecera de la clase controlan la «visibilidad» de los miembros de la clase padre incluidos en la clase hijo:

  • public : todos los miembros heredados conservan sus derechos y limitaciones.
  • protected : cambia los derechos de los miembros public heredados a protected
  • private: hace que todos los miembros heredados sean privados (private)

El modificador public se utiliza en la gran mayoría de las definiciones. Las otras dos opciones sólo tienen sentido en casos excepcionales, ya que infringen el principio básico de la herencia: los objetos de una clase derivada deben ser representantes de pleno derecho de la familia padre, y si «truncamos» sus derechos, pierden parte de sus características. Las estructuras también pueden heredarse entre sí de forma similar. Está prohibido heredar clases de estructuras o estructuras de clases.

A diferencia de C++, MQL5 no es compatible con la herencia múltiple. Una clase puede tener como máximo un padre.

Un objeto de clase derivada tiene integrado un objeto de clase base. Teniendo en cuenta que la clase base puede, a su vez, ser heredada de alguna otra clase padre, el objeto creado puede compararse con muñecas matrioskas anidadas unas dentro de otras.

En la nueva clase necesitamos un constructor que rellene los campos del objeto de la misma forma que se hizo en la clase base.

class Rectangle : public Shape
{
public:
   Rectangle(int pxint pycolor back) :
      Shape(pxpyback)
   {
      Print(__FUNCSIG__" ", &this);
   }
};

En este caso, la lista de inicialización se ha convertido en una única llamada al constructor Shape. No se pueden establecer directamente variables de clase base en una lista de inicialización, ya que el constructor base es responsable de inicializarlas. No obstante, si fuera necesario, podríamos cambiar los campos protected de la clase base desde el cuerpo del constructor Rectangle (las sentencias del cuerpo de la función se ejecutan después de que el constructor base haya completado su llamada en la lista de inicialización).

El rectángulo tiene dos dimensiones, así que vamos a añadirlas como campos protegidos dx y dy. Para establecer sus valores es necesario completar la lista de parámetros del constructor.

class Rectangle : public Shape
{
protected:
   int dxdy// dimensions (width, height)
   
public:
   Rectangle(int pxint pyint sxint sycolor back) :
      Shape(pxpyback), dx(sx), dy(sy)
   {
   }
};

Es importante señalar que los objetos Rectangle contienen implícitamente la función toString heredada de Shape (sin embargo, draw también está ahí, aunque sigue estando vacía). Por lo tanto, el siguiente código es correcto:

void OnStart()
{
   Rectangle r(1002005075clrBlue);
   Print(r.toString());
};

Esto demuestra no sólo la llamada a toString, sino también la creación de un objeto rectángulo utilizando nuestro nuevo constructor.

No hay constructor por defecto (sin parámetros) en la clase Rectangle. Esto significa que el usuario de la clase no puede crear objetos rectángulo de forma sencilla, sin argumentos:

   Rectangle r// 'Rectangle' - wrong parameters count

El compilador mostrará un error «Número de argumentos no válido».

Vamos a crear otra clase hija: Ellipse. Por ahora, no se diferenciará en nada de Rectangle, salvo en el nombre. Más adelante presentaremos las diferencias entre ellas.

class Ellipse : public Shape
{
protected:
   int dxdy// dimensions (large and small radii)
public:
   Ellipse(int pxint pyint rxint rycolor back) :
      Shape(pxpyback), dx(rx), dy(ry)
   {
      Print(__FUNCSIG__" ", &this);
   }
};

A medida que aumenta el número de clases, sería estupendo mostrar el nombre de la clase en el método toString. En la sección Operadores especiales sizeof y typename hemos descrito el operador typename. Vamos a intentar usarlo.

Recuerde que typename espera un parámetro, para el que se devuelve el nombre del tipo. Por ejemplo, si creamos un par de objetos s y r de las clases Shape y Rectangle, respectivamente, podemos averiguar su tipo de la siguiente manera:

void OnStart()
{
   Shape s;
   Rectangle r(1002007550clrRed);
   Print(typename(s), " "typename(r));      // Shape Rectangle
}

Pero necesitamos obtener este nombre dentro de la clase de alguna manera. Para ello, vamos a añadir un parámetro de cadena al constructor paramétrico Shape y a almacenarlo en un nuevo campo de cadena type (preste atención a la sección protected y al modificador const: este campo está oculto al mundo exterior y no puede editarse una vez creado el objeto):

class Shape
{
protected:
   ...
   const string type;
   
public:
   Shape(int pxint pycolor backstring t) :
      coordinates(pxpy),
      backgroundColor(back),
      type(t)
   {
      Print(__FUNCSIG__" ", &this);
   }
   ...
};

En los constructores de clases derivadas, rellenamos este parámetro del constructor base utilizando typename(this):

class Rectangle : public Shape
{
   ...
public:
   Rectangle(int pxint pyint sxint sycolor back) :
      Shape(pxpybacktypename(this)), dx(sx), dy(sy)
   {
      Print(__FUNCSIG__" ", &this);
   }
};

Ahora podemos mejorar el método toString utilizando el campo type.

class Shape
{
   ...
public:
   string toString() const
   {
      return type + " " + (string)coordinates.x + " " + (string)coordinates.y;
   }
};

Vamos a asegurarnos de que nuestra pequeña jerarquía de clases genera objetos según lo previsto e imprime entradas de registro de prueba cuando se llama a los constructores y destructores.

void OnStart()
{
   Shape s;
   //setting up an object by chaining calls via 'this'
   s.setColor(clrWhite).moveX(80).moveY(-50);
   Rectangle r(1002007550clrBlue);
   Ellipse e(200300100150clrRed);
   Print(s.toString());
   Print(r.toString());
   Print(e.toString());
}

Como resultado, obtenemos aproximadamente las siguientes entradas de registro (las líneas en blanco se añaden intencionadamente para separar la salida de diferentes objetos):

Pair::Pair(int,int) 0 0
Shape::Shape() 1048576
   
Pair::Pair(int,int) 100 200
Shape::Shape(int,int,color,string) 2097152
Rectangle::Rectangle(int,int,int,int,color) 2097152
   
Pair::Pair(int,int) 200 300
Shape::Shape(int,int,color,string) 3145728
Ellipse::Ellipse(int,int,int,int,color) 3145728
   
Shape 80 -50
Rectangle 100 200
Ellipse 200 300
   
Ellipse::~Ellipse() 3145728
Shape::~Shape() 3145728
Pair::~Pair() 200 300
   
Rectangle::~Rectangle() 2097152
Shape::~Shape() 2097152
Pair::~Pair() 100 200
   
Shape::~Shape() 1048576
Pair::~Pair() 80 -50

El registro deja claro en qué orden se llama a los constructores y destructores.

Para cada objeto, en primer lugar se crean los campos de objeto descritos en él (si los hay) y, a continuación, se llama al constructor base y a todos los constructores de las clases derivadas a lo largo de la cadena de herencia. Si hay campos propios (añadidos) de algunos tipos de objetos en una clase derivada, los constructores para ellos serán invocados inmediatamente antes del constructor de esta clase derivada. Cuando hay varios campos de objeto, estos se crean en el orden en que se describen en la clase.

Los destructores se invocan exactamente en el orden inverso.

En las clases derivadas se pueden definir constructores de copia, de los que hablamos en Constructores: predeterminado, paramétrico y de copia. Para tipos de forma específicos, como un rectángulo, su sintaxis es similar:

class Rectangle : public Shape
{
   ...
   Rectangle(const Rectangle &other) :
      Shape(other), dx(other.dx), dy(other.dy)
   {
   }
   ...
};

El ámbito de aplicación se amplía ligeramente. Un objeto de clase derivada puede utilizarse para copiar a una clase base (porque la clase derivada contiene todos los datos de la clase base). Sin embargo, en este caso, por supuesto, se ignoran los campos añadidos en la clase derivada.

void OnStart()
{
   Rectangle r(1002007550clrBlue);
   Shape s2(r);         // ok: copy derived to base
   
   Shape s;
   Rectangle r4(s);     // error: no one of the overloads can be applied 
                        // requires explicit constructor overloading
}

Para copiar en la dirección opuesta es necesario proporcionar una versión del constructor con una referencia a la clase derivada en la clase base (lo que, en teoría, contradice los principios de la programación orientada a objetos); de lo contrario se producirá el error de compilación «No se puede aplicar ninguna de las sobrecargas a la llamada a la función».

Ahora podemos incluir en el script un par o más de variables de forma para luego «pedirles» que se dibujen a sí mismas utilizando el método draw.

void OnStart()
{
   Rectangle r(1002005075clrBlue);
   Ellispe e(1002005075clrGreen);
   r.draw();
   e.draw();
};

Sin embargo, una entrada de este tipo significa que el número de formas, sus tipos y los parámetros están «programados» en el programa, mientras que el usuario debería ser capaz de elegir qué y dónde dibujar. De ahí la necesidad de crear formas de forma dinámica.