Curso de iniciación a la programación con C# |
Hasta ahora hemos aprendido un montón de cosas con respecto a las variables, pero siempre teníamos que saber con antelación el número de variables que el programa iba a necesitar. Sin embargo, habrá situaciones en las que no sea posible determinar este número hasta que el programa no se esté ejecutando. Pongamos por ejemplo que estamos diseñando un programa de facturación. Evidentemente, cada factura tendrá una serie de líneas de detalle, pero será imposible conocer el número de líneas de detalle de cada factura en tiempo de diseño, esto es, antes de que el programa comience su ejecución. Pues bien, para solucionar estas dificultades contamos con los arrays y los indizadores.
ARRAYS
Antes de comenzar a explicaros con mayor claridad qué es un array quiero advertir nuevamente a los programadores de C/C++: En C#, aunque parecidos, los arrays son diferentes tanto semántica como sintácticamente, de modo que te recomiendo que no pases por alto esta entrega.
Bien, una vez hechas todas las aclaraciones previas, creo que podemos comenzar. Un array es un indicador que puede almacenar varios valores simultáneamente. Cada uno de estos valores se identifica mediante un número al cual se llama índice. Así, para acceder al primer elemento del array habría que usar el índice cero, para el segundo el índice uno, para el tercero el índice dos, y así sucesivamente. Que nadie se preocupe si de momento todo esto es un poco confuso, ya que lo voy a ir desmenuzando poco a poco. Vamos a ver cómo se declara un array:
tipo[] variable;
Bien, como veis es muy parecido a como se declara una variable normal, sólo que hay que poner corchetes detrás del tipo. Los programadores de C/C++ habrán observado inmediatamente la diferencia sintáctica. En efecto, en la declaración de un array en C# los corchetes se colocan detrás del tipo y no detrás de la variable. Esta pequeña diferencia sintáctica se debe a una importante diferencia semántica: aquí los arrays son objetos derivados de la clase System.Array. Por lo tanto, y esto es muy importante, cuando declaramos un array en C# este aún no se habrá creado, es decir, no se habrá reservado aún memoria para él. En consecuencia, los arrays de C# son todos dinámicos, y antes de poder usarlos habrá que instanciarlos, como si fuera cualquier otro objeto. Veamos un breve ejemplo de lo que quiero decir:
string[] nombres; // Declaración del array
nombres = new string[3]; // Instanciación del array
En efecto, tal como podéis apreciar, el array nombres será utilizable únicamente a partir de su instanciación. En este ejemplo, el número 3 que está dentro de los corchetes indica el número total de elementos de que constará el array. No os equivoquéis, puesto que todos los arrays de C# están basados en cero, esto es, el primer elemento del array es cero. Por lo tanto, en este caso, el último elemento sería 2 y no 3, ya que son tres los elementos que lo componen (0, 1 y 2). Veamos un ejemplo algo más completo y después lo comentamos:
using System;
namespace Arrays
{
class ArraysApp
{
static void Main()
{
string[] nombres; // Declaración del array
ushort num=0;
do
{
try
{
Console.Write("¿Cuántos nombres vas a introducir? ");
num=UInt16.Parse(Console.ReadLine());
}
catch
{
continue;
}
} while (num==0);
nombres=new string[num]; // Instanciación del array
for (int i=0; i<num; i++)
{
Console.Write("Escribe el nombre para elemento {0}: ", i);
nombres[i]=Console.ReadLine();
}
Console.WriteLine("Introducidos los {0} nombres", num);
Console.WriteLine("Pulsa INTRO para listarlos");
string a=Console.ReadLine();
for (int i=0; i<num; i++)
{
Console.WriteLine("Elemento {0}: {1}", i, nombres[i]);
}
a=Console.ReadLine();
}
}
}
Veamos ahora la salida en la consola (en rojo, como siempre, lo que se ha escrito durante la ejecución del programa):
¿Cuántos nombres vas a introducir? 3
Escribe el nombre para el elemento 0: Juanito
Escribe el nombre para el elemento 1: Jaimito
Escribe el nombre para el elemento 2: Joselito
Introducidos los 3 nombres
Pulsa INTRO para listarlos
Elemento 0: Juanito
Elemento 1: Jaimito
Elemento 2: Joselito
En este pequeño programa hemos declarado un array y lo hemos instanciado después de haber preguntado al usuario cuántos elementos iba a tener. Como veis, hemos utilizado un bucle for para recoger todos los valores que hay que meter en el array. Quiero que prestéis especial atención a cómo hemos introducido los valores en el array: en la línea "nombres[i] = Console.ReadLine()" lo que hacemos es que al elemento "i" del array le asignamos lo que devuelva el método ReadLine. Como "i" tomará valores entre 0 y el número total de elementos menos uno rellenaremos el array completo (fijaos en la condición del bucle, que es i<num, es decir, que si i es igual a num el bucle ya no se itera). Después tenemos otro bucle for para recorrer todo el array y escribir sus valores en la consola. En definitiva, para acceder a un elemento del array se usa la sintaxis "array[índice]".
Un array también puede inicializarse en la propia declaración, bien instanciándolo (como cualquier otro objeto) o bien asignándole los valores directamente. Vamos a reescribir el ejemplo anterior instanciando el array en la declaración del mismo:
using System;
namespace Arrays2
{
class Arrays2App
{
static void Main()
{
ushort num=3;
do
{
try
{
Console.Write("¿Cuántos nombres vas a introducir? ");
num=UInt16.Parse(Console.ReadLine());
}
catch
{
continue;
}
} while (num==0);
string[] nombres=new string[num]; // Declaración e instanciación del array
for (int i=0; i<num; i++)
{
Console.Write("Escribe el nombre para elemento {0}: ", i);
nombres[i]=Console.ReadLine();
}
Console.WriteLine("Introducidos los {0} nombres", num);
Console.WriteLine("Pulsa INTRO para listarlos");
string a=Console.ReadLine();
for (int i=0; i<num; i++)
{
Console.WriteLine("Elemento {0}: {1}", i, nombres[i]);
}
a=Console.ReadLine();
}
}
}
Bien, ahora, como puedes observar, el array ha sido instanciado en la misma línea en la que fue declarado. El funcionamiento de este ejemplo, por lo tanto, sería el mismo que el del ejemplo anterior. Veamos ahora otro ejemplo de inicialización del array asignándole los valores en la declaración:
using System;
namespace Arrays3
{
class Arrays3App
{
static void Main()
{
// Declaración e inicialización del array
string[] nombres={"Juanito", "Jaimito", "Joselito"};
for (int i=0; i<nombres.Length; i++)
{
Console.WriteLine("Elemento {0}: {1}", i, nombres[i]);
}
string a=Console.ReadLine();
}
}
}
En este caso, el array nombres ha sido inicializado en la propia declaración del mismo, asignándole los tres valores que va a contener. Como ves, dichos valores están entre llaves y separados por comas. Las comillas son necesarias en este caso, ya que el array es de tipo string. ¿Que dónde está la instanciación del array? Bueno, cuando hacemos esto, la instanciación la hace por debajo el compilador, es decir, de forma implícita. Presta atención también a la condición del bucle: ahora hemos usado la propiedad Length del array nombres en lugar de una variable. En efecto, esta propiedad nos devuelve el número de elementos de un array. Por lo tanto, la salida en consola de este programa sería esta:
Elemento 0: Juanito
Elemento 1: Jaimito
Elemento 2: Joselito
Por otro lado, el hecho de que un array haya sido inicializado no quiere decir que sea inamovible. Si un array que ya contiene datos se vuelve a instanciar, el array volverá a estar vacío, y obtendrá las dimensiones de la nueva instanciación.
Bien, todos estos arrays que hemos explicado hasta el momento son arrays unidimensionales, es decir, que tienen una sola dimensión (un solo índice). Sin embargo esto no soluciona aún todas las necesidades del programador. Pongamos, por ejemplo, que queremos almacenar las combinaciones de las ocho columnas de una quiniela de fútbol en un array.¿Cómo lo hacemos? Pues bien, el mejor modo es utilizar un array multidimensional.
ARRAYS MULTIDIMENSIONALES
Los arrays multidimensionales son aquellos que constan de dos o más dimensiones, es decir, que cada elemento del array viene definido por dos o más índices. Vamos a echar un vistazo a la declaración de un array multidimensional (en este caso, será tridiensional, es decir, con tres dimensiones):
tipo[,,] variable;
Como ves, hay dos comas dentro de los corchetes, lo cual indica que el array es tridimensional, puesto que los tres índices del mismo se separan uno de otro por comas. Veamos un pequeño ejemplo que lo clarifique un poco más:
string[,] alumnos = new string[2,4];
Este array es bidimensional y serviría para almacenar una lista de alumnos por aula, esto es, tenemos dos aulas (el primer índice del array es 2) y cuatro alumnos en cada una (el segundo índice es 4). Veamos un poco de código y una tabla para que os hagáis una idea de cómo se almacena esto:
alumnos[0,0]="Lolo";
alumnos[0,1]="Mario";
alumnos[0,2]="Juan";
alumnos[0,3]="Pepe";
alumnos[1,0]="Lola";
alumnos[1,1]="María";
alumnos[1,2]="Juana";
alumnos[1,3]="Pepa";
Esto sería como almacenar los datos en esta tabla:
AULA 0 | AULA 1 | |
NOMBRE 0 | Lolo | Lola |
NOMBRE 1 | Mario | María |
NOMBRE 2 | Juan | Juana |
NOMBRE 3 | Pepe | Pepa |
¿Que quieres saber por qué he separado a los chicos de las chicas? Bueno, no es que sea un retrógrado, es para que se vea mejor todo esto. Mira que sois detallistas... Bueno, creo que va quedando bastante claro. ¿Y cómo recorremos un array multidimensional? Pues con bucles anidados. Vamos ya con un ejemplo más completito de todo esto. Este pequeño programa pregunta al usuario por el número de columnas que quiere generar de una quiniela de fútbol, y después las rellena al azar y las muestra en pantalla:
using System;
namespace Quinielas
{
class QuinielasApp
{
static void Main()
{
const char local='1';
const char empate='X';
const char visitante='2';
const byte numFilas=14;
byte numColumnas=0;
char[,] quiniela;
byte azar;
Random rnd=new Random(unchecked((int) DateTime.Now.Ticks));
do
{
try
{
Console.WriteLine("Mínimo una columna y máximo ocho");
Console.Write("¿Cuántas columnas quieres generar? ");
numColumnas=Byte.Parse(Console.ReadLine());
}
catch
{
continue;
}
} while (numColumnas<1 || numColumnas>8);
quiniela=new char[numColumnas, numFilas];
for (byte i=0; i<numColumnas; i++)
{
for (byte j=0; j<numFilas; j++)
{
azar=(byte) (rnd.NextDouble()*3D);
switch (azar)
{
case 0:
quiniela[i,j]=local;
break;
case 1:
quiniela[i,j]=empate;
break;
case 2:
quiniela[i,j]=visitante;
break;
}
}
}
Console.WriteLine("Quiniela generada. Pulsa INTRO para verla");
string a=Console.ReadLine();
for (byte i=0; i<numColumnas; i++)
{
Console.Write("Columna {0}: ", i+1);
for (byte j=0; j<numFilas; j++)
{
Console.Write("{0} ", quiniela[i,j]);
}
Console.WriteLine();
Console.WriteLine();
}
a=Console.ReadLine();
}
}
}
Como veis, esto se va poniendo cada vez más interesante. De este programa, aparte de la clase Random, hemos visto todo excepto los bloques try y catch, de modo que si hay algo que no entiendes te recomiendo que revises las entregas anteriores. La clase Random es para generar números aleatorios (al azar). En la instanciación de dicha clase hemos puesto algo que puede resultarte algo confuso. Es esta línea:
Random rnd=new Random(unchecked((int) DateTime.Now.Ticks));
Bien, el constructor de esta clase tiene dos sobrecargas: una de ellas es sin argumentos, y la otra acepta un argumento de tipo int, que es la que hemos usado. ¿Por qué? Porque de lo contrario siempre generaría los mismos números en cada ejecución del programa, lo cual no sería muy útil en este caso. Como necesitamos que se generen números distintos tenemos que pasarle números diferentes en el argumento int del constructor de la clase Random, y el modo más eficaz de conseguirlo es hacer que ese número dependa del tiempo que lleve encendido el ordenador. Por otro lado, el número lo generamos al ejecutar el método NextDouble, el cual nos retorna un número mayor o igual a 0 y menor que 1. Esta es la línea:
azar=(byte) (rnd.NextDouble()*3D);
¿Por qué lo hemos multiplicado por 3D? Pues bien, como queremos números enteros entre 0 y 2 (o sea, 0, 1 o 2) bastará con multiplicar este número (recuerda que está entre cero y uno) por 3. ¿Y la D? Ahora voy, hombre. ¿Os acordáis de los sufijos en los literales, para indicar si se debía considerar si el número era de un tipo o de otro? Pues aquí está la explicación. Dado que el método NextDouble retorna un valor double, tenemos que multiplicarlo por otro valor double. Por eso le ponemos el sufijo "D" al número tres. Después todo ese resultado se convierte a byte y se asigna a la variable azar, que es la que se comprueba en el switch para asignar el carácter necesario según su valor a cada elemento del array.
Por lo demás creo que a estas alturas no debería tener que explicaros gran cosa: tenemos un par de bucles anidados para asignar los valores al array y después otros dos bucles anidados para recorrer dicho array y mostrar su contenido en la consola.
Otra cuestión importante en la que quiero que te fijes es en que ya estoy empezando a dejar de usar "literales y números mágicos", usando constantes en su lugar. Efectivamente, podría haberme ahorrado las cuatro constantes: local, empate, visitante y numFilas, poniendo sus valores directamente en el código, algo así:
...
for (byte i=0; i<numColumnas; i++)
{
for (byte j=0; j<14; j++)
{
azar=(byte) (rnd.NextDouble()*3D);
switch (azar)
{
case 0:
quiniela[i,j]='1';
break;
case 1:
quiniela[i,j]='X';
break;
case 2:
quiniela[i,j]='2';
break;
}
}
}
...
En efecto, funcionaría exactamente igual, pero ¿qué ocurriría si otra persona que no sabe qué es una quiniela, o por qué tiene que ser el número 14, o qué significan el 1, la X o el 2? Pues que el código sería menos claro. Las constantes, sin embargo, hacen la lectura del código más fácil. Por otro lado, si algún día cambiaran los signos, por ejemplo, si hubiese que poner una "a" en lugar del "1", una "b" en lugar de la "x" y una "c" en lugar del "2" y no hubiésemos usado constantes habría que buscar todos estos literales por todo el código y sustituirlos uno por uno, mientras que usando constantes (que están declaradas al principio) basta con modificar sus valores, haciendo así el cambio efectivo ya para todo el programa. Así que ya lo sabéis: a partir de ahora vamos a evitar en lo posible los "literales y los números mágicos".
Para terminar con esto, el número de dimensiones de un array se llama rango. Para conocer el rango de un array mediante código basta con invocar la propiedad Rank del mismo (heredada de la clase System.Array). Veamos un ejemplo de esto:
using System;
namespace Rangos
{
class RangosApp
{
static void Main()
{
int[] array1=new int[2];
int[,] array2=new int[2,2];
int[,,] array3=new int[2,2,2];
int[,,,] array4=new int[2,2,2,2];
Console.WriteLine("Rango de array1: {0}", array1.Rank);
Console.WriteLine("Rango de array2: {0}", array2.Rank);
Console.WriteLine("Rango de array3: {0}", array3.Rank);
Console.WriteLine("Rango de array4: {0}", array4.Rank);
string a=Console.ReadLine();
}
}
}
La salida en la consola de todo esto sería la siguiente:
Rango de array1: 1
Rango de array2: 2
Rango de array3: 3
Rango de array4: 4
ARRAYS DE ARRAYS
En efecto, para liar un poco más la madeja, tenemos también los arrays de arrays. Estos son arrays que pueden contener otros arrays. ¿Y para qué diablos queremos meter un array dentro de otro? ¿No nos basta con los arrays multidimensionales? Pues realmente podría bastarnos, en efecto, pero habría ocasiones en las que tendríamos que hacer bastantes cabriolas con el código por no usar los arrays de arrays. Pensad en un programa en el que el usuario tiene que manejar simultáneamente múltiples objetos de distintas clases derivadas de una clase base, por ejemplo, triángulos y cuadrados derivados de la clase figura. Si solamente pudiéramos usar arrays unidimensionales o multidimensionales tendríamos que declarar un array distinto para cada tipo de objeto (uno para triángulos y otro para cuadrados). La dificultad viene ahora: ¿Qué ocurre si hay que redibujar todos los objetos, ya sean cuadrados o triángulos? Evidentemente, habría que escribir un bucle para cada uno de los arrays para poder invocar los métodos Redibujar de cada uno de los elementos. Sin embargo, si metemos todos los arrays dentro de un array de arrays nos bastaría con escribir un par de bucles anidados para recorrer todos los objetos y dejar el resto en manos del polimorfismo. Ciertamente, aún no hemos estudiado a fondo ninguno de los mecanismos de la herencia. No obstante, con lo que sabemos hasta ahora, podemos poner un ejemplo sobre los arrays de arrays, aunque probablemente no se aprecie realmente la ventaja. Veamos el ejemplo, y luego lo comentamos. Eso sí, presta especial atención a la sintaxis, tanto en la declaración como en las instanciaciones:
using System;
namespace ArraysdeArrays
{
class ArraysDeArraysApp
{
static void Main()
{
object[][] numeros; // Declaración del array de arrays
numeros=new object[2][]; // Instanciación del array de arrays
numeros[0]=new object[3]; // Instanciación del primer array
numeros[1]=new object[4]; // Instanciación del segundo array
numeros[0][0]=3.325D;
numeros[0][1]=6.25D;
numeros[0][2]=3D;
numeros[1][0]=3u;
numeros[1][1]=7u;
numeros[1][2]=4u;
numeros[1][3]=87u;
for (int i=0;i<numeros.Length;i++)
{
for (int j=0;j<numeros[i].Length;j++)
{
Console.WriteLine(numeros[i][j].ToString());
}
}
string a=Console.ReadLine();
}
}
}
En este ejemplo vamos a usar un array en el que incluiremos dos arrays: uno para números de tipo double y otro para números de tipo ulong. Como estos dos tipos están derivados de la clase System.Object, lo que hacemos es declarar el array de este tipo en la primera línea del método Main, y después lo instanciamos diciéndole que contendrá dos arrays (en la segunda línea). Después instanciamos también como tipo object los dos arrays que contendrá el primero, y le asignamos valores: al array numeros[0] le asignamos valores de tipo double, y al array numeros[1] le asignamos valores de tipo ulong. Después usamos un par de bucles anidados para recorrer todos los elementos del array de arrays con el objeto de invocar el método ToString() de todos ellos (heredado de la clase System.Object). Como ves, el bucle "i" recorre el array de arrays (fíjate en la condición, i<numeros.Length), y el bucle "j" recorre cada uno de los elementos del array numeros[i], según sea el valor de i en cada iteración. Con los ejemplos de esta entrega se incluye también el de los cuadrados y los triángulos que te mencioné antes (en la carpeta Figuras), pero no lo reproduzco aquí porque aún no hemos visto la herencia. Sin embargo, cuando lo ejecutes, verás mejor la utilidad de los arrays de arrays.
Bien, creo que ya es suficiente para esta entrega. No te pondré ejercicios sobre los arrays todavía, pues prefiero esperar a que hayamos vistos los indizadores y los bucles foreach. Por cierto... espero que la próxima entrega no se haga esperar tanto como esta. A ver si hay algo de suerte...
Sigue este vínculo para bajarte los ejemplos de esta entrega.