Tema 15: 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 que sólo cuenten
con 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 tiene muchas diferencias con éstas:
-
Es posible definir tipos que deriven de más de una
interfaz. Esto se debe a que los problemas que se pueden
presentar a la hora de crear tipos que hereden de varios tipos
son debidos a que pueden haber conflictos difíciles de
resolver y si un tipo hereda más de una versión de
un mismo método procedentes de padres diferentes y con
códigos distintos. Sin embargo, con las interfaces las
interfaces se permite la herencia múltiple porque esto
nunca puede ocurrir debido a que las interfaces no incluyen
código.
-
Aunque las estructuras no pueden heredar clases, sí
pueden hacerlo de 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.
-
Las interfaces sólo pueden tener como miembros
métodos normales, eventos, propiedades e indizadores;
pero no pueden incluir definiciones de campos, operadores,
constructores o destructores.
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 último 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 impícitamente:
-
Métodos: <tipoRetorno>
<nombreMétodo>(<parámetros>
);
-
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 puede
dársele 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
{
void IA.F() // Sin implementación explícita no redefiniría, sino ocultaría
{
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 sólamente
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
{}
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