Hay ocasiones en las que puede
resultar interesante usar la herencia únicamente como mecanismo de
reutilización de código pero no necesariamente para reutilizar miembros. Es
decir, puede que interese heredar de una clase sin que ello implique que su
clase hija herede sus miembros tal cuales sino con ligeras modificaciones.
Esto puede muy útil al usar la
herencia para definir versiones especializadas de clases de uso genérico. Por
ejemplo, los objetos de la clase System.Collections.ArrayList incluida en la BCL pueden almacenar cualquier número
de objetos System.Object,
que al ser la clase primigenia ello significa que pueden almacenar objetos de
cualquier tipo. Sin embargo, al recuperarlos de este almacén genérico se tiene
el problema de que los métodos que para
ello se ofrecen devuelven objetos System.Object, lo que implicará que muchas veces
haya luego que reconvertirlos a su tipo original mediante downcasting para
poder así usar sus métodos específicos. En su lugar, si sólo se va a usar un ArrayList para
almacenar objetos de un cierto tipo puede resultar más cómodo usar un objeto de
alguna clase derivada de ArrayList cuyo método extractor de objetos oculte
al heredado de ArrayList
y devuelva directamente objetos de ese tipo.
Para ver más claramente cómo
hacer la ocultación, vamos a tomar el siguiente ejemplo donde se deriva de una
clase con un método void F() pero
se desea que en la clase hija el método que se tenga sea de la forma int F():
class Padre
{
public void F()
{}
}
class
Hija:Padre
{
public int F()
{return 1;}
}
Como en C# no se admite que en
una misma clase hayan dos métodos que sólo se diferencien en sus valores de
retorno, puede pensarse que el código anterior producirá un error de
compilación. Sin embargo, esto no es así sino que el compilador lo que hará
será quedarse únicamente con la versión definida en la clase hija y desechar la
heredada de la clase padre. A esto se le conoce como ocultación de miembro ya que hace desparacer en la clase hija el
miembro heredado, y cuando al compilar se detecte se generará el siguiente de
aviso (se supone que clases.cs almacena
el código anteiror):
clases.cs(9,15):
warning CS0108: The keyword new is required on 'Hija.F()' because it hides
inherited member 'Padre.F()'
Como generalmente cuando se
hereda interesa que la clase hija comparta los mismos miembros que la clase
padre (y si acaso que añada miembros extra), el compilador emite el aviso anterior para indicar que no
se está haciendo lo habitual. Si
queremos evitarlo hemos de preceder la definición del método ocultador de la
palabra reservada new para así indicar
explíctamente que lo que queremos hacer es ocultar el F()
heredado:
class Padre
{
public void F()
{}
}
class
Hija:Padre
{
new public int F()
{return 1;}
}
En realidad la ocultación de
miembros no implica los miembros ocultados tengan que ser métodos, sino que
también pueden ser campos o cualquiera de los demás tipos de miembro que en
temas posteriores se verán. Por ejemplo, puede que se desee que un campo X de tipo int esté disponible en la clase hija como si
fuese de tipo string.
Tampoco implica que los miembros
métodos ocultados tengan que diferenciarse de los métodos ocultadores en su tipo
de retorno, sino que pueden tener exáctamente su mismo tipo de retorno,
parámetros y nombre. Hacer esto puede dar lugar a errores muy sutiles como el
incluido en la siguiente variante de la clase Trabajador
donde en vez de redefinirse Cumpleaños()
lo que se hace es ocultarlo al olvidar incluir el override:
using System;
class Persona
{
public string Nombre; // Campo de cada objeto Persona que
almacena su nombre
public int Edad; // Campo de cada objeto Persona que almacena su edad
public string NIF; // Campo de cada objeto Persona
que almacena su NIF
public virtual void Cumpleaños() //
Incrementa en uno la edad del objeto Persona
{
Console.WriteLine(“Incrementada
edad de persona”);
}
public Persona (string nombre, int edad, string nif) // Constructor de
Persona
{
Nombre
= nombre;
Edad
= edad;
NIF
= nif;
}
}
class
Trabajador: Persona
{
int Sueldo; // Campo de cada objeto
Trabajador que almacena cuánto gana
Trabajador(string nombre, int edad, string
nif, int sueldo): base(nombre, edad, nif)
{ // Inicializamos cada Trabajador en base al
constructor de Persona
Sueldo = sueldo;
}
public Cumpleaños()
{
Edad++;
Console.WriteLine("Incrementada
edad de trabajador");
}
public static void Main()
{
Persona p = new
Trabajador("Josan", 22, "77588260-Z", 100000);
p.Cumpleaños();
//
p.Sueldo++; //ERROR: Sueldo no es miembro de Persona
}
}
Al no incluirse override
se ha perdido la capacidad de polimorifsmo, y ello puede verse en que la salida
que ahora mostrara por pantalla el código:
Incrementada edad
de persona
Errores de este tipo son muy
sutiles y podrían ser difíciles de detectar. Sin embargo, en C# es fácil hacerlo gracias a que el compilador emitirá
el mensaje de aviso ya visto por haber hecho la ocultación sin new.
Cuando el programador lo vea podrá añadir new para suprimirlo si realmente lo que quería
hacer era ocultar, pero si esa no era su intención así sabrá que tiene que
corregir el código (por ejemplo, añadiendo el override olvidado)
Como su propio nombre indica, cuando se redefine un método
se cambia su definición original y por ello las llamadas al mismo ejecutaran
dicha versión aunque se hagan a través de variables de la clase padre que
almacenen objetos de la clase hija
donde se redefinió. Sin embargo, cuando se oculta un método no se cambia
su definición en la clase padre sino
sólo en la clase hija, por lo que las llamadas al mismo realizadas a través de
variables de la clase padre ejecutarán la versión de dicha clase padre y las
realizadas mediante variables de la clase hija ejecutarán la versión de la
clase hija.
En realidad el polimorfismo y la ocultación no son
conceptos totalmente antagónicos, y aunque no es válido definir métodos que
simultánemente cuenten con los modificadores override y new
ya que un método ocultador es como si fuese la primera versión que se hace del
mismo (luego no puede redefinirse algo no definido), sí que es posible combinar
new
y virtual
para definir métodos ocultadores redefinibles. Por ejemplo:
using System;
class A
{
public virtual
void F() { Console.WriteLine("A.F"); }
}
class B: A
{
public override
void F() { Console.WriteLine("B.F"); }
}
class C: B
{
new public virtual void F() {
Console.WriteLine("C.F"); }
}
class D: C
{
public override void F() {
Console.WriteLine("D.F"); }
}
class Ocultación
{
public static void
Main()
{
A a = new D();
B
b = new D();
C c = new
D();
D d = new
D();
a.F();
b.F();
c.F();
d.F();
}
}
La salida por pantalla de este
programa es:
B.F
B.F
D.F
D.F
Aunque el verdadero tipo de los
objetos a cuyo método se llama en Main()
es D, en las dos primeras llamadas
se llama al F() de B. Esto se debe a que la redefinición dada en B cambia la versión de F()
en A por
la suya propia, pero la ocultación dada en C
hace que para la redefinición que posteriormente se da en D se considere que la versión original de F() es la dada en C
y ello provoca que no modifique la versiones de dicho método dadas en A y B (que,
por la redefinición dada en B, en ambos casos son la versión de B)
Un truco nemotécnico que puede
ser útil para determinar a qué versión del método se llamará en casos complejos
como el anterior consiste en considerar que el mecanismo de polimorfismo
funciona como si buscase el verdadero tipo del objeto a cuyo método se llama
descendiendo en la jerarquía de tipos desde el tipo de la variable sobre la que
se aplica el método y de manera que si durante dicho recorrido se llega a
alguna versión del método con new se para la búsqueda y se queda con la versión
del mismo incluida en el tipo recorrido justo antes del que tenía el método
ocultador.
Hay que tener en cuenta que el
grado de ocultación que proporcione new depende del nivel de
accesibilidad del método ocultador, de modo que si es privado sólo ocultará
dentro de la clase donde esté definido. Por ejemplo, dado:
using System;
class A
{
public virtual
void F() // F() es un método
redefinible
{
Console.WriteLine(“F()
de A”);
}
}
class B: A
{
new private void F() {} // Oculta la versión de F() de A sólo dentro
de B
}
class C: B
{
public override
void F() // Válido, pues aquí sólo se ve el F() de A
{
base.F();
Console.WriteLine(“F()
de B”);
}
public static void Main()
{
C
obj = new C();
obj.F();
}
}
La salida de este programa por pantalla será:
F() de A
F() de B
Pese a todo lo comentado, hay que
resaltar que la principal utilidad de poder indicar explícitamente si se desea
redefinir u ocultar cada miembro es que facilita enormemente la resolución de
problemas de versionado de tipos que
puedan surgir si al derivar una nueva clase de otra y añadirle miembros
adicionales, posteriormente se la desea actualizar con una nueva versión de su
clase padre pero ésta contiene miembros que entran en conflictos con los
añadidos previamente a la clase hija cuando aún no existían en la clase padre.
En lenguajes como Java donde todos los miembros son implícitamente virtuales
estos da lugar a problemas muy graves debidos sobre todo a:
·
Que por sus nombres los nuevos miembros de la clase
padre entre en conflictos con los añadidos a la clase hija cuando no exisitían.
Por ejemplo, si la versión inicial de
de la clase padre no contiene ningún método de nombre F(),
a la clase hija se le añade void F() y
luego en la nueva versión de la clase padre se incorporado int F(), se producirá un error por tenerse en la
clase hija dos métodos F()
En Java para
resolver este problema una posibilidad sería pedir al creador de la clase padre
que cambiase el nombre o parámetros de su método, lo cual no es siempre posible
ni conveniente en tanto que ello podría trasladar el problema a que hubiesen
derivado de dicha clase antes de volverla a modificar. Otra posibilidad sería
modificar el nombre o parámetros del método en la clase hija, lo que nuevamente
puede llevar a incompatibilidades si también se hubiese derivado de dicha clase
hija.
·
Que los nuevos miembros tengan los mismos nombres y
tipos de parámetros que los incluidos en las clases hijas y sea obligatorio que
toda redefinición que se haga de ellos siga un cierto esquema.
Esto es muy
problemático en lenguajes como Java donde toda definición de método con igual
nombre y parámetros que alguno de su clase padre es considerado implícitamente
redefinición de éste, ya que difícilmente en una clase hija escrita con
anterioridad a la nueva versión de la clase padre se habrá seguido el esquema
necesario. Por ello, para resolverlo habrá que actualizar la clase hija para
que lo siga y de tal manera que los cambios que se le hagan no afecten a sus
subclases, lo que ello puede ser más o menos difícil según las características
del esquema a seguir.
Otra
posibilidad sería sellar el método en la clase hija, pero ello recorta la
capacidad de reutilización de dicha clase y sólo tiene sentido si no fue
redefinido en ninguna subclase suya.
En C# todos estos problemas son
de fácil solución ya que pueden resolverse con sólo ocultar los nuevos miembros
en la clase hija y seguir trabajando como si no existiesen.