Interfaces

Concepto de interfaz

    Una interfaz es la definición de un conjunto de métodos para los que no se da implementación, sino que se les define de manera similar a como se definen los métodos abstractos. Es más, una interfaz puede verse como una forma especial de definir clases abstractas que tan sólo contengan miembros abstractos.

    Como las clases abstractas, las interfaces son tipos referencia, no puede crearse objetos de ellas sino sólo de tipos que deriven de ellas, y participan del polimorfismo. Sin embargo, también tienen numerosas diferencias con éstas:

  • Es posible definir tipos que deriven de más de una interfaz. Esto se debe a que los problemas que se podrían presentar al crear tipos que hereden de varios padres se deben a la difícil resolución de los conflictos derivados de la herencia de varias implementaciones diferentes de un mismo método. Sin embargo, como con las interfaces esto nunca podrá ocurrir en tanto que no incluyen código, se permite la herencia múltiple de las mismas.

  • Las estructuras no pueden heredar de clases pero sí de interfaces, y las interfaces no pueden derivar de clases, pero sí de otras interfaces.

  • Todo tipo que derive de una interfaz ha de dar una implementación de todos los miembros que hereda de esta, y no como ocurre con las clases abstractas donde es posible no darla si se define como abstracta también la clase hija. De esta manera queda definido un contrato en la clase que la hereda que va a permitir poder usarla con seguridad en situaciones polimórficas: toda clase que herede una interfaz implementará todos los métodos de la misma. Por esta razón se suele denominar implementar una interfaz al hecho de heredar de ella.

    Nótese que debido a esto, no suele convenir ampliar interfaces ya definidas e implementadas, puesto que cualquier añadido invalidará sus implementaciones hasta que se defina en las mismas un implementación para dicho añadido. Sin embargo, si se hereda de una clase abstracta este problema no se tendrá siempre que el miembro añadido a la clase abstracta no sea abstracto.

  • Las interfaces sólo pueden tener como miembros métodos normales, eventos, propiedades e indizadores; pero no pueden incluir definiciones de campos, operadores, constructores, destructores o miembros estáticos. Además, todos los miembros de las interfaces son implícitamente públicos y no se les puede dar ningún modificador de acceso (ni siquiera public, pues se supone)

Definición de interfaces

    La sintaxis general que se sigue a la hora de definir una interfaz es:


<modificadores> interface <nombre>:<interfacesBase>
{
 <miembros>
}

    Los <modificadores> admitidos por las interfaces son los mismos que los de las clases Es decir, public, internal, private, protected, protected internal o new (e igualmente, los cuatro últimos sólo son aplicables a interfaces definidas dentro de otros tipos)

    El <nombre> de una interfaz puede ser cualquier identificador válido, aunque por convenio se suele usar I como primer carácter del mismo (IComparable, IA, etc)

    Los <miembros> de las interfaces pueden ser definiciones de métodos, propiedades,  indizadores o eventos, pero no campos, operadores, constructores o destructores. La sintaxis que se sigue para definir cada tipo de miembro es la misma que para definirlos como abstractos en una clase pero sin incluir abstract por suponerse implícitamente:

  • Métodos: <tipoRetorno> <nombreMétodo>(<parámetros>);

  • Propiedades: <tipo> <nombrePropiedad> {set; get;}

    Los bloques get y set pueden intercambiarse y puede no incluirse uno de ellos (propiedad de sólo lectura o de sólo escritura según el caso), pero no los dos.

  • Indizadores: <tipo> this[<índices>] {set; get;}

    Al igual que las propiedades, los bloques set y get pueden intercambiarse y obviarse uno de ellos al definirlos.

  • Eventos: event <delegado> <nombreEvento>;

    Nótese que a diferencia de las propiedades e indizadores, no es necesario indicar nada sobre sus bloques add y remove. Esto se debe a que siempre se han de implementar ambos, aunque si se usa la sintaxis básica el compilador les da una implementación por defecto automáticamente.

    Cualquier definición de un miembro de una interfaz puede incluir el modificador new para indicar que pretende ocultar otra heredada de alguna interfaz padre. Sin embargo, el resto de modificadores no son válidos ya que implícitamente siempre se considera que son public y abstract. Además, una interfaz tampoco puede incluir miembros de tipo, por lo que es incorrecto incluir el modificador static al definir sus miembros.

    Cada interfaz puede heredar de varias interfaces, que se indicarían en <interfacesBase> separadas por comas. Esta lista sólo puede incluir interfaces, pero no clases o estructuras; y a continuación se muestra un ejemplo de cómo definir una interfaz IC que hereda de otras dos interfaces IA y IB:


public delegate void D (int x);
interface IA
{
 int PropiedadA{get;}
 void Común(int x);
}
interface IB
{
 int this [int índice] {get; set;}
 void Común(int x);
}
interface IC: IA, IB
{
 event D EventoC;
}

    Nótese que aunque las interfaces padres de IC contienen un método común no hay problema alguno a la hora de definirlas. En el siguiente epígrafe veremos cómo se resuelven las ambigüedades que por esto pudiesen darse al implementar IC.

Implementación de interfaces

    Para definir una clase o estructura que implemente una o más interfaces basta incluir los nombres de las mismas como si de una clase base se tratase -separándolas con comas si son varias o si la clase definida hereda de otra clase- y asegurar que la clase cuente con definiciones para todos los miembros de las interfaces de las que hereda -lo que se puede conseguir definiéndolos en ella o heredándolos de su clase padre.

    Las definiciones que se den de miembros de interfaces han de ser siempre públicas y no pueden incluir override, pues como sus miembros son implícitamente abstract se sobreentiende. Sin embargo, sí pueden dársele los modificadores como virtual ó abstract y usar override en redefiniciones que se les den en clases hijas de la clase que implemente la interfaz.

    Cuando una clase deriva de más de una interfaz que incluye un mismo miembro, la implementación que se le dé servirá para todas las interfaces que cuenten con ese miembro. Sin embargo, también es posible dar una implementación diferente para cada una usando una implementación explícita, lo que consiste en implementar el miembro sin el modificador public y anteponiendo a su nombre el nombre de la interfaz a la que pertenece seguido de un punto (carácter .)

    Cuando un miembro se implementa explícitamente, no se le pueden dar modificadores como en las implementaciones implícitas, ni siquiera virtual o abstract. Una forma de simular los modificadores que se necesiten consiste en darles un cuerpo que lo que haga sea llamar a otra función que sí cuente con esos modificadores.

    El siguiente ejemplo muestra cómo definir una clase CL que implemente la interfaz IC:


class CL:IC
{
 public int PropiedadA    
 {
  get {return  5;}
  set {Console.WriteLine("Asignado{0} a PropiedadA", value);}
 }
 
 void IA.Común(int x)
 {
  Console.WriteLine("Ejecutado Común() de IA");
 }
 
 public int this[int índice]
 {
  get { return 1;}
  set { Console.WriteLine("Asignado {0} a indizador", value); }
 }
 
 void IB.Común(int x)
 {
  Console.WriteLine("Ejecutado Común() de IB");
 }
 
 public event D EventoC;
}
           
    Como se ve, para implementar la interfaz IC ha sido necesario implementar todos sus miembros, incluso los heredados de IA y IB, de la siguiente manera:

  • Al EventoC se le ha dado la implementación por defecto, aunque si se quisiese se podría haber dado una implementación específica a sus bloques add y remove.

  • Al método Común() se le ha dado una implementación para cada versión heredada de una de las clases padre de IC, usándose para ello la sintaxis de implementación explícita antes comentada. Nótese que no se ha incluido el modificador public en la implementación de estos miembros.

  • A la PropiedadA se le ha dado una implementación con un bloque set que no aparecía en la definición de PropiedadA en la interfaz IA. Esto es válido hacerlo siempre y cuando la propiedad no se haya implementado explícitamente, y lo mismo ocurre con los indizadores y en los casos en que en vez de set sea get el bloque extra implementado.

    Otra utilidad de las implementaciones explícitas es que son la única manera de conseguir poder dar implementación a métodos ocultados en las definiciones de interfaces. Por ejemplo, si tenemos:


interface IPadre
{
 int P{get;}
}
interface IHija:Padre
{
 new int P();
}

    La única forma de poder definir una clase donde se dé una implementación tanto para el método P() como para la propiedad P, es usando implementación explícita así:


class C: IHija
{
 void IPadre.P {}
 public int P() {…}
}

O así:


class C: IHija
{
 public void P() {}
 int IHija.P() {}
}

O así:


class C: IHija
{
 void IPadre.P() {}
 int IHija.P() {}
}

    Pero como no se puede implementar es sin ninguna implementación explícita, pues se  produciría un error al tener ambos miembros las misma signatura. Es decir, la siguiente definición no es correcta:


class C: IHija
{
 public int P() {} // ERROR: Ambos miembros tienen la misma signatura
 public void P(){}
}

    Es posible reimplementar en una clase hija las definiciones que su clase padre diese para los métodos que heredó de una interfaz. Para hacer eso basta hacer que la clase hija también herede de esa interfaz y dar en ella las definiciones explícitas de miembros de la  interfaz que se estimen convenientes, considerándose que las implementaciones para los demás serán las heredadas de su clase padre. Por ejemplo:


 using System;
 
 interface IA
 {
  void F();
 }
 
 class C1: IA
 {
  public void F()
  {
   Console.WriteLine("El F() de C1");
  }
 }
 
 class C2: C1, IA
 {// Sin implementación explícita no redefiniría, sino ocultaría
  void IA.F()   
{
   Console.WriteLine("El F() de C2");
  }
  
  public static void Main()
  {
   IA obj = new  C1();
   IA obj2 = new C2();
   obj.F();
   obj2.F();
  }  
 

    Reimplementar un  miembro de una interfaz de esta manera es parecido a redefinir los miembros reimplementados, sólo que ahora la redefinición sería solamente accesible a través de variables del tipo de la  interfaz. Así, la salida del ejemplo anterior sería:

 

 El F() de C1

 El F() de C2

 

    Hay que tener en cuenta que de esta manera sólo pueden hacerse reimplementaciones de miembros si la clase donde se reimplementa hereda directamente de la interfaz implementada explícitamente o de alguna interfaz derivada de ésta. Así, en el ejemplo anterior sería incorrecto haber hecho:

 
 class C2:C1 //La lista de herencias e interfaces implementadas por C2 sólo incluye a C1
 {
  void IA.f();    // ERROR: Aunque C1 herede de IA, IA no se incluye directamente
                  // en la lista de interfaces implementadas por C2
 }

     Es importante señalar que el nombre de interfaz especificado en una implementación explícita ha de ser exactamente el nombre de la interfaz donde se definió el miembro implementado, no el de alguna subclase de la misma. Por ejemplo:


interface I1
{
 void F()
}
interface I2:I1 
{}
class C1:I2
{
 public void I2.F();  //ERROR: habría que usar I1.F()
}

    En el ejemplo anterior, la línea comentada contiene un error debido a que F() se definió dentro de la interfaz I1, y aunque también pertenezca a I2 porque ésta lo hereda de I1, a la hora de implementarlo explícitamente hay que prefijar su nombre de I1, no de I2.

Acceso a miembros de una interfaz

    Se puede acceder a los miembros de una interfaz implementados en una clase de manera no explícita a través de variables de esa clase como si de miembros normales de la misma se tratase. Por ejemplo, este código mostraría un cinco por pantalla:


CL c = new CL();
Console.WriteLine(c.PropiedadA);

    Sin embargo, también es posible definir variables cuyo tipo sea una interfaz. Aunque no existen constructores de interfaces, estas variables pueden inicializarse gracias al polimorfismo asignándoles objetos de clases que implementen esa interfaz. Así, el siguiente código también mostraría un cinco por pantalla:


IA a = new CL();
Console.WriteLine(a.PropiedadA);

     Nótese que a través de una variable de un tipo interfaz sólo se puede acceder a miembros del objeto almacenado en ella que estén definidos en esa interfaz. Es decir, los únicos miembros válidos para el objeto a anterior serían PropiedadA y Común()

    En caso de que el miembro al que se pretenda acceder haya sido implementado  explícitamente, sólo puede accederse a él a través de variables del tipo interfaz al que pertenece y no a través de variables de tipos que hereden de ella, ya que la definición de estos miembros es privada al no llevar modificador de acceso. Por ejemplo:


CL cl = new CL();
IA a = cl;
IB b = cl;
// Console.WriteLine(cl.Común());  // Error: Común()
// fue implementado explícitamente
Console.WriteLine(a.Común());  
Console.WriteLine(b.Común());
Console.WriteLine(((IA) cl).Común());     
Console.WriteLine(((IB) cl).Común());     

    Cada vez que se llame a un método implementado explícitamente se llamará a la versión del mismo definida para la interfaz a través de la que se accede. Por ello, la salida del código anterior será:


 Ejecutado Común() de IA

 Ejecutado Común() de IB

 Ejecutado Común() de IA

 Ejecutado Común() de IB 

    Se puede dar tanto una implementación implícita como una explícita de cada miembro de una interfaz. La explícita se usará cuando se acceda a un objeto que implemente esa interfaz a través de una referencia a la interfaz, mientras que la implícita se usará cuando el acceso se haga a través de una referencia del tipo que implementa la interfaz. Por ejemplo, dado el siguiente código:


interface I
{object Clone();}
class Clase:I
{
 public object Clone()
 {
  Console.WriteLine("Implementación implícita");
 }
 public object IClonable.Clone()
 {
  Console.WriteLine("Implementación explícita");
 }
 
 public static void Main()
 {
  Clase obj = new Clase();
  ((I) obj).Clone();
  obj.Clone();      
 }
}

     El resultado que por pantalla se mostrará tras ejecutarlo es:

 Implementación explícita

 Implementación implícita

Acceso a miembros de interfaces y boxing

Es importante señalar que aunque las estructuras puedan implementar interfaces tal y como lo hacen las clases, el llamarlas a través de referencias a la interfaz supone una gran pérdida de rendimiento, ya que como las interfaces son tipos referencia ello implicaría la realización del ya visto proceso de boxing. Por ejemplo, en el código:


 using System;
 
 interface IIncrementable
 { void Incrementar();}
 
 struct Estructura:IIncrementable
 {
  public int Valor;
  
  public void Incrementar()
  {
   Valor++;
  }
  
  static void Main()
  {
   Estructura o = new Estructura();
   Console.WriteLine(o.Valor);
   ((IIncrementable) o).Incrementar();
   Console.WriteLine(o.Valor);
   o.Incrementar();
   Console.WriteLine(o.Valor);
  }
 }

    La salida obtenida será:


 0

 0

 1 

    Donde nótese que el resultado tras la primera llamada a Incrementar() sigue siendo el mismo ya que como se le ha hecho a través de un referencia a la interfaz, habrá sido aplicado sobre la copia de la estructura resultante del boxing y no sobre la estructura original. Sin embargo, la segunda llamada sí que se aplica a la estructura ya que se realiza directamente sobre el objeto original, y por tanto incrementa su campo Valor.

Interfaces
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:09/11/2006
Última actualizacion:09/11/2006
Visitas totales:54158
Valorar el contenido:
Últimas consultas realizadas en los foros
Últimas preguntas sin contestar en los foros de devjoker.com