Ocultación de miembros

     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 anterior):

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ícitamente que queremos 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 exactamente 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
  {
     // Campo de cada objeto Persona que almacena su nombre
     public string Nombre;     
     // Campo de cada objeto Persona que almacena su edad
     public int Edad;              
     // Campo de cada objeto Persona que almacena su NIF
     public string NIF;            
    
     // Incrementa en uno la edad del objeto Persona
     public virtual void Cumpleaños()
     {
     Console.WriteLine("Incrementada edad de persona");
     }
 
    // Constructor de Persona
     public Persona (string nombre, int edad, string nif)
     {
      Nombre = nombre;
      Edad = edad;
      NIF = nif;
     }
  }
 
  class Trabajador: Persona
 
  {  // Campo de cada objeto Trabajador que almacena cuánto gana
     int Sueldo;
 
     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 polimorfismo, 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áneamente tengan 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 mnemoté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
{
 // F() es un método redefinible
 public virtual void F()     
 {
  Console.WriteLine("F() de A");
 } 
}
class B: A
{
  // Oculta la versión de F() de A sólo dentro de B
  new private void F() {} 
}
 
class C: B
{
 // Válido, pues aquí sólo se ve el F() de A
 public override void F()
 {
  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 donde implícitamente todos los miembros son virtuales, como Java, esto 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 existí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.

Ocultación de miembros
José Antonio González Seco

José Antonio es experto en tecnologias Microsoft. Imparte cursos y conferencias en congresos sobre C# y .NET en Universidades de toda España (Sevilla, Barcelona, San Sebastián, Valencia, Oviedo, etc.) en representación de grandes empresas como Microsoft.
Fecha de alta:03/10/2006
Última actualizacion:03/10/2006
Visitas totales:27134
Valorar el contenido:
Últimas consultas realizadas en los foros
Últimas preguntas sin contestar en los foros de devjoker.com