Inicio > .NET Framework, C#, Tutorial > Todo lo que siempre quisiste saber sobre colecciones y tenías miedo de preguntar… Arrays

Todo lo que siempre quisiste saber sobre colecciones y tenías miedo de preguntar… Arrays


Los arrays son estructuras de datos que nos permiten almacenar objetos de forma contigua. Los arrays tienen un tipo de dato bien definido. En C#, los declaramos utilizando corchetes, y los asignamos indicando el número de elementos que tendrán.

// inicialización explícita
int[] nums = new nums[5];

// inicialización implícita y asignación
int[] nums = new nums[] { 1, 2, 3, 4, 5 };

// inicialización y asignación implícita
int[] nums = { 1, 2, 3, 4, 5 };

Propiedades básicas

Los arrays tienen algunas propiedades interesantes. Length y LongLenth regresan el número de elementos que tiene un array. Por su parte, Rank regresa el número de dimensiones del array. Ojo, Length nos dice cuántos elementos existen, pero no nos dice cuántos elementos por dimensión hay. En el ejemplo anterior, Length nos regresa 15, mientras que Rank nos regresa 2. Cuando trabajamos con arrays unidimensionales (Rank == 1)  no hay problema, pero si trabajamos con más dimensiones podemos tener problemas. Para paliarlos, podemos hacer uso tanto de Rank como de dos métodos: GetLowerBound y GetUpperBound. Estos métodos regresan el índice mínimo y máximo, respectivamente, de una dimensión determinada. Por supuesto, GetLowerBound siempre regresará cero (o al menos no puedo pensar algún caso en el que el inicio de un array no sea cero). Así, podemos recorrer un array multidimensional iterando sobre el valor de Rank, y de ahí un bucle anidado que itere desde GetLowerBound hasta GetUpperBound.

Por otra parte, los arrays cuentan con dos propiedades: GetValue y SetValue. Estos métodos cuentan con sobrecargas para obtener o establecer los valores en las posiciones indicadas en los parámetros, y se puede utilizar incluso con varias dimensiones.

static void Main(string[] args)
{
  int[,] nums = new int[,] { 
    { 1, 2, 3, 4, 5 }, 
    { 6, 7, 8, 9, 10 }, 
    { 11, 12, 13, 14, 15 } 
  };

  int num = (int)nums.GetValue(1, 3);
  Console.WriteLine(num);

  nums.SetValue(0, 2, 4);
  Console.WriteLine(nums[2, 4]);

  Console.ReadKey(true);
}

El programa anterior escribe un “9” seguido de un “0” en la siguiente línea.

Nota que GetValue regresa un object, y SetValue utiliza un object como primer parámetro (lo cual es lógico porque el array puede ser de cualquier tipo de dato). Esto provoca que para arrays de tipos valor (estructuras, ValueType) habrá un boxing y unboxing esperándonos, efectívamente alentando nuestra aplicación. Así que es más eficiente que utilices los indexadores normalitos (corchetes) de los arrays.

Pero entonces, la pregunta… ¿como para qué proveer esos métodos? La respuesta es que todos los arrays derivan de una clase base, y que cuando tienes un método que tenga que trabajar con arrays sin importar el tipo de dato, se utiliza ésta para la referencia.

 

La clase base

Los arrays son objetos del CLR y como tal, ultimadamente heredan de object. En particular, existe una clase abstracta de la cual heredan todos los arrays: la clase Array, ubicada en el espacio de nombres System. La herencia la hace en automático el compilador, en modo ninja. Y uno, simple mortal, no puede heredar de dicha clase: de hacerlo, el compilador provocará un error de compilación (CS0644). Para usar los arrays debemos utilizar la construcción propia del lenguaje. Pero sí podemos referenciar cualquier array a través de esta clase.

Array nums = new int[] { 1, 2, 3, 4, 5 };

Incluso podemos referenciar arrays multidimensionales.

Array nums = new int[,] 
  { 
      { 1, 2, 3, 4, 5 }, 
      { 1, 2, 3, 4, 5 }, 
      { 1, 2, 3, 4, 5 } 
  };

Ahora sí tiene sentido utilizar las propiedades y métodos que describimos en la sección anterior.

La clase Array cuenta con algunos métodos estáticos que nos pueden servir mucho. En primera, tenemos CreateInstance, que nos permite crear un array en tiempo de ejecución dado su tipo de dato y número de dimensiones.

Array nums = Array.CreateInstance(typeof(int), 3, 5);
for (int i = 0; i < 3; i++)
  for (int j = 0; j < 5; j++)
    nums.SetValue((i * 5) + j + 1, i, j);

for (int i = 0; i < 3; i++)
  for (int j = 0; j < 5; j++)
    Console.WriteLine(nums.GetValue(i, j));

Podemos realizar búsquedas binarias con el método BinarySearch. Por supuesto, este algoritmo requiere que el array esté ordenado. No problemo, usamos el método Sort antes.

Random rnd = new Random();
Array nums = Array.CreateInstance(typeof(int), 10);
for (int i = 0; i < nums.Length; i++)
  nums.SetValue(rnd.Next(0, 10), i);

Array.Sort(nums);
for (int i = 0; i < nums.Length; i++)
  Console.WriteLine(nums.GetValue(i));

int index = Array.BinarySearch(nums, 4);
Console.WriteLine("Posición: {0}", index);

Array.Sort utiliza el comparador por defecto del tipo de dato. Podemos especificar algún comparador en particular, o incluso algún predicado. El siguiente ejemplo genera diez números aleatorios y los ordena de forma descendente, utilizando un delegado comparador.

Random rnd = new Random();
int[] nums = new int[10];
for (int i = 0; i < nums.Length; i++)
  nums[i] = rnd.Next(0, 10);

Array.Sort(nums, (n1, n2) => -n1.CompareTo(n2));
for (int i = 0; i < nums.Length; i++)
  Console.WriteLine(nums.GetValue(i));

Array.Foreach nos permite realizar una acción (pasada como delegado) a ejecutar sobre cada elemento del array. El siguiente ejemplo genera un array de 10 números aleatorios y los imprime en la consola, con un código más compacto.

Random rnd = new Random();
int[] nums = new int[10];
for (int i = 0; i < nums.Length; i++)
  nums[i] = rnd.Next(0, 10);

Array.ForEach(nums, n => Console.WriteLine(n));

 

Los métodos Find, FindAll y FindLast obtienen el primer elemento, todos, y el último elemento, respectivamente, que coincida con un predicado de búsqueda. En el siguiente ejemplo generamos un array de 10 números aleatorios y buscamos aquellos que sean pares.

Random rnd = new Random();
int[] nums = new int[10];
for (int i = 0; i < nums.Length; i++)
  nums[i] = rnd.Next(0, 10);

int[] pairs = Array.FindAll(nums, n => n == 0 || n % 2 == 0);
Array.ForEach(pairs, n => Console.WriteLine(n));

FindIndex y FindLastIndex funcionan igual que Find y FindLast, solo que en lugar de devolvernos un elemento del array nos devuelve el índice de dicho elemento.

Por su parte, el método TrueForAll ejecuta un predicado (un delegado de tipo Predicate<T>) sobre cada elemento del array. Éste le pasa el elemento al predicado, uno a uno, y el predicado devuelve verdadero o falso. TrueForAll devuelve verdadero en caso de que todos los predicados hayan devuelto verdadero. El siguiente ejemplo crea un array de diez números con valores aleatorios, y luego utiliza TrueForAll para determinar si todos los elementos del array son menores e iguales a cinco o no.

Random rnd = new Random();
int[] nums = new int[10];
for (int i = 0; i < nums.Length; i++)
  nums[i] = rnd.Next(0, 10);

Array.ForEach(nums, n => Console.Write("{0} ", n));
Console.WriteLine();

bool value = Array.TrueForAll(nums, n => n <= 5);
Console.WriteLine("¿Son todos menores a cinco? {0}", value);

Otro método interesante es ConvertAll. Éste método toma como parámetro algún array de tipo especificado y lo convierte en algún otro array, a través de un delegado, el cual realiza la conversión de un elemento en otro. Por ejemplo, el siguiente código muestra cómo convertir y transformar un array de números aleatorios en un array de cadenas de texto.

Random rnd = new Random();
int[] nums = new int[10];
for (int i = 0; i < nums.Length; i++)
  nums[i] = rnd.Next(0, 10);

Array.ForEach(nums, n => Console.Write("{0} ", n));
Console.WriteLine();

string[] strs = Array.ConvertAll<int, string>(nums, n => "Número " + n);
Array.ForEach(strs, str => Console.WriteLine(str));

 

Arrays como colecciones

Todas estas propiedades son útiles para manipular arrays. Pero este tutorial es sobre colecciones, no sobre arrays. ¿Qué tiene que ver? Bueno, un array implementa la interfaz ICollection (y por ende, IEnumerable), efectívamente haciéndola una colección. De hecho, Array también implementa IList, así que un array es una lista también. Veamos.

Si el array implementa IEnumerable, quiere decir que podemos iterar sobre los elementos del array a través de un foreach. En efecto.

Array nums = new int[] { 1, 2, 3, 4, 5 };
foreach (int num in nums)
  Console.WriteLine(num);

El iterador (y por ende, el foreach) regresa todos los elementos del array. Así, en un array multidimensional (i.e. Rank > 1) se regresan los elementos de la primera dimensión más los de la segunda, y así sucesivamente.

int[,] nums = new int[,] 
  { 
    { 1, 2, 3, 4, 5 }, 
    { 6, 7, 8, 9, 10 }, 
    { 11, 12, 13, 14, 15 } 
  };
foreach (int num in nums)
  Console.WriteLine(num);

El ejemplo anterior imprime los números del 1 al 15.

Por otra parte, el implementar ICollection me garantiza al menos una propiedad Count para saber el número de elemetos que contiene, más una propiedad SyncRoot que me permita bloquear el acceso al array usando la cláusula lock. En efecto, SyncRoot está disponible en cualquier array, pero Count se implementa expícitamente, así que para acceder a ésta hay que hacer una conversión (explícita o implícita) hacia la interfaz ICollection para poder acceder a Count.

int[] nums = new int[] { 1, 2, 3, 4, 5 };
      
ICollection col = nums; // conversión implícita
lock (col.SyncRoot)
{
  Console.WriteLine("Count: {0}", col.Count);
  foreach (object num in col)
    Console.WriteLine(num);
}

Escapa a mi entendimiento el por qué el equipo de .NET definió la propiedad Length cuando ya tenía Count, y en cambió decidió ocultar ésta última. En fin, el chiste es que Count, como bien podemos ver utilizando Reflector y desensamblando mscorlib.dll, regresa lo mismo que Length. Pero bueno, así es la vida…

Más interesante es el hecho de que implementen IList. Por supuesto, si hacemos la conversión a IList de un array, y de ahí llamamos a Add, Remove, RemoveAt o Insert, obtendremos una NotSupportedException. Si llamamos al método Clear, empero, lo que pasará es que el array se inicializará de nuevo. Es decir, si el array es de tipo valor, se establecerá a cero y todos sus miembros se establecerán a cero o null. Si el array es de tipo referencia, los miembros se establecerán a null.

El siguiente ejemplo ilustra cómo se lanzan las excepciones esperadas. A su vez, se utiliza Clear para re-establecer todo el array.

int[] nums = new int[] { 1, 2, 3, 4, 5 };
Array.ForEach(nums, n => Console.WriteLine(n));

IList list = nums; // conversión implícita
try { list.Add(6); }
catch { Console.WriteLine("Error al agregar."); }
try { list.Remove(1); }
catch { Console.WriteLine("Error al remover."); }
try { list.Insert(0, 6); }
catch { Console.WriteLine("Error al insertar."); }

list.Clear();
Array.ForEach(nums, n => Console.WriteLine(n));

La salida en consola es la siguiente:

1
2
3
4
5
Error al agregar.
Error al remover.
Error al insertar.
0
0
0
0
0

Otro hecho interesante es el indexador de IList. Sabemos que un array tiene su indexador. Pues bien, el indexador de IList regresa los mismos elementos que el indexador del array. Pero ojo, que esto solo funciona para arrays unidimensionales. Si intentas acceder al indexador de un array multidimensional, tendrás un ArgumentException. El siguiente ejemplo ilustra lo anterior.

int[] nums = new int[] { 1, 2, 3, 4, 5 };
IList list = nums; // conversión implícita
for (int i = 0; i < list.Count; i++)
{
  object obj = list[i];
  Console.Write("{0} ", obj);
}
Console.WriteLine();

int[,] multinums = new int[,] { { 1, 2 }, { 1, 2 } };
list = multinums;
try
{
  for (int i = 0; i < list.Count; i++)
  {
    object obj = list[i];
    Console.Write("{0} ", obj);
  }
}
catch (ArgumentException e)
{
  Console.WriteLine(e.Message);
}

La salida en consola es la siguiente.

1 2 3 4 5
Array was not a one-dimensional array.

Con respecto a IList.IndexOf e IList.Contains, ambas declaradas explícitamente en la clase Array, cabe decir que IndexOf hace uso directamente de Array.IndexOf, y Contains regresa verdadero si el valor devuelto por IndexOf es mayor o igual al devuelto por GetLowerBound. Sobra decir, por cierto, que las propiedades IsReadOnly e IsFixedSize son false y true, respectivamente.

Los arrays son muy buenos para guardar conjunto de objetos no variables… pero… ¿qué pasa con

Los arrays son mucho muy útiles, pero tienen un defecto: no están hechos para que cambien de tamaño. Por supuesto, siempre podemos utilizar Array.Resize, pero esto tiende a ser ineficiente, sobre todo si lo usamos a cada oportunidad. Hay, sin embargo, una mejor opción: ArrayList.

Clase ArrayList

La clase ArrayList, ubicada en System.Collections, es una de las listas más utilizadas, dada su versatilidad. Y no es de extrañarse, dado que está desde .NET 1.0. ArrayList implementa IList, ICollection e IEnumerable, además de ICloneable. Es también una de las colecciones más versátiles que hay. Veamos cómo utilizarla, aunque he de decir que es muy intuitiva.

En primer lugar, cuenta con tres constructores: uno por default, uno que toma una colección cualquiera (ICollection) y agrega sus elementos a esta colección, y una que determina la capacidad inicial.


// inicializa un ArrayList dada una colección cualquiera
string[] strs = {"hugo", "paco", "luis" };
ArrayList list = new ArrayList(strs);

El método Add nos permite agregar un elemento mientras que AddRange nos permite agregar varios (toma como parámetro un ICollection). En contraparte, Remove, RemoveAt y RemoveRange  nos permite eliminar elementos dado un objeto en particular a eliminar, un índice y un índice con su rango.


ArrayList list = new ArrayList();

list.Add("donald");
list.Add("mcpato");

string[] strs = {"hugo", "paco", "luis" };
list.AddRange(strs);

list.Remove("donald");
list.RemoveAt(0); // remueve "mcpato"
list.RemoveRange(0, 2); // remueve "hugo" y "paco"

Dado que ArrayList implementa IList, tenemos las propiedades IsFixedSize, IsReadOnly, Count y SyncRoot, además del conocido indexador. Además, implementa Clear para borrar todo el contenido de la lista, y Contains para determinar si un elemento existe en la colección o no.

IndexOf nos permite obtener el índice de algún objeto (o –1 si éste no existe). CopyTo nos permite copiar el contenido a un array, mientras que ToArray devuelve un array con los objetos de la colección.

Por su parte, BinarySearch realiza una búsqueda binaria y nos devuelve el índice donde se encuentra. Para hacer una búsqueda binaria la colección tiene que estar ordenada, por lo que podemos usar el método Sort. El método Sort por default obtiene el IComparer por defecto del objeto en cuestión, o bien se le puede especificar IComparer de forma predefinida.


ArrayList list = new ArrayList();

list.Add("donald");
list.Add("mcpato");

string[] strs = {"hugo", "paco", "luis" };
list.AddRange(strs);

list.Sort(); // utiliza la comparación estándar de texto
int index = list.BinarySearch("hugo");
if (index >= 0)
   Console.WriteLine(list[index]);

La clase también cuenta con algunos métodos estáticos interesantes. Por ejemplo, ArrayList.Adapter convierte un IList en un ArrayList. FixedSize convierte un ArrayList en una lista de solo lectura y FixedSize devuelve un ArrayList que imposibilita el agregar o eliminar elementos.

Tristemente ArrayList acepta un objet, así que podemos agregar cualquier tipo de objeto, incluso mezclar varios tipos. Esto nos provoca problemas si tratamos con tipos valor (ValueType), debido al boxing y unboxing. Aún así, es común encontrar colecciones que derivan de ArrayList.

class StringList : ArrayList
{
    public StringList()
        : base()
    {
    }

    public void Add(string str)
    {
        base.Add(str);
    }

    public string this[int index]
    {
        get { return base[index] as string; }
    }
}

ArrayList no es, sin embargo, la mejor base para crear nuestras propias colecciones. En efecto, existen CollectionBase y DictionaryBase para ello, pero éstas son tema de otra entrada en el blog. Por otra parte, ArrayList ha perdido popularidad a partir de que salieron los genéricos en .NET 2.0 y se creó la clase List<T>, que es más eficiente que ArrayList para tipos valor (ValueType) ya que no incurren en las penas de boxing y unboxing. Sin embargo, ArrayList puede ser una solución sencilla cuando necesitamos arrays dinámicos con funcionalidad básica.

Conclusiones

Hemos visto cómo los arrays son otro tipo de colecciones, aunque éstos no sean dinámicos. Y cómo ArrayList funciona como un array dinámico. Pero todavía queda mucho camino por recorrer en esto de las colecciones.

En la siguiente entrada veremos algunos tipos especiales de colecciones: las pilas y las colas. Por ahora, es tiempo de ir a dormir.

Anuncios
Categorías:.NET Framework, C#, Tutorial Etiquetas: , ,
  1. febrero 27, 2014 en 1:56 am

    Muy útil Fernando. Gracias.
    Sugerencia: La diferencia entre los métodos .ToArray y .CopyTo creo que no está clara y es que el segundo permite extraer sólo un subconjunto de elementos. ¿cierto?

  2. febrero 27, 2014 en 2:06 am

    Fernando, en el comentario anterior me refiero a la sección de ArrayList.
    En la misma sección, casi al final del post, ArrayList.FixedSize contiene un error.
    Gracias por compartir tu claridad de conocimientos.

  1. No trackbacks yet.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s