jueves, 14 de julio de 2011

Colecciones no genéricas en .NET

Llevo programado en .NET ya unos años y me he dado cuenta que el uso que hago de las colecciones no es el más óptimo. En realidad nunca he invertido el tiempo necesario en conocer las distintas colecciones, y por eso me muevo siempre con las pocas que conozco e intento no meterme en líos.

Lo de no meterme en líos va a continuar, pero lógicamente ha llegado el momento de al menos conocer que colecciones están disponibles en .NET y cuando debería utilizar una u otra en función de las necesidades de mi código.

Lo primero que hay que saber cuándo hablamos de colecciones en .NET es que existen dos grandes bloques que las aglutinan. Tenemos las colecciones no genéricas (las que existen en .NET desde el comienzo de los tiempos) y las colecciones genéricas (que se agregaron después y que además suponen un salto cualitativo imposible de ignorar). Aunque a día de hoy mi percepción es que se utilizan mucho más las colecciones genéricas, vamos a comenzar por el principio y en este post veremos las colecciones no genéricas.

Se diferencia entre colecciones genéricas y no genéricas porque las colecciones genéricas son la solución al establecimiento inflexible de tipos.

 

Es decir, en una colección no genérica se puede guardar desde un Integer hasta un Boolean (porque no se permite acotar el tipo de los objetos que guarda la colección). Sin embargo, las colecciones genéricas permiten (y de hecho obligan) a que se declare cual es el tipo de los elementos que guardan. De este forma, si se declara una colección genérica para guardar objetos de tipo Boolean, no se podrán guardar objetos de tipo Integer (bien!!), lo que supone que en tiempo de ejecución evitaremos errores relacionados con la conversión de tipos (esperaba sólo guardar Boolean pero guardé un Integer por error) y además la colección no será penalizada constantemente con operaciones de tipo boxing y unboxing (el boxing es cuando se empaqueta un tipo por valor en un tipo por referencia, y el unboxing es cuando se desempaqueta desde un tipo por referencia a un tipo por valor, esta operación es muy costosa y sino te lo crees te dejo este enlace que lo explica perfectamente).


Las colecciones que veremos serán:

  • System.Array
  • System.Collections.ArrayList
  • System.Collections.BitArray
  • System.Collections.Hashtable
  • System.Collections.Queue
  • System.Collections.SortedList
  • System.Collections.Stack

Antes de comenzar con sus particularidades, es preciso conocer una serie de interfaces que expone el espacio de nombres System.Collection.

  • ICollection
  • IEnumerable
  • IList
  • IDictionary

Como vemos en el siguiente diagrama, ICollection hereda de IEnumerable, y a su vez, IList e IDictionary heredan de ICollection.

clip_image001

IEnumerable permite que una colección sea enumerable, es decir, que a través del método GetEnumerator (que devuelve un objeto de tipo IEnumerator) podamos iterar sobre los elementos de la colección. También habilita la instrucción For…Each  para la colección.

ICollection tan sólo establece que la colección debe tener un método Count para obtener el número de elementos que guarda y un método CopyTo para guardar el contenido actual de la colección en un objeto del tipo System.Array.

IEnumerable e ICollection son las únicas interfaces que todas las colecciones del espacio de nombres System.Collections implementan sin excepción.


Hasta aquí, IEnumerable e ICollection tan sólo han marcado unas sencillas pautas para las colecciones de .NET.

Es ya con IList e IDictionary donde podemos apreciar la dicotomía en la colecciones, es decir, a grandes rasgos existirán 2 tipos distintos de colección, las basadas en IList y las basadas en IDictionary.

IList establece que los elementos de la colección serán accedidos por índice, esto es así porque la propiedad Item es Property Item(index As Integer) As Object. Por otro lado, el método Add igualmente recibe un valor para agregar a la colección y devuelve un índice, el método RemoveAt nos permite eliminar un elemento por su índice, etc.

IDictionary permite guardar pares de clave-valor. Además, los elementos serán accedidos por clave. Si nos fijamos en la propiedad Item de IDictionary, vemos que es la siguiente: Property Item(key As Object) As Object.

Llegados a este punto y antes de comenzar a estudiar los detalles de cada colección, la pregunta es:

¿Qué colecciones tengo disponibles y de qué tipo son: IList o IDictionary, o lo que lo es lo mismo, son listas o diccionarios?

 

IList

IDictionary

System.Array

 

System.Collections.ArrayList

 

 

System.Collections.Hashtable

 

System.Collections.SortedList

 

Las colecciones BitArray, Queue y Stack no son ni listas ni diccionarios porque son colecciones que se especializan en un uso muy concreto, y como decíamos antes, sólo implementan ICollection e IEnumerable.


Después de esta pequeña introducción donde hemos abordado las interfaces (porque conociendo las interfaces también conoceremos las colecciones), ahora veremos cuáles son las principales características de cada colección:

Array

  • Tamaño fijo.
  • Implementa IEnumerable, ICollection, IList.

ArrayList

  • Su tamaño aumenta dinámicamente.
  • Implementa IEnumerable, ICollection, IList.

BitArray

  • Tamaño fijo.
  • Guardar valores de bit que se representan con el tipo Boolean.
  • Implementa IEnumerable, ICollection.

Hashtable

  • Pares de clave-valor organizados en función del código hash de la clave.
  • Implementa IEnumerable, ICollection, IDictionary.

Queue

  • Representa una cola (FIFO, first in first out, primero en entrar primero en salir).
  • Tienes métodos especializados para gestionar la cola:
    • Dequeue, quita y devuelve el primer elemento de la cola.
    • Enqueue, agrega un objeto al final de la cola.
    • Peek, devuelve el primer elemento de la cola, pero no lo elimina.
  • Implementa IEnumerable, ICollection.

SortedList

  • Pares de clave-valor ordenados por clave.
  • Implementa IEnumerable, ICollection, IDictionary.

Stack

  • Representa una pila (LIFO, last in first out, último en entrar primero en salir)
  • Tiene métodos especializados en gestionar la pila:
    • Peek, devuelve el objeto al principio de la pila, pero no lo elimina.
    • Pop, quita y devuelve el objeto al principio de la pila.
    • Push, inserta un objeto al comienzo de la pila.
  • Implementa ICollection, IEnumerable.

Antes dijimos que IEnumerable sólo tiene un método que devuelve un objeto de tipo IEnumerator. Al respecto cabe mencionar que para colecciones de tipo diccionario (IDictionary), el método implementado IEnumerator.GetEnumerator no devuelve un objeto de tipo IEnumerator, sino un objeto de tipo IDictionaryEnumerator (que hereda por otro parte de IEnumerator).

clip_image002

Por ejemplo, para enumerar colecciones basadas en IList bastaría con el siguiente código (más polimórfico, imposible):

        Dim gente As New ArrayList

        gente.Add("Sergio")

        gente.Add("Antonio")

        gente.Add("Dani")

        For Each nombre As Object In gente

            Console.WriteLine(nombre.ToString)

        Next

 

Si quisiéramos utilizar este código con una colección basada en IDictionary (por ejemplo, SortedList), funcionaría pero nos devolvería la siguiente salida:

clip_image003

Esto es porque SortedList.GetEnumerator devuelve un objeto de tipo IDictionaryEnumerator que itera sobre elementos del tipo DictionaryEntry (este es tipo que realmente las colecciones de tipo diccionario utilizan para guarda cada uno de los elementos que contienen, es por ello que este tipo las propiedades Key y Value). Siendo así, deberíamos utilizar este otro código:

        Dim gente As New SortedList

        gente.Add("S", "Sergio")

        gente.Add("A", "Antonio")

        gente.Add("D", "Dani")

        For Each persona As System.Collections.DictionaryEntry In gente

            Console.WriteLine(String.Format("Clave {0} Valor {1}", persona.Key, persona.Value))

        Next

        Console.ReadLine()

 

clip_image004

Sería fantástico acabar este post con una guía definitiva de cuando se debe utilizar una u otro colección, pero a excepción de las colecciones especializadas, que como su propio nombre indica resuelven una situación muy concreta y que cualquiera podría identificar (estamos hablando de Queue, Stack, BitArray), el resto de las colecciones son muy parecidas y el único guía-burros que me sale es el siguiente:

¿Necesitas guardar o acceder a los elementos por su clave? Si es sí, entonces clases basadas en IDictionary, si es no, entonces basadas en IList.

Si te has decantado por IDictionary (porque si lo has hecho por IList, casi sólo tienes disponible ArrayList) ¿Necesitas que los elementos de la colección estén ordenados? Si es sí, entonces SortedList, si es no, entonces Hashtable.

Como verás, nuestra toma de decisiones no es que sea muy científica, pero lo cierto es que con la aparición de las colecciones genéricas (que veremos en un siguiente post, ya puedes visitar el post de colecciones genéricas), casi todas estas decisiones ya no se evalúan (al menos en mi caso) porque casi siempre terminas decantándote por una colección genérica, como List(Of T) o Dictionary(Of TKey, TValue). Para el resto de las veces, me despacho a gusto con ArrayList.

En cualquier caso, queda fuera del ámbito de este post el explicar (básicamente porque yo tampoco lo sé) que colección es más eficiente y en qué contexto.

Simplemente quería que cuando alguien hable de colecciones no pongamos cara de otro ;-)

Un saludo!

1 comentario: