EXES
Aproximación a C#. Programación orientada a objetos
Bases generales: introducción a la tecnología .NET y bases sintácticas de C#
Espacios de nombres, clases e indicadores. Sistema de tipos de C# (CTS de .NET Framework)
Operadores de C#
Nuestra primera aplicación en C#: "Hola mundo"
Métodos (sobrecarga, argumentos y métodos static)
Constructores, destructores y el recolector de basura
Campos y propiedades
Control de flujo condicional
Control de flujo iterativo: bucles, salto y recursividad
Arrays
Indizadores, sobrecarga de operadores y conversiones definidas
Estructuras; Herencia e Interfaces
Pistas y resolución de ejercicios
Créditos
La Compañía
 
Área de Programación y Desarrollo
  Curso de Iniciación a C#
www.exes.es
Tel: 902 360 417
   Principal
 Exes   Contactar 
Estructuras, Las clases en profundidad, Herencia, Interfaces, Ejercicio 6, Ejercicio 7 | Curso C#: Decimotercera entrega

Por fin nos vamos acercando a las cuestiones más interesantes de los lenguajes .NET, que son, indudablemente, las enormes ventajas y la gran flexibilidad que nos ofrece el hecho de que dichos lenguajes sean orientados a objetos. Yo sé que esto al principio parece una afirmación un tanto abstracta, pero no me cabe la menor duda de que aprenderás a apreciarlo en su verdadera dimensión a medida que vayas dominando todas las técnicas que empezaré a exponer a partir de ahora.

Ciertamente, todavía nos queda por ver algunas cosas no menos interesantes que no están tan estrechamente relacionadas con la herencia, pero creo que, después de doce entregas, podemos ya adentrarnos en la programación orientada a objetos con C# propiamente dicha sin temor a que os resulte tan difícil o frustrante que os haga abandonar. Ya habrá tiempo para reflexión, atributos, delegados, manejadores de eventos, ejecución multi-hilo, punteros y demás parabienes de la programación en C#.

Dado que estas técnicas nos van a obligar a redefinir o ampliar (ligeramente, eso sí) algunos de los conceptos que habíamos asentado hasta ahora, ya os aviso de que esta parte va a ocuparnos varias entregas, aunque ahora no puedo precisaros cuántas exactamente.

ESTRUCTURAS

¿Más retrasos? Hombre, pues... sí... lamentablemente, sí. Antes de empezar a heredar y diseñar interfaces y clases abstractas y cosas de esas, tengo que hablar de las estructuras, porque son también una parte muy importante del lenguaje C# (y VB.NET, y C++ gestionado...).

Hasta ahora nos hemos hartado de escribir clases y clases, con sus campos, sus propiedades, sus métodos, sus constructores y destructores, y hemos visto también cómo se podían instanciar objetos de estas clases. En lo que no nos hemos fijado aún en profundidad, sin embargo, es en qué es lo que esto implica dentro de la filosofía de .NET.

En la entrega 3 diferenciábamos entre tipos valor y tipos referencia, diciendo que los primeros representaban un valor y se almacenaban en la pila, y los segundos representaban una referencia, esto es, devolvían internamente un puntero al comienzo del objeto que, recordemos, se alojaba en el montón. Pues bien, las clases representan tipos referencia, es decir, cuando instanciamos un objeto de una determinada clase, lo que hacemos es crear un objeto en el montón, de modo que la variable devuelve internamente un puntero al comienzo de dicho objeto. Entonces, ¿cómo podemos crear un tipo valor? Pues con las estructuras. ¿Y no podemos almacenar un valor en una clase? Pues sí, poder... sí que podemos. De hecho, eso era lo que os pedía en el ejericio 5 (en la entrega anterior), ni más ni menos.

Sin embargo, usar clases para almacenar algo que realmente son valores nos va a restar eficiencia y también funcionarán de un modo distinto (y más engorroso). Restarán eficiencia puesto que, necesariamente, tendrá que ser creado como un objeto en el montón, cuyo acceso es más lento que la pila debido a que a ésta se accede directamente, mientras que para acceder al montón hay que resolver previamente la referencia. Además, dicho objeto también tendrá que ser tenido en cuenta por el recolector de basura. Por otra parte, operaciones muy sencillas en principio, como podría ser una simple asignación de una variable a otra, se complicarán, dado que dicha asignación no haría otra cosa que crear una doble referencia, y no una copia del objeto, lo cual, seguramente, nos obligaría a implementar la interface ICloneable, y hacer asignaciones de lo más antinaturales. Ah, y no os preocupéis por eso de implementar interfaces, que lo veremos dentro de muy poco. Por ejemplo, supongamos que hemos diseñado una clase Moneda. Pues bien, sin entrar en los detalles de su implementación, veamos cómo habría que hacer una asignación para crear una copia de un objeto de esta clase:

Moneda m

m=new Moneda(10);

Moneda m2=m.Clone();

Como podéis ver, resulta bastante raro, no por el hecho de invocar el método Clone (pobrecito, que no tiene culpa de nada...), sino por tener que invocarlo para crear una simple copia de su valor en otra variable, puesto que la clase Moneda puede ser considerada, perfectamente, como un valor. Ahora observa el siguiente código y dime cuál te gusta más:

Moneda m

m=10;

Moneda m2=m;

No sé si coincidirás conmigo, pero a mí me gusta bastante más este último. ¿Cómo lo hemos conseguido? Pues, obviamente, utilizando una estructura en lugar de una clase. Vamos a echarle un vistazo a la estructura Moneda:

public struct Moneda

{

    private double valor;

 

    public static implicit operator Moneda(double d)

    {

        Moneda m;

        m.valor=Math.Round(d,2);

 

        return m;

    }

 

    public static implicit operator double(Moneda m)

    {

        return m.valor;

    }

 

    public static Moneda operator++(Moneda m)

    {

        m.valor++;

        return m;

    }

 

    public static Moneda operator--(Moneda m)

    {

        m.valor--;

        return m;

    }

 

    public override string ToString()

    {

        return this.valor.ToString();

    }

}

Como podéis ver se parece enormemente a una clase, salvo que aquí hemos utilizado la palabra struct en lugar de class. La mayoría de los conceptos que hemos visto hasta el momento con las clases nos sirven también para las estructuras. Pero, ojo, digo la mayoría, puesto que hay cosas habituales en las clases que no se pueden hacer con las estructuras:

  • Se puede escribir constructores, pero estos han de tener argumentos. Dicho de otro modo, no se puede escribir un constructor sin argumentos para una estructura.

  • Lógicamente, al tratarse de un tipo valor que se almacena en la pila, las estructuras no admiten destructores.

  • No soportan herencia, lo cual implica que no pueden ser utilizadas como clases base ni como clases derivadas. No obstante, sí pueden implementar interfaces, y lo hacen igual que las clases (tranquilos, que veremos esto muy pronto).

  • Los campos de una estructura no pueden ser inicializados en la declaración. O sea, por ejemplo, no vale decir int a=10; porque tendríamos un error de compilación.

  • Las estructuras, si no se especifican los modificadores ref o out, se pasan por valor, mientras que las clases se pasan siempre por referencia.

  • No es necesario instanciar una estructura para poder usar sus miembros. Fíjate en el ejemplo, y verás que, en el segundo fragmento de código no hemos instanciado el objeto m.

LAS CLASES EN PROFUNDIDAD

En la introducción dimos ya una definición de lo que era una clase, y también me preocupé por estableceros claramente la diferencia entre clase y objeto. Hoy vamos a profundizar un poquito más en este concepto, pues es algo que nos ayudará muchísimo en el futuro a la hora de diseñar acertadamente una jerarquía de clases con alguna relación de herencia.

Vamos a refrescar, primeramente, el concepto de clase: decíamos que una clase era la plantilla a partir de la cual podíamos crear objetos. Todos los objetos de la misma clase comparten la interface (es decir, métodos, campos y propiedades), pero los datos que contiene cada objeto en sus campos y propiedades pueden diferir. Algunos autores dan otras definiciones, más o menos aproximadas. Por ejemplo, se puede decir también que una clase es algo a lo que se le puede poner un nombre (un coche, un avión, un boli, una mesa, una barbacoa... con su choricito..., y su pancetita..., y sus chuletillas... y... eeeeeh, bueno... etc.).

Por su misión específica, podemos dividir las clases (casi sería mejor decir los tipos, porque así hablamos tanto de clases como de estructuras) en tres grandes grupos:

  • Tipos que ofrecen un valor: son aquellos en los que lo único que nos importa es el valor que contienen. Pongamos, por ejemplo, que queremos pagar un artículo que ha costado 20 €. En realidad, nos dará igual usar un billete de 20 u otro, también de 20. Lo importante es pagar los 20 €, pero no el billete de 20 que utilicemos para ello. Por lo tanto, los objetos cuyo único interés es su valor son perfectamente intercambiables, y suelen estar implementados como estructuras (Int32, Int16, Double...).

  • Tipos que ofrecen un servicio: se puede incluir aquí los tipos cuyo único interés es el servicio que ofrecen. Por ejemplo, cuando vamos a comprar el pan, lo más importante es que nos lo vendan, pero quién nos lo venda nos trae sin cuidado (salvo que sea algún guarreras con las manos sucias, pero bueno, ese es otro cantar...). Por lo tanto, nos interesa el servicio, pero no quién haga ese servicio. Un ejemplo claro de tipo que ofrece un servicio es la clase Console, pues no se usan nunca instancias de ella (de hecho, no se puede instanciar), sino que, simplemente, usamos sus métodos static para aprovechar los servicios que nos ofrece.

  • Tipos de identidad: en este caso, lo más relevante no es la información que contienen ni el servicio que prestan, sino cuál es el objeto que nos ofrece sus datos y servicios. Por ejemplo, en una factura lo más importante es la factura en sí, es decir, no es lo mismo la factura del gas del mes pasado que la de este mes, o la del teléfono de hace dos años. Por este motivo decimos que son tipos de identidad, porque lo más relevante es el objeto en sí.

Ciertamente puede haber tipos que, según los miembros que se examinen, podrían incluirse simultáneamente en dos grupos, o, incluso, en los tres. Por ejemplo, un tipo TarjetaDeCrédito sería, claramente, un tipo de identidad. Sin embargo, también podría ofrecer algún servicio que no dependiera de la identidad del objeto, como calcular la tasa anual que cobra una determinada entidad por ella, dado que esta información será común para todas las tarjetas emitidas por dicha entidad.

HERENCIA

También explicamos el concepto de herencia en la introducción, aunque hoy profundizaremos un poquito más. Decíamos, pues, que gracias a la herencia podíamos definir clases nuevas basadas en clases antiguas, añadiéndoles más datos o funcionalidad. No quiere decir esto que podamos utilizar la herencia de cualquier manera, sin orden ni concierto (bueno, lo que es poder... podemos, pero no debemos). Generalmente, la relación de herencia debe basarse en una relación jerárquica de conjuntos y subconjuntos más pequeños incluidos en aquellos. Así, podemos decir que un dispositivo de reproducción de vídeo sirve para reproducir vídeo. Sin embargo, un reproductor de VHS no es igual que un reproductor de DVD, a pesar de que ambos son subconjuntos de un conjunto mayor, es decir, los dispositivos de reproducción de video. En otras palabras, tanto los reproductores VHS como los reproductores de DVD son también, todos ellos, dispositivos de reproducción de vídeo. Por lo tanto, podemos establecer una clase base que determine e implemente cuáles serán los miembros  y el comportamiento o una parte del comportamiento común de todos los dispositivos de reproducción de vídeo (reproducir, parar, pausa, expulsar...), y esta clase servirá de base a las clases de reproductor VHS y reproductor de DVD, que derivarán sus miembros de la clase base. Sin embargo, alguna de las clases derivadas puede añadir algo específico de ella que no tuviera sentido en otro subconjunto distinto. Por ejemplo, la clase del reproductor de VHS necesitará incluir también un método para rebobinar la cinta, cosa que no tendría sentido con un reproductor de DVD.

Vamos a implementar estas tres clases para que así os hagáis una idea más clara de lo que estamos explicando. Comenzaremos con la clase DispositivoVideo:

public enum EstadoReproductor

{

    Parado,

    EnPausa,

    Reproduciendo,

    SinMedio

}

 

class DispositivoVideo

{

    protected bool reproduciendo=false;

    protected bool pausado=false;

    protected bool medio=false;

 

    public virtual void Reproducir()

    {

        if (!medio)

            Console.WriteLine("Inserte un medio");

        else if (reproduciendo)

            Console.WriteLine("Ya estaba reproduciendo");

        else

        {

            Console.WriteLine("Reproduciendo vídeo");

            reproduciendo=true;

        }

    }

 

    public virtual void Detener()

    {

        if (!medio)

            Console.WriteLine("Inserte un medio");

        else if (reproduciendo)

        {

            Console.WriteLine("Reproducción detenida");

            reproduciendo=false;

            pausado=false;

        }

        else

            Console.WriteLine("Ya estaba parado, leñe");

    }

 

    public virtual void Pausa()

    {

        if (!medio)

            Console.WriteLine("Inserte un medio");

        else if (reproduciendo && !pausado)

        {

            Console.WriteLine("Reproducción en pausa");

            pausado=true;

        }

        else if(reproduciendo && pausado)

        {

            Console.WriteLine("Reproducción reanudada");

            pausado=false;

        }

        else

            Console.WriteLine("No se puede pausar. Está parado");

    }

 

    public virtual void IntroducirMedio()

    {

        if (medio)

            Console.WriteLine("Antes debe expulsar el medio actual");

        else

        {

            Console.WriteLine("Medio introducido");

            medio=true;

        }

    }

    public virtual void Expulsar()

    {

        if (!medio)

            Console.WriteLine("Inserte un medio");

        else

        {

            medio=false;

            reproduciendo=false;

            pausado=false;

 

            Console.WriteLine("Expulsando medio");

        }

    }

 

    public EstadoReproductor Estado

    {

        get

        {

            if (!medio)

                return EstadoReproductor.SinMedio;

            else if (pausado)

                return EstadoReproductor.EnPausa;

            else if (reproduciendo)

                return EstadoReproductor.Reproduciendo;

            else

                return EstadoReproductor.Parado;

        }

    }

}

Podríamos decir que todos los dispositivos de vídeo incorporan este comportamiento que hemos implementado ¿cómo, que todavía no hemos visto eso de enum? Pues vaya despiste... Si es que... Bueno, os lo explico brevemente que es muy fácil (más adelante lo veremos con más detalle). La instrucción enum sirve para agrupar constantes. En este caso, hemos agrupado cuatro constantes (Parado, EnPausa, Reproduciendo y SinMedio) en un grupo llamado EstadoReproductor, de forma que podemos utilizar un código mucho más fácil de leer que usando números directamente. La sintaxis es la que veis, ni más ni menos.

Bueno, a lo que íbamos. Decíamos que esta clase incorporaba todo aquello que, como mínimo, necesitaba un dispositivo de reproducción de vídeo. Por lo tanto, podríamos usar esta clase como base para construir otras clases de reproductores de vídeo más específicas, como un reproductor de VHS o un reproductor de DVD. Hemos de fijarnos en algo importante: hay tres campos con el modificador de acceso protected, el cual casi no habíamos usado hasta este momento. Aunque ya explicamos los modificadores de acceso en la entrega 3, os lo recuerdo: protected hace que el miembro en cuestión sea visible en las clases derivadas, pero no en el cliente. Por lo tanto, estos tres campos los tendremos disponibles en cualquier clase que derivemos de la clase DispositivoVideo. Por otra parte, hemos añadido la palabra virtual en la declaración de cada método. ¿Para qué? Bien, si hacemos esto, permitimos que las clases derivadas puedan modificar la implementación de estos métodos. Si no lo hacen, obviamente, se ejecutará el código de la clase base. Lo veremos mejor si escribimos ahora las clases derivadas. Comenzaremos con la clase DispositivoVHS:

class DispositivoVHS:DispositivoVideo

{

    public void Rebobinar()

    {

        if (!medio)

            Console.WriteLine("Introduzca una cinta");

        else

            Console.WriteLine("Cinta rebobinada");

    }

 

    public override void IntroducirMedio()

    {

        if (medio)

            Console.WriteLine("Antes debe expulsar la cinta actual");

        else

        {

            Console.WriteLine("Cinta introducida");

            medio=true;

        }

    }

}

Como veis, esta clase es mucho más corta, porque únicamente tiene que añadir o modificar aquello de la clase base que no le sirve. El modo de indicar que queremos heredar los miembros de una clase es añadiendo dos puntos al final del nombre de la clase y poner a continuación el nombre de la clase base. En nuestra clase DispositivoVHS hemos añadido un método llamado Rebobinar que no estaba en la clase base y hemos sobreescrito el método IntroducirMedio que sí se encontraba en la clase base. ¿Cómo que es igual? Anda, fíjate en las cadenas que escribe en la consola, y luego me dices si es igual o no lo es... Para poder sobreescribir un método virtual de la clase base hay que utilizar la palabra override, como veis aquí. El resto de miembros de la clase base nos sirve, de modo que no tenemos que tocarlos.

Vamos ahora con la clase DispositivoDVD:

class DispositivoDVD:DispositivoVideo

{

    public void IrA(int escena)

    {

        if (!medio)

            Console.WriteLine("Inserte un medio");

        else

        {

            reproduciendo=true;

            pausado=true;

        }

    }

}

En este caso, no hemos tocado ninguno de los métodos de la clase base, y hemos añadido el método IrA, para saltar a una escena determinada. Como podéis ver, la herencia nos ayuda mucho porque no tenemos que repetir el mismo código una y otra vez. Ahora escribiremos un programín que use estas clases, a ver qué tal:

class VideosApp

{

    static void Main()

    {

        Console.WriteLine("Vamos a crear un dispositivo genérico");

        DispositivoVideo video=new DispositivoVideo();

        

        Console.WriteLine();

        Acciones(video);

 

        Console.WriteLine();

        Console.WriteLine("Ahora crearemos un reproductor VHS");

        video=new DispositivoVHS();

        Acciones(video);

 

        Console.WriteLine();

        Console.WriteLine("Para terminar crearemos un reproductor DVD");

        video=new DispositivoDVD();

        Acciones(video);

 

        Console.ReadLine();

    }

 

    static void Acciones(DispositivoVideo video)

    {

        Console.WriteLine();

        Console.WriteLine("Pulsa intro para invocar IntroducirMedio");

        Console.ReadLine();

        video.IntroducirMedio();

        Estado(video);

 

        Console.WriteLine();

        Console.WriteLine("Pulsa intro para invocar Reproducir");

        Console.ReadLine();

        video.Reproducir();

        Estado(video);

 

        Console.WriteLine();

        Console.WriteLine("Pulsa intro para invocar Pausa");

        Console.ReadLine();

        video.Pausa();

        Estado(video);

 

        Console.WriteLine();

        Console.WriteLine("Pulsa intro para invocar Reproducir");

        Console.ReadLine();

        video.Reproducir();

        Estado(video);

 

        Console.WriteLine();

        Console.WriteLine("Pulsa intro para invocar Pausa");

        Console.ReadLine();

        video.Pausa();

        Estado(video);

 

        Console.WriteLine();

        Console.WriteLine("Pulsa intro para invocar Detener");

        Console.ReadLine();

        video.Detener();

        Estado(video);

 

        Console.WriteLine();

        Console.WriteLine("Pulsa intro para invocar Pausa");

        Console.ReadLine();

        video.Pausa();

        Estado(video);

 

        Console.WriteLine();

        Console.WriteLine("Pulsa intro para invocar Expulsar");

        Console.ReadLine();

        video.Expulsar();

        Estado(video);

 

        if (video is DispositivoVHS)

        {

            DispositivoVHS videoVHS=(DispositivoVHS) video;

 

            Console.WriteLine();

            Console.WriteLine("Pulsa intro para invocar Rebobinar");

            Console.ReadLine();

            videoVHS.Rebobinar();

            Estado(video);

        }

 

        if (video is DispositivoDVD)

        {

            DispositivoDVD videoDVD=(DispositivoDVD) video;

 

            Console.WriteLine();

            Console.WriteLine("Pulsa intro para invocar IrA(5)");

            Console.ReadLine();

            videoDVD.IrA(5);

            Estado(video);

        }

 

        Console.WriteLine();

    }

 

    static void Estado(DispositivoVideo video)

    {

        Console.WriteLine("Estado actual del reproductor: {0}",

            video.Estado);

    }

}

Hay algunas cosillas interesantes que no quiero dejar de comentar. En el método main he usado el objeto video, declarado de la clase DispositivoVideo para instanciar los dispositivos de los tres tipos distintos. Y no contento con esto, encima se los paso a los métodos Acciones y Estado con total impunidad, cuando estos solamente aceptan como argumento un objeto de la clase DispositivoVideo. ¿Cómo diablos no coge el compilador y me manda de nuevo al cole, a ver si aprendo algo? La razón de que todo esto funcione perfectamente es la que os di al principio: tanto los dispositivos VHS como los dispositivos DVD son también dispositivos de vídeo ¿no? Pues al haber derivado estas dos clases de la clase DispositivoVideo, el compilador entiende esto mismo, de modo que los admite sin ningún tipo de problemas. Por otro lado, tengo dos if marcados en negrilla que quizá resulten bastante enigmáticos. ¿No decías que el compilador tragaba porque, al fin y al cabo, todos eran dispositivos de vídeo? Pues sí, en efecto, pero date cuenta de un hecho importante: los métodos Rebobinar e IrA no están implementados en la clase DispositivoVideo, sino que son particulares de las otras dos clases. Por este motivo necesitaremos la conversión al tipo específico que implementa el método para poder invocarlo.

Los más avispados (si habéis ejecutado el ejemplo, obviamente) os habréis dado cuenta también de que ha ocurrido algo raro al invocar el método IntroducirMedio la segunda vez, es decir, cuando el objeto era un reproductor de VHS. En efecto, se ejecutó el método sobreescrito en la clase DispositivoVHS, en lugar de ejecutarse el método virtual definido en la clase base. ¿Cómo pudo el compilador saber que era un dispositvo de VHS, si el objeto que se estaba utilizando era, según está declarado el argumento, un dispositivo genérico? Pues esta es la magia del polimorfismo. Podemos reemplazar métodos de la clase base con toda tranquilidad, porque siempre se ejecutarán correctamente. Si os acordáis, el polimorfismo era la capacidad que tenían los objetos de comportarse de un modo distinto unos de otros aun compartiendo los mismos miembros. Pues aquí lo tenéis.

INTERFACES

Ahora bien, no tendría sentido establecer una relación de herencia entre conjuntos completamente distintos, por más que muchos de sus miembros fueran a ser comunes. Por ejemplo, no estaría bien heredar una clase tarjeta de crédito y otra clase cuenta corriente de una clase banco. Por más que en todos ellos puedan hacerse ingresos o reintegros, está claro que ni las tarjetas de crédito ni las cuentas corrientes son bancos. En resumen, no debemos basarnos únicamente en la funcionalidad para establecer una relación de herencia entre clases.

Es en este último caso donde juegan un papel importante las interfaces. Podemos definir las interfaces como la definición de un conjunto de miembros que serán comunes entre clases que serán (o no) completamente distintas. La interface conoce cómo será la funcionalidad de cualquier clase que la implemente pero, lógicamente, no podrá conocer los detalles de esa implementación en cada una de esas clases, de modo que una interface no puede implementar nada de código, sino que, únicamente, puede describir un conjunto de miembros. Anteriormente hablamos de la interface ICloneable para poder hacer copias de objetos de tipo referencia mediante el método Clone. Está claro que serán muchas las clases que puedan hacer copias de sí mismas, pero también está claro que, aparte de este detalle, dichas clases no tienen por qué tener nada más en común. Yo sé que te estarás preguntando que, dado que una interface no implementa nada de código, puesto que este tiene que ser escrito en cada una de las clases que implemente dicha interface, ¿Para qué diablos queremos la interface? ¿No sería más fácil escribir el método Clone en cada clase y olvidarnos de la interface? Pues esto se responde muy bien con un ejemplo: las clases Factura y ArrayList podrán hacer copias de sí mismas, pero está muy claro que una factura no tiene absolutamente nada más que ver con un ArrayList. Sin embargo, si quisiéramos escribir un método que se ocupara de hacer la copia de uno de estos objetos, cualquiera que sea, tendremos el problema de que habría que escribir una sobrecarga para cada uno de los tipos que queremos poder copiar. Es decir, si dicho método se llama Copiar, en este caso habría que escribir dos sobrecargas de él: Copiar (Factura f) y Copiar (ArrayList a). Ciertamente, también podríamos escribir un sólo método usando como argumento un tipo object, es decir, Copiar (object o), pero dentro de él tendríamos que determinar cuál es, exactamente, el tipo del objeto ( if (o is Factura)...else if (o is ArrayList) ) y hacer la conversión en cada caso (Factura f = (Factura) o; o bien ArrayList a = (ArrayList) o ), para poder invocar el método Clone ( return f.Clone(); o bien return a.Clone() ), dado que el tipo object no contiene ninguna definición para dicho método. Sin embargo, si implementamos la interface ICloneable en ambas clases nos ahorraremos el trabajo, puesto que podremos escribir un único método con un argumento que especifique cualquier objeto que implemente dicha interface, es decir, Copiar(ICloneable ic), puesto que el objeto ic sí tiene un método Clone que se puede invocar directamente, independientemente que sea una Factura o un ArrayList ( return ic.Clone() ). En resumen, las interfaces sirven para poder agrupar funcionalidades.

Antes de poner algún ejemplo con las interfaces, debo recordaros un par de cuestiones que ya os mencioné en la introducción: los lenguajes .NET soportan únicamente herencia simple, es decir, una clase se puede derivar de otra clase, pero no de varias; sin embargo, sí podemos implementar en una misma clase tantas interfaces como nos apetezca.

Veamos un ejemplo de uso de las interfaces. Vamos a pensar en que tenemos varias clases distintas que deben ofrecer la capacidad de presentar sus datos en la consola. Ciertamente, no tendría mucho sentido utilizar una clase base para todas ellas, porque ya hemos dicho que cada una será distinta de la otra. Por lo tanto, diseñaremos una interface a la que llamaremos, por ejemplo, IPresentable (que no es igual que im-presentable, ¿eh? ojo a la di-ferencia...). Dicha interface especificará simplemente la existencia de un método, al cual llamaremos Presentar:

interface IPresentable

{

    void Presentar();

}

Como os decía, fijaos bien en que la Interface no implementa el código del método, sino que simplemente se limita a indicar que debe existir un método Presentar en todas las clases que la implementen. Ah, otra cosa: os habréis fijado en que las interfaces siempre empiezan por la letra I. No es que lo exija el compilador, sino que es una convención de codificación, es decir, todo el mundo comienza nombrando sus interfaces con la letra I, de modo que te aconsejo que tú también lo hagas. A continuación vamos a escribir dos clases completamente distintas que implementarán esta interface:

class Triangulo:IPresentable

{

    public double Base;

    protected double Altura;

 

    public Triangulo(double Base, double altura)

    {

        this.Base=Base;

        this.Altura=altura;

    }

 

    public double Area

    {

        get { return Base*Altura/2; }

    }

 

    public void Presentar()

    {

        Console.WriteLine("Base del triángulo: {0}", Base);

        Console.WriteLine("Altura del triángulo: {0}", Altura);

        Console.WriteLine("Área del triángulo: {0}", Area);

    }

}

 

class Proveedor:IPresentable

{

    public string Nombre;

    public string Apellidos;

    public string Direccion;

 

    public Proveedor(string nombre, string apellidos, string direccion)

    {

        Nombre=nombre;

        Apellidos=apellidos;

        Direccion=direccion;

    }

 

    public void Presentar()

    {

        Console.WriteLine("Nombre: {0}", Nombre);

        Console.WriteLine("Apellidos: {0}", Apellidos);

        Console.WriteLine("Dirección: {0}", Direccion);

    }

}

Puedes apreciar claramente que un proveedor no tiene absolutamente nada que ver con un triángulo (bueno... algunos proveedores pueden ser bastante obtusos... pero esa es otra cuestión...). Seguramente ahora verás más claramente el motivo de que la interface no implemente el método: evidentemente, no lo hace porque no tiene forma de saber los detalles de implementación de cada clase. Un programín que utilice todo esto te ayudará a apreciar la ventaja de haber usado una interface:

class EjemploInterfacesApp

{

    static void Main()

    {

        Triangulo t=new Triangulo(10,5);

        Proveedor p=new Proveedor("Erik","Erik otra vez", "su casa");

 

        Console.WriteLine("Ya se han creado los ojbetos");

        Console.WriteLine("Pulsa INTRO para invocar VerDatos(triangulo)");

        Console.ReadLine();

        VerDatos(t);

 

        Console.WriteLine();

        Console.WriteLine("Pulsa INTRO para invocar VerDatos(proveedor)");

        Console.ReadLine();

        VerDatos(p);

    }

 

    static void VerDatos(IPresentable IP)

    {

        IP.Presentar();

    }

}

Como ves, al método VerDatos le podemos pasar cualquier objeto que implemente la interface IPresentable, independientemente de cuál sea su clase, lo cual resulta verdaderamente cómodo.

Creo que ya es suficiente para esta entrega. En la próxima seguiremos profundizando en estas cosillas tan lindas. De momento, y para que estés entretenido, te propondré un par de ejercicios:

EJERCICIO 6

Para este ejercicio te pediré que diseñes una clase Factura y una clase Presupuesto. La factura tendrá número, fecha, datos del cliente, líneas de detalles, porcentaje de iva, base imponible, cuota y total. El presupuesto será parecido, aunque en este deberá incluir también fecha de caducidad, pero no habrá iva, ni base imponible, ni cuota. Como ambos documentos van a ser muy parecidos, te recomiendo que consideres la posibilidad de utilizar la herencia.

EJERCICIO 7

Ahora te voy a complicar un poco la vida (quizá un poco mucho). A ver si eres capaz de diseñar un tipo Moneda, es decir, un tipo numérico de dos decimales, convertible implícitamente al tipo Decimal. Ojo, tiene que ser un tipo valor, no un tipo referencia. Dirás que prácticamente lo tienes hecho en la entrega... qué más quisieras... este tipo debe implementar las interfaces IComparable, IConvertible e IFormatable, todas ellas dentro del espacio de nombres System de la biblioteca de clases de NET Framework. ¿Que te faltan datos? Ya, supongo que desearías saber cuáles son los miembros de cada una de las interfaces y para qué sirven. Sin embargo, esta vez no te lo daré tan mascado. El secreto de un programador no es sabérselo todo de memoria, sino saber buscar lo que necesita. Como ya llevamos trece entregas con esta, creo que lo más productivo será que empecéis a buscar y solucionar los problemas por vuestra cuenta, porque os servirá muy bien como entrenamiento.

De momento, nada más.

 

··> Ver todos los cursos
Solicita Información 
Rellene sus datos y amplíe información sobre nuestros Programas Formativos
Nombre *
Apellidos *
Provincia
País
E-mail *
Tel. Móvil *
Tel. Fijo
Curso:
¿Por qué desea cursar este Programa?
Por favor, para validar su información introduzca el siguiente código (*):
 
 
 
 
Sello de Calidad   Sello de Calidad
  EXES - C/ Albasanz, 14 Bis, 1-C. 28037 Madrid - Tel: 902 360 417 Fax: 902 931 305 - exes@exes.es