Curso de iniciación a la programación con C# |
Larga ha sido mi ausencia de estas páginas desde la publicación de la última entrega. Os pido perdón por tanto retraso. Desde este momento hago propósito de enmienda para las entregas sucesivas, a ver si, al menos, me es posible tener lista una entrega al mes.
Cada vez estamos más cerca de las opciones más avanzadas de este lenguaje. En esta entrega comenzaremos hablando de un concepto más o menos nuevo: los indizadores. Ciertamente, el palabro es un tanto extraño, pero os aseguro que esta vez no he sido yo el que le ha puesto ese nombre...
Para terminar esta entrega entraremos de lleno en la sobrecarga de operadores y las conversiones definidas, lo que dejará libre el camino para que en las próximas entregas podamos empezar ya a hablar en profundidad de los mecanismos de la herencia, interfaces y demás florituras.
INDIZADORES
Decía que un indizador es un concepto más o menos nuevo porque no es nuevo en realidad (al menos, a mí no me lo parece). Más bien se trata de una simplificación en lo que se refiere a la lógica de funcionamiento de un objeto que es en realidad un array o una colección.
Antes de ver su sintaxis considero que es necesario comprender bien el concepto y el objetivo de un indizador. Para ello vamos a poner un ejemplo: podemos considerar que un libro no es más que un objeto que contiene una serie de capítulos. Si nos olvidamos por un momento de los indizadores deberíamos construir el objeto Libro con una colección Capítulos dentro de él en la que pudiéramos añadir o modificar capítulos, de modo que deberíamos ofrecer, por ejemplo, un método Add para lo primero y un método Modify para lo segundo. De este modo, habríamos de llamar a alguno de estos métodos desde el código cliente para efectuar dichas operaciones, es decir, algo como esto:
static void Main()
{
Libro miLibro=new Libro();
miLibro.Capitulos.Add("La psicología de la musaraña");
miLibro.Capitulos.Add("¿Puede una hormiga montar en bicicleta?");
miLibro.Capitulos.Modify("Un pedrusco en manos de Miró es un Miró",1);
...
}
Los indizadores, sin embargo, ofrecen la posibilidad de tratar al objeto Libro como si fuera un array o una colección en sí mismo, haciendo la codificación más intuitiva a la hora de usarlo. Si hubiéramos escrito la clase Libro como un indizador, el código equivalente al anterior podría ser algo como esto:
static void Main()
{
Libro miLibro=new Libro();
miLibro[0]="La psicología de la musaraña";
miLibro[1]="¿Puede una hormiga montar en bicicleta?";
miLibro[1]="Un pedrusco en manos de Miró es un Miró";
...
}
Sin duda, este código resulta mucho más natural: ya que el objeto Libro no es más que un conjunto de capítulos, lo suyo es tratarlo como si fuera un array, independientemente de que dicho objeto pueda ofrecer también otra serie de propiedades, métodos y demás.
Bien, ahora que ya sabemos para qué sirve un indizador podemos ver su sintaxis. Ya veréis que no es nada del otro mundo:
class Libro
{
public object this[int index]
{
get
{
...
}
set
{
...
}
}
...
}
Como podéis apreciar, el indizador se construye de un modo muy similar a las propiedades, con la salvedad de que el nombre de esta "propiedad" es el propio objeto, es decir, this, el tipo, lógicamente, es object y, además, requiere un argumento entre corchetes, que sería el índice del elemento al que queremos acceder. Veamos ahora el ejemplo del libro completo:
using System;
using System.Collections;
namespace IndizadorLibros
{
class Libro
{
private ArrayList capitulos=new ArrayList();
public object this[int indice]
{
get
{
if (indice >= capitulos.Count || indice < 0)
return null;
else
return capitulos[indice];
}
set
{
if (indice >= 0 && indice < capitulos.Count)
capitulos[indice]=value;
else if (indice == capitulos.Count)
capitulos.Add(value);
else
throw new Exception("No se puede asignar a este elemento");
}
}
public int NumCapitulos
{
get
{
return capitulos.Count;
}
}
}
class IndizadorLibrosApp
{
static void Main()
{
Libro miLibro=new Libro();
miLibro[0]="La psicología de la musaraña";
miLibro[1]="¿Puede una hormiga montar en bicicleta?";
miLibro[1]="Un pedrusco en manos de Miró es un Miró";
for (int i=0;i<miLibro.NumCapitulos;i++)
Console.WriteLine("Capitulo {0}: {1}",i+1,miLibro[i]);
string a=Console.ReadLine();
}
}
}
Hay un detalle realmente importante que no os había contado hasta ahora. La propiedad NumCapitulos (os la he marcado en amarillo) devuelve el número de capítulos que hay incluidos en el objeto Libro hasta este momento. Puede que alguno se esté preguntando por qué diantres he puesto esta propiedad, ya que un array tiene ya la propiedad Length que devuelve exactamente lo mismo, y una colección tiene ya la propiedad Count, que hace también lo mismo. La razón es muy simple: Libro es una clase, no una colección ni un array. El hecho de que hayamos implementado un indizador hará que se pueda acceder a los elementos de los objetos de esta clase como si fuera un array, pero no que el compilador genere una propiedad Count o Length por su cuenta y riesgo. Por este motivo, si queremos una propiedad que ofrezca el número de elementos contenidos en el objeto tendremos que implementarla nosotros. En este caso, yo la he llamado NumCapitulos.
Por otro lado, veis que he declarado el campo privado "capitulos" del tipo System.Collections.ArrayList. ¿Queréis saber por qué? Pues porque necesito meter los capítulos en algún array o en alguna colección (en este caso, se trata de una colección). Es decir, una vez más, el hecho de implementar un indizador no convierte a una clase en una colección o en un array, sino simplemente hace que ofrezca una interfaz similar a estos, nada más. Por lo tanto, si tengo que almacenar elementos en un array o en una colección, lógicamente, necesito un array o una colección donde almacenarlos. En definitiva, el indizador hace que podamos "encubrir" dicha colección en aras de obtener una lógica más natural y manejable.
Para terminar, la línea que dice "throw new Exception..." manda un mensaje de error al cliente. No os preocupéis demasiado porque veremos las excepciones o errores a su debido tiempo.
SOBRECARGA DE OPERADORES
Esto es algo que te sonará muy extraño, sobre todo al principio, si no eras programador de C++, así que procura tomártelo con calma y entenderlo bien porque aquí es bastante fácil armarse un buen jaleo mental.
Veamos, sobrecargar un operador consiste en modificar su comportamiento cuando este se utiliza con una determinada clase. ¿Qué operador? Pues casi cualquiera: +, -, *, /, <<, >>, etc. (luego te pongo una lista de los operadores que puedes sobrecargar en C#). Vamos a empezar poniendo ejemplos de la utilidad de esto para ver si consigo que lo vayáis comprendiendo.
Todos sabemos que es perfectamente factible sumar dos números de tipo int, o dos de tipo short, e incluso se pueden sumar dos números de tipos distintos. Pero, por ejemplo, ¿qué ocurriría si tengo una variable en la que almaceno cantidades en metros y otra en centímetros y las sumo? Pues ocurrirá que el resultado sería incorrecto, puesto que solo puedo sumar metros con metros y centímetros con centímetros. Esto es:
double m=10;
double c=10;
double SumaMetros=m+c;
double SumaCentimetros=m+c;
Console.WriteLine(SumaMetros);
Console.WriteLine(SumaCentimetros);
Evidentemente, el ordenador no sabe nada de metros ni centímetros, de modo que el resultado sería 20 en ambos casos. Claro, esto es incorrecto, porque 10 metros más 10 centímetros serán 10.1 metros, o bien 1010 centímetros, pero en ningún caso será el 20 que hemos obtenido. ¿De qué modo podríamos solventar esto? Pues una posibilidad sería crear una clase metros con un método que se llamara SumarCentimetros, de modo que hiciera la suma correcta. Sin embargo, el uso de esta clase sería un poco forzado, ya que si queremos sumar, lo suyo es que usemos el operador + en lugar de tener que invocar un método específico.
Pues bien, aquí es donde entra la sobrecarga de operadores. Si puedo sobrecargar el operador + en la clase metros para que haga una suma correcta independientemente de las unidades que se le sumen, habremos resuelto el problema facilitando enormemente la codificación en el cliente. Estoy pensando en algo como esto:
Metros m=new Metros(10);
Centimetros c=new Centimetros(10);
Metros SumaMetros=m+c;
Centimetros SumaCentimetros=c+m;
Console.WriteLine(SumaMetros.Cantidad);
Console.WriteLine(SumaCentimetros.Cantidad);
Y que el resultado sea correcto, es decir, 10.1 metros y 1010 centímetros, sin necesidad de convertir previamente los centímetros a metros y viceversa. ¿Lo vamos pillando? ¿Sí? Bien es cierto que esto resulta aún un tanto extraño al tener que usar los constructores y la propiedad Cantidad, pero más adelante veremos que esto también tiene solución. Por ahora vamos a ver la sintaxis de un operador + sobrecargado:
[acceso] static NombreClase operator+(Tipo a[, Tipo b])
{
...
}
Veamos, en primer lugar el modificador de acceso en caso de que proceda, con la precaución de que el modificador de acceso de un operador sobrecargado no puede ser de un ámbito mayor que el de la propia clase, es decir, si la clase es internal, el operador sobrecargado no puede ser public, puesto que su nivel de acceso sería mayor que el de la clase que lo contiene. Después la palabra static. Lógicamente, tiene que ser static puesto que el operador está sobrecargado para todos los objetos de la clase, y no para una sola instancia en particular. Después el nombre de la clase, lo cual implica que hay que retornar un objeto de esta clase. Después la palabra "operator" seguida del operador que deseamos sobrecargar (ojo, deben ir juntos sin separarlos con ningún espacio en blanco) y, a continuación, la lista de argumentos entre paréntesis. Si el operador a sobrecargar es unitario, necesitaremos un único argumento, y si es binario necesitaremos dos. Veamos una primera aproximación a la clase Metros para hacernos una idea más clara y luego seguimos dando más detalles:
public class Metros
{
private double cantidad=0;
public Metros() {}
public Metros(double cant)
{
this.cantidad=cant;
}
public double Cantidad
{
get
{
return this.cantidad;
}
set
{
this.cantidad=value;
}
}
public static Metros operator+(Metros m, Centimetros c)
{
Metros retValue=new Metros();
retValue.Cantidad=m.Cantidad+c.Cantidad/100;
return retValue;
}
}
Como ves, la sobrecarga del operador está abajo del todo y en negrilla. ¿Cómo fucionaría esto? Pues vamos a ver si consigo explicártelo bien para que lo entiendas: Digamos que esto es un método que se ejecutará cuando el compilador se tope con una suma de dos objetos, el primero de la clase Metros y el segundo de la clase Centimetros. Por ejemplo, si "m" es un objeto de la clase Metros, y "c" un objeto de la clase Centimetros, la línea:
Metros SumaMetros=m+c;
Haría que se ejecutara el método anterior. Es decir, se crea un nuevo objeto de la clase Metros (el objeto retValue). En su propiedad Cantidad se suman lo que valga la propiedad Cantidad del argumento m y la centésima parte de la propiedad Cantidad del argumento c, retornando al final el objeto con la suma hecha, objeto que se asignaría, por lo tanto, a SumaMetros. Es decir, al haber sobrecargado el operador + en la clase Metros, cuando el compilador se encuentra con la suma en cuestión lo que hace es lo que hayamos implementado en el método, en lugar de la suma normal. Dicho de otro modo, hemos modificado el comportamiento del operador +.
Ahora bien, ¿qué ocurriría si nos encontramos con una resta en lugar de una suma?
Metros SumaMetros=m-c;
Pues que el compilador daría un error, ya que el operador - no está sobrecargado y, por lo tanto, es incapaz de efectuar la operación. Por lo tanto, para que se pudiera efectuar dicha resta habría que sobrecargar también el operador -, de este modo:
public static Metros operator-(Metros m, Centimetros c)
{
Metros retValue=new Metros();
retValue.Cantidad=m.Cantidad-c.Cantidad/100;
return retValue;
}
Ya ves que es muy parecido, sólo que esta vez restamos en lugar de sumar. Lo mismo habría que hacer con los operadores * y /, para que también se hicieran correctamente las multiplicaciones y divisiones.
Por otro lado, ¿cómo reaccionaría el compilador si se encuentra con esta otra línea?
Centimetros SumaCentimetros=c+m;
Pues volvería a dar error, puesto que la sobrecarga en la clase Metros afecta a las sumas cuando el primer operando es de la clase Metros y el segundo es de la clase Centímetros. Ya sé que estarás pensando en añadir otra sobrecarga del operador + en la clase Metros en la que pongas los centímetros en el primer argumento y los metros en el segundo. Sin embargo, eso seguiría dando error, puesto que SumaCentimetros no es un objeto de la clase Metros, sino de la clase Centimetros. Por lo tanto, lo más adecuado sería sobrecargar el operador de ese modo pero en la clase Centimetros y no en la clase Metros.
Puede que ahora estés pensando también en sobrecargar el operador =, para que puedas asignar directamente un objeto de la clase centímetros a otro de la clase metros. Sin embargo el operador de asignación (es decir, =) no se puede sobrecargar. Mala suerte... En lugar de esto podemos usar las conversiones definidas, como vemos a continuación.
Estos son los operadores que puedes sobrecargar con C#:
Unitarios: +. -, !, ~, ++, --, true, false
Binarios: +, -, *, /, %, &, |, ^,<<, >>, ==, !=, >, <, >=, <=
Efectivamente, tal como supones, las comas las he puesto para separar los operadores, porque la coma no se puede sobrecargar.
CONVERSIONES DEFINIDAS
Son también un elemento de lo más útil. Hasta ahora hemos visto que con la sobrecarga de operadores podemos hacer algo tan inverosímil como por ejemplo sumar objetos de clases distintas e incompatibles. Pues bien, las conversiones definidas vienen a abundar un poco más sobre estos conceptos, permitiendo hacer compatibles tipos que antes no lo eran.
Decíamos antes que el fragmento de código que pongo a continuación seguía siendo un tanto extraño por culpa de tener que usar constructores y propiedades:
Metros m=new Metros(10);
Centimetros c=new Centimetros(10);
Metros SumaMetros=m+c;
Centimetros SumaCentimetros=c+m;
Console.WriteLine(SumaMetros.Cantidad);
Console.WriteLine(SumaCentimetros.Cantidad);
Las conversiones definidas nos van a permitir manejar todo esto de un modo mucho más natural, como veréis en este código equivalente:
Metros m=(Metros) 10;
Centimetros c=(Centimetros) 10;
Metros SumaMetros=m+c;
Centimetros SumaCentimetros=c+m;
Console.WriteLine((double) SumaMetros);
Console.WriteLine((double) SumaCentimetros);
Ahora todo parece mucho más claro, ¿no es cierto? Como veis, ya no tenemos que complicarnos en usar constructores y tampoco tenemos que usar la propiedad Cantidad para escribir su valor con WriteLine. Esto es así porque, tanto para la clase Metros como para la clase Centimetros, hemos creado conversiones definidas que nos han hecho compatibles esas clases con el tipo double. Gracias a esto podemos convertir a metros un número (como en la primera línea) y también podemos convertir a double un objeto de la clase Metros (dentro de WriteLine). Veamos el código de estas conversiones definidas en la clase Metros:
public static explicit operator Metros(double cant)
{
Metros retValue=new Metros(cant);
return retValue;
}
public static explicit operator double(Metros m)
{
return m.Cantidad;
}
Fijaos bien: En la primera conversión definida, la conversión que estoy definiendo es Metros, de modo que pueda convertir en objetos de esta clase cualquier número de tipo double con la sintaxis (Metros) numero. En la segunda lo que hago es precisamente lo contrario, esto es, defino la conversión double para convertir en datos de este tipo objetos de la clase Metro, mediante la sintaxis (double) ObjetoMetros. Por este motivo, en la primera conversión definida el argumento es de tipo double, y en la segundo es de tipo Metros. Por otra parte, la palabra explicit hace que sea obligatorio usar el operador de conversión para convertir entre estos dos tipos. Es decir, no se puede asignar un valor de tipo double a un objeto de la clase Metros sin usar el operador de conversión (Metros). Ahora bien, también podríamos definir estas conversiones como implícitas, de modo que el operador de conversión no fuera necesario. Para ello bastaría con poner la palabra implicit en lugar de explicit, esto es, así:
public static implicit operator Metros(double cant)
{
Metros retValue=new Metros(cant);
return retValue;
}
public static implicit operator double(Metros m)
{
return m.Cantidad;
}
Con lo que el resultado todavía es mucho más manejable. Fijaos en cómo podríamos dejar finalmente el código cliente de las clases Metros y Centimetros:
Metros m=10;
Centimetros c=10;
Metros SumaMetros=m+c;
Centimetros SumaCentimetros=c+m;
Console.WriteLine(SumaMetros);
Console.WriteLine(SumaCentimetros);
Ahora veis que ni siquiera hemos necesitado hacer las conversiones explícitas, ya que las conversiones definidas en ambas clases las hemos hecho implícitas con la palabra clave implicit en lugar de explicit. ¿Que por qué se puede asignar 10, que es de tipo int al no tener el sufijo D, si la conversión definida está creada solamente para el tipo double? Pues, sencillamente, porque el tipo double es compatible con el tipo int, de modo que basta con hacer la conversión definida con el tipo double para que sirva con todos los tipos numéricos.
Antes de terminar aún nos queda una cosilla más. ¿qué pasaría si intentamos asignar un objeto de la clase centímetros a otro de la clase metros y viceversa?
Metros cEnMetros=c;
Centimetros mEnCentimetros=m;
Pues, nuevamente, el compilador generaría otro error. Antes de poder hacer eso deberíamos crear también las conversiones definidas pertinentes en Metros y Centímetros para hacerlos compatibles y, también, para que se asignen los valores adecuadamente. Vamos a verlo:
En la clase Metros habría que escribir:
public static implicit operator Metros(Centimetros c)
{
Metros retValue=new Metros();
retValue.Cantidad=c.Cantidad/100;
return retValue;
}
Y en la clase Centímetros, lo mismo pero al revés:
public static implicit operator Centimetros(Metros m)
{
Centimetros retValue=new Centimetros();
retValue.Cantidad=m.Cantidad*100;
return retValue;
}
Como veis, no basta con escribir las conversiones para hacerlos compatibles, sino que también hay que escribirlas correctamente para que se asignen valores adecuados, es decir, si a "m" (que es de tipo Metros) le asignamos 10 centímetros, "m" tiene que valer, necesariamente, 0.1 metros.
Por último, sería bueno que no os olvidarais de otro pequeño detalle, y es reescribir el método ToString (recordad que este método se hereda siempre de System.Object). Soy consciente de que aún no hemos visto la herencia, pero bueno, dicho método tendría que ir así tanto en la clase Metros como en la clase Centímetros:
public override string ToString()
{
return this.cantidad.ToString();
}
No quiero dar por terminada esta entrega sin aconsejaros en cuanto a esto de la sobrecarga de operadores y las conversiones definidas, y es que hay algunos principios que siempre es bueno respetar:
Usa la sobrecarga de operadores y las conversiones definidas únicamente cuando el resultado final sea más natural e intuitivo, no por el mero hecho de que sabes hacerlo. Por ejemplo, en una clase Alumno podrías sobrecargar el operador + por ejemplo para cambiar su nombre. Ciertamente se puede hacer, pero evidentemente el resultado será muy confuso, porque no tiene sentido lógico alguno sumar una cadena a un objeto de clase Alumno para modificar una de sus propiedades. Dicho de otro modo, es como el vino: mientras su uso normal es bueno, el abuso es muy malo.
Por otra parte tienes que pensar en un detalle que es muy importante: si por medio de la sobrecarga de operadores y las conversiones definidas haces que dos tipos sean compatibles, tienes que intentar que sean compatibles a todos los niveles posibles. Por ejemplo, con las clases Metros y Centimetros, habría que sobrecargar también los operadores /, *, %, >, >=, <, <=, == y != para hacer posibles las divisiones, multiplicaciones, el cálculo de restos y las comparaciones (es decir, la comparación 1 metro == 100 centímetros debería retornar true).
Te voy a proponer un par de ejercicios para esta entrega. Es probable que con ello te ponga en algún apuro, pero recuerda que el método prueba-error es el mejor modo de aprender.
Una anotación para aquellos que seguís el curso desde fuera de Europa: En las pistas de este ejercicio explico algunos conceptos con los que quizá no estéis familiarizados en vuestro país.
Necesitamos una clase para almacenar los datos de una factura. Dichos datos son: Nombre del cliente, teléfono, dirección, población, provincia, código postal, NIF o CIF y porcentaje de IVA. Por otra parte tienes que tener presente que en una misma factura puede haber una o varias líneas de detalle con los siguientes datos: Cantidad, descripción, precio unitario e importe. Usa un indizador para acceder a cada una de estas líneas de detalle. Esta clase debe ofrecer, además, propiedades que devuelvan la base imponible, la cuota de IVA y el total a pagar. Escribid también un método Main cliente de esta clase que demuestre que funciona correctamente.
Supongo que ya habrás deducido que para que la clase Factura cumpla los requisitos que te pido tendrás que construir también una clase Detalle. Pues bien, te propongo también que sobrecargues el operador + para que puedas sumar objetos de la clase Detalle a objetos de la clase Factura. Ojo, en este caso solamente queremos hacer posible la suma Factura+Detalle, nada más.
Sigue este vínculo para ir a las pistas de este ejercicio.
Intenta construir dos clases: la clase Euro y la clase Peseta (la peseta era la antigua moneda oficial de España antes de ser reemplazada por el Euro). Tienes que hacer que los objetos de estas clases se puedan sumar, restar, comparar, incrementar y disminuir con total normalidad como si fueran tipos numéricos, teniendo presente que 1 Euro + 166.386 pesetas=2 euros. Además, tienen que ser compatibles entre sí y también con el tipo double. Recuerda que 1 Euro = 166.386 pesetas. Para este ejercicio no hay pistas.
Sigue este vínculo si quieres bajarte los ejemplos de esta entrega.