Archivo

Archive for the ‘Tutorial’ Category

Todo lo que siempre quisiste saber sobre colecciones pero tenías miedo de preguntar… Genéricos: lo mismo pero más barato


El mundo de .NET Framework 1.0 y 1.1 no era perfecto. Y es obvio: un esfuerzo monumental como .NET no podía quedar completo en la primera versión. Faltaba algo que muchos programadores de C++ extrañábamos: programación genérica. Este concepto estaba bien desarrollado en el mundo de C++: clases cuyo tipo de dato es parametrizable. Esto es especialmente útil en el mundo de las colecciones. Y es algo de lo que adolesció .NET en sus primeros días. Para darnos una idea, revisa el siguiente código.

#include <iostream>
#include <vector>
#include <string>

using std::cout;
using std::vector;
using std::string;
...

vector<int> ints;
ints.push_back(1);
ints.push_back(5);
ints.push_back(10);
for (vector<int>::iterator i = ints.begin(); i != ints.end(); ++i)
{
    cout << *i << endl;
}

vector<string> strs;
strs.push_back("Hola");
strs.push_back("mundo");
strs.push_back("C++");
for (vector<string>::iterator i = strs.begin(); i != strs.end(); ++i)
{
    cout << *i << " " << endl;
}

El código anterior es un pequeño ejemplo en C++ que muestra la clase genérica vector. Esta clase representa un arreglo dinámico, similar a ArrayList de .NET, con la salvedad de que vector acepta como parámetro el tipo de dato. Es decir, la funcionalidad de vector no necesariamente depende del tipo de dato (a final de cuentas, la funcionalidad de un arreglo, como añadir elementos e iterar sobre ellos, es la misma si empleamos enteros o cadenas de texto). Esto es lo que muestra el ejemplo: cómo usamos una clase con un tipo de dato entero y posteriormente la misma clase, pero con cadenas de texto (o una clase creada para trabajar con cadenas de texto, como es std::string). Podemos apreciar, pues, la versatilidad de la programación genérica, cuando trabajamos con colecciones.

En .NET 1.0 y 1.1 este concepto no existía. Lo más que podíamos utilizar eran clases que aceptaran tipos de dato object, y como en C# cualquier tipo de dato tiene su clase base en object, cualquier tipo de dato es aceptable. Un ejemplo de esto es ArrayList. Y de hecho, todas las colecciones y diccionarios que hemos visto hasta el momento. Pero esto acarrea dos problemas fundamentales.

El primero es que las colecciones no son fuertemente tipadas. Es decir, no hay forma de forzar a ArrayList, Stack, Queue, Hashtable o a OrderedDictionary para que acepten un tipo de dato en particular. Uno puede añadirles enteros, decimales, cadenas de texto y no hay forma de prevenir esto. La alternativa para estos casos era derivar de CollectionBase o DictionaryBase e implementar los métodos que requerían el tipo de dato (como Add, Remove, IndexOf, etc) de forma manual (las cuales, por cierto, utilizan un ArrayList / Hashtable de forma interna), o bien sobreescribir algún método como OnInsert y OnSet y ahí revisar que el objeto insertado tenga el tipo de dato deseado (y si no, lanzar una excepción).

Pero aún haciendo esto, no hay forma de evitar el segundo problema: el boxing y unboxing de tipos de datos valor (int, decimal, Point, y en general cualquier estructura). El concepto de boxing y unboxing es el siguiente. Las estructuras (struct int, por ejemplo) son tipos de datos que se pasan por valor. Es decir, si tenemos un método que acepta una estructura, al invocarlo y pasarle el parámetro lo que sucede es que se crea una copia del objeto. Con los tipos de dato referencia (cualquier clase) esto no pasa. Sin embargo, todos los objetos, sean tipo referencia o tipo valor, heredan de object, que es un tipo referencia.

public void foo(object o)
{
    Console.WriteLine(o.ToString());
}
...

foo("Hola mundo");
foo(15);

 

En el código anterior, el método foo recibe cualquier objeto. Lo invocamos con una cadena de texto y con un entero. Pero como int es un tipo valor y object un tipo referencia, el runtime de .NET crea por abajo del agua un objeto que “envuelve” al tipo valor para que pueda ser tratado como tipo referencia. Esto es lo que se conoce como “boxing”. El proceso inverso, obtener un tipo valor a partir de un tipo referencia, se conoce como “unboxing”.

object obj = 15; //boxing: int (valor) a object (referencia)
int i = (int)obj; //unboxin: object (referencia) a int (valor)

 

Pero como habrás podido imaginar, el proceso de envolver/desenvolver tipos valor es un proceso que consume recursos, y de hecho hacer esto seguido puede causar daños en rendimiento a tu programa. Comprenderás ahora el problema de utilizar ArrayList (o cualquier colección de las que hemos visto) con tipos de dato valor. Y esto es algo que ni siquiera se soluciona heredando de CollectionBase/DictionaryBase. La única forma real de solventar esta situación sería crear una clase que implemente ICollection/IList/IDictionary y utilizar arrays, los cuales tendríamos que redimensionar manualmente. Y esto no es particularmente productivo.

La llegada de los genéricos en .NET 2.0 solucionó este problema. El poder usar genéricos nos evita los boxings y unboxings para las estructuras, y naturalmente, proporciona métodos y clases fuertemente tipadas. Podríamos reescribir el método foo de hace tres párrafos de la siguiente forma.

public void foo<T>(T t)
{
    Console.WriteLine(t.ToString());
}
...

foo("Hola mundo");
foo(15);

 

Así las cosas, a partir de .NET 2.0 se crearon muchas clases de colecciones genéricas, la mayoría bajo el espacio de nombres System.Collections.Generic. Algunas son resultado de portar directamente clases existentes. Otras a parte de portar añaden funcionalidad nueva. Otras son totalmente nuevas y añaden valor. En esta entrada veremos las versiones genéricas de colecciones que ya hemos visto.

Comencemos por las interfaces. En primer lugar, tenemos a IEnumerable<T>. Al igual que su contraparte IEnumerable, esta interfaz (que además hereda IEnumerable) define un método GetEnumerator, que regresa un IEnumerator<T>, la contraparte genérica de IEnumerator. De manera general, cualquier array además de implementar IEnumerable, implementa la versión genérica IEnumerable<T>, donde el tipo de dato T es el tipo de dato del arreglo.

string[] strs = new string[] { "Seiya", "Shiriu", "Yoga", "Shun", "Ikki" };
IEnumerable<string> strenum = strs;
foreach (string str in strenum)
    Console.WriteLine(str);

 

Luego tenemos ICollection<T>. Pero esta interfaz sí difiere de ICollection, salvo por las propiedades Count e IsReadOnly. En primer lugar, ICollection<T> define un método CopyTo, que a diferencia del de ICollection, éste es fuertemente tipado. Sin embargo, no implementa ni SyncRoot ni IsSynchronized. Y finalmente, ICollection<T> sí define cuatro métodos obvios para cualquier colección: Add, Clear, Contains y Remove, siendo (salvo Clear) métodos fuertemente tipados. Esto contrasta mucho con ICollection, pero la razón que hay detrás es que aquí sí sabemos el tipo de dato de la colección, de antemano, dado que éste es genérico y parametrizable. Así, ICollection<T> en realidad se parece más a IList que a ICollection.

Y hablando de listas, obvio que también tenemos IList<T>. Ésta interfaz, que hereda de ICollection<T>, solo define tres métodos más: IndexOf, Insert y RemoveAt, además de un indexador fuertemente tipado. Contrastándola con IList, muchos de los métodos de ésta ya están definidos en ICollection<T>. Recordemos que una de las razones por las que ICollection no define algunos métodos como Add, Remove e IndexOf es que éstos toman como parámetro un objeto cuyo tipo de dato sería el empleado en la colección. Dado que ICollection debe ser la interfaz base para todas las colecciones, los métodos anteriores sólo podrían tomar un parámetro de tipo object, y esto afectaría a las colecciones fuertemente tipadas (por ejemplo, aquellas que creásemos derivando de CollectionBase y DictionaryBase). Sin embargo, éste no es el caso con ICollection<T>, dado que esta interfaz es fuertemente tipada por definición.

Por supuesto, no podía faltar la interfaz IDictionary<K, V>, donde K define el tipo de dato de las llaves y V el de los valores. Comparándola con IDictionary, la versión genérica no implementa las propiedades IsFixedSize ni IsReadOnly. Pero añade algunos métodos, como ContainsKey (que regresa verdadero cuando una llave existe en el diccionario) y TryGetValue (que intenta obtener una llave existente y si no existe, regresa falso en lugar de lanzar una excepción).

Con respecto a las clases, la primera que tenemos es Queue<T>, contraparte de Queue. De forma similar, tenemos a Stack<T>, contraparte genérica de Stack. Ambas clases son símiles de sus versiones no genéricas, por lo que su comportamiento ya ha sido explicado. También una clase especializada, SortedList, tienen su versión genérica: SortedList<T>. Por otra parte, OrderedDictionary tiene una versión genérica similar: SortedDictionary<K, V>, que aunque no son iguales, son similares (SortedDictionary<K, V> mantiene al diccionario ordenado por la llave).

Una de las clases nuevas que nos encontramos es LinkedList<T>. Esta clase es una lista doblemente enlazada que realiza sus búsquedas internas recorriendo los nodos de la lista. En cierto sentido, se parece a ListDictionary, solo que aplicado a colecciones en lugar de diccionarios. LinkedList<T> contiene tres propiedades importantes: Count, First y Last. La primera se explica fácilmente. First y Last son dos propiedades de tipo LinkedListNode<T>, que representan el primer y el último nodo de la lista. Cada LinkedListNode<T> contiene una referencia a la lista enlazada a la que pertenece (List), una referencia al nodo anterior (Previous), la cual será nula si el nodo es la cabeza de la lista, una referencia al siguiente nodo (Next), igualmente nula si el nodo es la cola de la lista, y finalmente el valor del nodo actual (Value). El siguiente código muestra un pequeño ejemplo sobre cómo utilizar y recorrer esta clase.

LinkedList<string> list = new LinkedList<string>();
list.AddLast("Seiya");
list.AddLast("Shiriu");
list.AddLast("Yoga");
list.AddLast("Shun");
list.AddLast("Ikki");

 for (LinkedListNode<string> node = list.First; node != null; node = node.Next)
{
    string prev = node.Previous != null ? node.Previous.Value : "NULL";
    string next = node.Next != null ? node.Next.Value : "NULL";
    Console.WriteLine("[{0}] - {1} - [{2}]", prev, node.Value, next);
}

Pero quizás la clase más interesante de todas las genéricas sea List<T>. Esta clase en cierto sentido es la contraparte de ArrayList. Pero es mucho más. Añade muchísima funcionalidad (como ordenamiento y búsquedas binarias) y es por mucho una de las clases más empleadas en todo el .NET Framework. Así que vale la pena dedicarle especial atención. Pero antes de continuar, presento la definición de una clase sobre la cual estaremos trabajando en nuestros ejemplos.

enum ArmourType { Bronze, Silver, Gold }

class ZodiacKnight
{
    public string Name { get; set; }
    public string Sign { get; set; }
    public int Senses { get; set; }
    public ArmourType Type { get; set; }

    public ZodiacKnight(string name, string sign, ArmourType type, int senses)
    {
        Name = name;
        Sign = sign;
        Senses = senses;
        Type = type;
    }

    public ZodiacKnight(string name, string sign, ArmourType type)
        : this(name, sign, type, 6)
    {
    }

    public ZodiacKnight()
        : this(string.Empty, string.Empty, ArmourType.Bronze, 6)
    {
    }

    public override string ToString()
    {
        return string.Format("{0} - {1} - {2}", Name, Sign, Type);
    }
}

Ahora sí, continuemos. En primer lugar, List<T> cuenta con tres constructores. Los primeros dos deberían ya sernos familiares: el primero es el constructor por defecto y el segundo toma como parámetro la capacidad inicial de la lista. Recordemos que ciertas colecciones reservan memoria conforme la van necesitando, y este proceso puede ser costoso, por lo que si sabemos de antemano la capacidad inicial, la lista puede reservar suficiente espacio para no tener que estar redimensionando a cada rato. El tercer constructor toma como parámetro un IEnumerable<T> y añade cada elemento de la enumeración a la lista. Este constructor es particularmente útil, ya que nos permite inicializar la lista con una colección o inclusive una consulta hecha con LINQ.

ZodiacKnight[] knights = new ZodiacKnight[] {
    new ZodiacKnight("Seiya", "Pegasus", ArmourType.Bronze),
    new ZodiacKnight("Shiriu", "Dragon", ArmourType.Bronze),
    new ZodiacKnight("Yoga", "Cignus", ArmourType.Bronze),
    new ZodiacKnight("Shun", "Andromeda", ArmourType.Bronze),
    new ZodiacKnight("Ikki", "Phoenix", ArmourType.Bronze)
};
List<ZodiacKnight> list = new List<ZodiacKnight>(knights);
foreach (ZodiacKnight knight in list)
    Console.WriteLine(knight);
ZodiacKnight[] knights = new ZodiacKnight[] {
    new ZodiacKnight("Seiya", "Pegasus", ArmourType.Bronze),
    new ZodiacKnight("Shiriu", "Dragon", ArmourType.Bronze),
    new ZodiacKnight("Yoga", "Cignus", ArmourType.Bronze),
    new ZodiacKnight("Shun", "Andromeda", ArmourType.Bronze),
    new ZodiacKnight("Ikki", "Phoenix", ArmourType.Bronze)
};
List<ZodiacKnight> list = new List<ZodiacKnight>(knights);
foreach (ZodiacKnight knight in list)
    Console.WriteLine(knight);

List<T> implementa las interfaces IList<T>, ICollection<T>, IEnumerable<T>, así como IList, ICollection e IEnumerable. Por lo que es posible recorrer la lista usando un bucle foreach.

La lista expone dos propiedades familiares: Count, que nos regresa el número de elementos que contiene la lista, y Capacity, que nos regresa el número de elementos que puede tener la lista antes de que ocurra un redimensionamiento. Éste ocurrirá al momento en que Count > Capacity, o bien cuando cambiemos Capacity manualmente. También cuenta con un indexador que toma como parámetro un número y regresa el objeto cuyo índice dentro de la colección coincida con el parámetro. Ojo que si este índice es menor a cero o mayor o igual a Count se lanzará un ArgumentOutOfRangeException.

ZodiacKnight knight = list[3]; // devuelve a Shun

Por supuesto, tenemos los métodos tradicionales Add y AddRange para añadir uno o más elementos, respectivamente, al final de la colección; Insert e InsertRange para añadir uno o más elementos en un índice determinado; Remove y RemoveRange para eliminar uno o más elementos; RemoveAt para eliminar un elemento en un índice determinado; y Clear para borrar todos los elementos de la lista. Adicionalmente, tenemos RemoveAll, que toma como parámetro un predicado: una función que recibe como parámetro un elemento de la lista, y devuelve un valor booleano, el cual, de ser verdadero, hará que se elimine dicho elemento de la colección. En otras palabras, RemoveAll elimina de la lista todos los elementos para los cuales el predicado se evalúe a verdadero.

En el siguiente ejemplo se eliminan todos los elementos de la lista cuyo nombre comience con S.

ZodiacKnight[] knights = new ZodiacKnight[] {
    new ZodiacKnight("Seiya", "Pegasus", ArmourType.Bronze),
    new ZodiacKnight("Shiriu", "Dragon", ArmourType.Bronze),
    new ZodiacKnight("Yoga", "Cignus", ArmourType.Bronze),
    new ZodiacKnight("Shun", "Andromeda", ArmourType.Bronze),
    new ZodiacKnight("Ikki", "Phoenix", ArmourType.Bronze)
};
List<ZodiacKnight> list = new List<ZodiacKnight>(knights);

list.RemoveAll(x => x.Name.StartsWith("s", StringComparison.CurrentCultureIgnoreCase));

foreach (ZodiacKnight knight in list)
    Console.WriteLine(knight);

El predicado lo pasamos en la forma de una expresión lambda, pero también pudimos haber usado la expresión de delegados tradicional.

static void Main(string[] args)
{
    ZodiacKnight[] knights = new ZodiacKnight[] {
        new ZodiacKnight("Seiya", "Pegasus", ArmourType.Bronze),
        new ZodiacKnight("Shiriu", "Dragon", ArmourType.Bronze),
        new ZodiacKnight("Yoga", "Cignus", ArmourType.Bronze),
        new ZodiacKnight("Shun", "Andromeda", ArmourType.Bronze),
        new ZodiacKnight("Ikki", "Phoenix", ArmourType.Bronze)
    };
    List<ZodiacKnight> list = new List<ZodiacKnight>(knights);

    list.RemoveAll(new Predicate<ZodiacKnight>(StartsWithS));

    foreach (ZodiacKnight knight in list)
        Console.WriteLine(knight);

    Console.ReadKey(true);
}

static bool StartsWithS(ZodiacKnight x)
{
    return x.Name.StartsWith("s", StringComparison.CurrentCultureIgnoreCase);
}

La clase List<T> cuenta, asimismo, con varios métodos que permiten realizar búsquedas de un elemento. Por supuesto, cuenta con Contains, que nos dice si un elemento existe o no dentro de la colección. Un método similar es Exists, el cual nos permite pasarle un predicado que nos devuelve verdadero cuando éste regresa verdadero para algún elemento. El siguiente código muestra su uso, utilizando una hermosa expresión lambda.

bool exists = list.Exists(x => x.Name.Equals("Shiriu"));
Console.WriteLine("Shiriu existe: {0}", exists);

Pero Exists solo regresa verdadero o falso, no nos devuelve el elemento. En contraste, el método Find sí que lo hace si encuentra algún elemento, en caso contrario nos regresa default(T), que sería null para tipos referencia.

ZodiacKnight shiriu = list.Find(x => x.Name.Equals("Shiriu"));
Console.WriteLine(shiriu);

Es de notar que Find regresa el primer elemento cuyo predicado devuelva verdadero, inclusive si existieran más. En contraste, FindLast nos devuelve el último elemento.

ZodiacKnight item = list.FindLast(x => x.Name.StartsWith("S"));
Console.WriteLine(item); // se salta a Seiya y a Shiriu, e imprime a Shun

Si queremos obtener no el primero ni el último elemento que concuerde con el predicado, sino todos, entonces usamos FindAll. Este método devuelve una lista con todos los elementos que concuerden. Así, si queremos obtener la lista de todos los elementos cuyo nombre comience son S, haríamos algo así:

List<ZodiacKnight> items = list.FindAll(x => x.Name.StartsWith("S"));
foreach (var item in items)
    Console.WriteLine(item);

Para obtener un índice dado un elemento determinado, hacemos uso del tradicional IndexOf que ya conocemos de otras colecciones. Este método cuenta con ciertas sobrecargas que nos permiten buscar en un rango acotado. Incluso contamos con LastIndexOf para buscar la última ocurrencia. Si quisiéramos buscar un índice, podríamos usar Find (o alguna de las variantes que hemos visto) y hacer un IndexOf con el elemento devuelto. Afortunadamente, List<T> nos provee métodos que hacen eso por nosotros, incluso dándonos la oportunidad de buscar en un rango de índices acotado: FindIndex, FindLastIndex.

Pero eso no es todo: ¡List<T> nos permite realizar incluso búsquedas binarias! El método en cuestión se llama BinarySearch, y nos devuelve el índice del elemento encontrado. Hacer una búsqueda binaria, especialmente sobre listas grandes, nos da un mejor rendimiento puesto que el número de comparaciones que hace se reduce drásticamente. Una sobrecarga de BinarySearch nos permite pasar como parámetro un IComparer, para que podamos nosotros hacer nuestas comparaciones sobre nuestros tipos de datos personalizados.

class ZodiacKnightComparer : IComparer<ZodiacKnight>
{
    public int Compare(ZodiacKnight x, ZodiacKnight y)
    {
        if (x == null)
            throw new ArgumentNullException("x");
        if (y == null)
            throw new ArgumentNullException("y");

        return x.Name.CompareTo(y.Name); 
    }
}

class Program
{
    static void Main(string[] args)
    {
        ZodiacKnight[] knights = new ZodiacKnight[] {
            new ZodiacKnight("Seiya", "Pegasus", ArmourType.Bronze),
            new ZodiacKnight("Shiriu", "Dragon", ArmourType.Bronze),
            new ZodiacKnight("Yoga", "Cignus", ArmourType.Bronze),
            new ZodiacKnight("Shun", "Andromeda", ArmourType.Bronze),
            new ZodiacKnight("Ikki", "Phoenix", ArmourType.Bronze)
        };
        List<ZodiacKnight> list = new List<ZodiacKnight>(knights);

        ZodiacKnightComparer comparer = new ZodiacKnightComparer();
        list.Sort(comparer);
        
        ZodiacKnight yoga = list.Find(x => x.Name.Equals("Yoga"));
        int index = list.BinarySearch(yoga, comparer);
        Console.WriteLine("Índice para Yoga: {0}", index);

        Console.ReadKey(true);
    }
}

El código anterior muestra en primera instancia una clase llamada ZodiacKnightComaprer, que implementa IComparer. En este caso, queremos que las comparaciones se hagan por el nombre y eso es lo que hace el método CompareTo. El método Main de la clase Program crea la lista y luego el comparador. Posteriormente, mostramos cómo hacer uso del BinarySearch para obtener el índice de un elemento determinado.

Habrás notado, por supuesto, que hacemos una llamada al método Sort. Éste método hace lo que promete: ordena los elementos de la lista. Tuvimos que hacer uso de Sort porque como bien recordarás, una búsqueda binaria sólo funciona si la colección de elementos a sobre la cuál buscará está ordenada. En este caso, la lista se ordena basándose en el comparador que le pasamos como parámetro (y por ende, la lista será ordenada en base al nombre). Una sobrecarga sin parámetros existe, y ésta usa el comparador por defecto que le encuentre al tipo de dato (en este caso, ZodiacKnight). Otra sobrecarga nos permite usar un delegado de tipo Comparison<T>, en cuyo caso podríamos usar una expresión lambda similar a esta:

list.Sort((x, y) => x.Name.CompareTo(y.Name));

Bien, ahora pasemos a ver métodos que nos ayudan a ejecutar acciones y transformaciones sobre los elementos de la lista. El primer método de ésta categoría es uno de mis favoritos. Supongamos que tenemos nuestra lista y queremos hacer algo con cada elemento de ésta, digamos, imprimir los elementos en la consola. ¿Qué hacemos? Pues un bucle foreach:

ZodiacKnight[] knights = new ZodiacKnight[] {
    new ZodiacKnight("Seiya", "Pegasus", ArmourType.Bronze),
    new ZodiacKnight("Shiriu", "Dragon", ArmourType.Bronze),
    new ZodiacKnight("Yoga", "Cignus", ArmourType.Bronze),
    new ZodiacKnight("Shun", "Andromeda", ArmourType.Bronze),
    new ZodiacKnight("Ikki", "Phoenix", ArmourType.Bronze)
};
List<ZodiacKnight> list = new List<ZodiacKnight>(knights);
foreach (ZodiacKnight knight in list)
    Console.WriteLine(knight);

Pues qué tedioso hacer eso. ¿No sería mejor tener alguna forma de ejecutar una acción en una sola línea? ¡Ese método existe! Y su nombre es, sorpresa-sorpresa, ForEach. Éste método toma un delegado de tipo Action<T>, el cuál pasa como parámetro cada elemento de la lista. Entonces, podríamos reemplazar el foreach anterior por la llamada a ForEach, utilizando una siempre útil expresión lambda:

list.ForEach(x => Console.WriteLine(x));

La vida es bella. Otro método útil es ConvertAll. Éste método nos permite transformar los elementos de la lista en otra lista con un tipo de dato diferente, si se requiere. Por ejemplo, supongamos que queremos crear una lista de cadenas de texto con los nombres de nuestros caballeros. Llamamos a ConvertAll, que toma como parámetro un delegado de tipo Converter<T, TOutput>, donde T es el tipo de dato de la lista (en este caso ZodiacKnight) y TOutput es el tipo de dato nuevo que queremos (en este caso, string). Y nos regresará un objeto List<TOutput>. Hacerlo no podría ser más sencillo.

List<ZodiacKnight> list = new List<ZodiacKnight>(knights);
List<string> names = list.ConvertAll(x => x.Name);

names.ForEach(x => Console.WriteLine(x));

Hay, por supuesto, otros métodos, pero éstos dos son mis favoritos. Tenemos, por ejemplo, a Reverse, que invierte el órden en el que se encuentran los elementos; los ya conocidos ToArray y CopyTo para convertir la lista a un arreglo, o bien copiar los elementos hacia otro arreglo, respectivamente; TrimExcess, que elimina los elementos cuyo índice sea mayor al parámetro; y AsReadOnly, que nos devuelve una colección de solo lectura, esto es, a la que no se le pueden añadir, cambiar ni eliminar elementos (el tipo de dato es ReadOnlyCollection<T>, que exploraremos en otra entrada de la serie).

Existe uno, sin embargo, que tiene utilidad bajo un escenario común. Por ejemplo, supongamos que queremos ver si todos los elementos de nuestra lista cumplen cierta condición. Lo tradicional sería hacer un foreach, probar cada elemento, y si alguno es falso, romper el bucle (con break) y listo. Pues bien, List<T> nos ofrece el método TrueForAll que hace precísamente eso: le pasamos como parámetro un predicado. Nuevamente, nos simplificamos mucho la vida:

List<ZodiacKnight> list = new List<ZodiacKnight>(knights);
bool allBronze = list.TrueForAll(x => x.Type == ArmourType.Bronze);
bool allGold = list.TrueForAll(x => x.Type == ArmourType.Gold);

Console.WriteLine("Todos son de bronce: {0}", allBronze);
Console.WriteLine("Todos son de oro: {0}", allGold);

Y ya para finalizar esta eulogía a List<T>, la cereza en el pastel: esta clase implementa IEnumerable<T>. Esto ya lo habíamos dicho, pero quiero resaltarlo, porque esto hace que además de todas las maravillas que nos ofrece, podemos realizar consultas con LINQ.

List<ZodiacKnight> list = new List<ZodiacKnight>(knights);
var query = from knight in list
            where knight.Name.StartsWith("S")
            select knight.Name;
var newList = query.ToList();
newList.ForEach(x => Console.WriteLine(x));

Con respecto a diccionarios, contamos con una clase que los implementa a la perfección: Dictionary<TKey, TValue>, de donde TKey es el tipo de dato de la llave y TValue, el del valor. A diferencia de List<T>, Dictionary<TKey, TValue> no es tan sofisticada, pero es cumplidora: implementa IDictionary<TKey, TValue> tal cual. El siguiente ejemplo muestra cómo utilizarla.

Dictionary<string, ZodiacKnight> dic = new Dictionary<string, ZodiacKnight>();
dic.Add("Pegasus", new ZodiacKnight("Seiya", "Pegasus", ArmourType.Bronze));
dic.Add("Dragon", new ZodiacKnight("Shiriu", "Dragon", ArmourType.Bronze));
dic.Add("Cignus", new ZodiacKnight("Yoga", "Cignus", ArmourType.Bronze));
dic.Add("Andromeda", new ZodiacKnight("Shun", "Andromeda", ArmourType.Bronze));
dic.Add("Phoenix", new ZodiacKnight("Ikki", "Phoenix", ArmourType.Bronze));

Console.WriteLine("Existe Cignus: {0}", dic.ContainsKey("Cignus"));
Console.WriteLine("Andromeda: {0}", dic["Andromeda"]);
Console.WriteLine();
foreach (var pair in dic)
    Console.WriteLine("Llave: {0}, Valor: {1}", pair.Key, pair.Value);

Ahora sí, hemos llegado al final de esta entrada. Hemos visto las maravillas que hacen los genéricos y en particular cómo benefician a las colecciones. Vimos los equivalentes genéricos a ciertas colecciones como Stack y Queue, vimos algunas nuevas y sobre todo, hicimos un repaso a profunidad de la clase List<T>, y le echamos un vistazo a Dictionary<TKey, TValue>. Todavía queda más en esta serie, pero creo que por ahora hay mucho que repasar antes de continuar.

Aprovecho también para dedicarle este trabajo a alguien quien por casi veintiocho años fue fuente de inspiración para mí: mi abuelo, Pedro Gómez Mijares, quién tristemente nos dejó apenas la semana pasada. Finalmente, me gustaría desearles a todos una feliz Navidad. Con suerte publico algo antes de que acabe el mes… pero si no, pues feliz año nuevo y los mejores deseos siempre.

Categorías: .NET Framework, C#, Tutorial Etiquetas: ,

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


A lo largo de la serie hemos platicado sobre el funcionamiento general de colecciones (aquellas clases que implementan ICollection o IList) y diccionarios (aquellas clases que implementan IDictionary). Hemos visto también diferentes tipos de colecciones, como ArrayList, Stack, Queue, y diferentes tipos de diccionarios, como Hashtable y OrderedDictionary. Todas estas colecciones, que salieron en las primeras versiones de .NET, han sido diseñadas con propósitos muy particulares. Así, mientras ArrayList permite tener un arreglo de elementos dinámico con acceso aleatorio, Stack y Queue permite implementar patrones de primeras-entradas-primeras salidas y últimas-entradas-primeras-salidas, y ListDictionary e HybirdDictionary nos permiten optimizar el acceso a pares llave-valor basados en el número de elementos que contiene, entre muchos otros ejemplos, hay ocasiones en las que las clases existentes simplemente no son suficientes y no satisfacen los requerimientos que podemos necesitar bajo un cierto escenario. En estos casos, no nos queda de otra que crear nuestras propias colecciones.

Para hacer esto, no tenemos otra cosa que hacer más que crearnos una clase que implemente ICollection o IList, o bien IDictionary para los diccionarios. Hacer esto desde cero, sin embargo, puede ser una labor tediosa. En principio, implementar ICollection significa que tenemos que implementar a manita propiedades y métodos como IsSynchronized, IsReadOnly y CopyTo. Para implementar un IList, adicionalmente tendremos que implementar nuestros propios métodos Add, Remove y Clear, por citar algunos. Y ya no hablemos de implementar un diccionario: las propiedades Keys, Values vienene a la mente, además del enumerador (IDictionaryEnumerator). Implementar nuestras propias colecciones desde cero no solo es tedioso, sino además no es una tarea productiva.

Afortunadamente, cuando salió el .NET Framework, Microsoft pensó en estos escenarios. Y para ello, tenemos tres clases abstractas importantes, ubicadas en System.Collections: CollectionBase, ReadOnlyCollectionBase y DictionaryBase. Veamos.

CollectionBase

La primera clase que veremos es CollectionBase. Esta clase sirve de base para crear colecciones, e implementa las tres interfases importantes: IList, ICollection e IEnumerable. Esta clase cuenta con una colección interna en la cuál podemos guardar los elementos de la misma. Esta colección interna la podemos acceder mediante dos propiedades: List e InnerList, la primera de tipo IList y la segunda de tipo ArrayList. La diferencia entre ambas es que cuando usamos IList para agregar, modificar, remover y cambiar elementos, se disparan una serie de eventos que podemos utilizar para realizar validaciones extras, como veremos más adelante.

CollectionBase expone las siguientes propiedades y métodos públicos, naturales a cualquier colección: Capacity, Count, Clear, GetEnumerator y RemoveAt. Sin embargo, algunos otros métodos, que podríamos esperar en una colección como Add, AddRange, Remove, IndexOf y Contains no existen. Esto es por dos razones. La primera, que podamos controlar de qué forma agregamos los elementos. Por ejemplo, podríamos crear nuestro método Add que acepte una serie de parámetros a través del cuál construir el objeto deseado (útil cuando queremos implementar el patrón de diseño de fábrica de calses, por ejemplo). La segunda, que podamos proveer métodos fuertemente tipados. Es decir, si tengo mi clase Employee y creo EmployeeCollection derivada de CollectionBase, queremos que nuestro método Add acepte como parámetro un Employee en lugar de un object cualquiera. En la mayoría de los casos, simplemente crearemos los métodos fuertemente tipados y le pasaremos el control ya sea a InnerList o a List, según nos convenga. En otros casos quizás querramos realizar validaciones previas. Esto nos da la oportunidad de crear colecciones enteramente a nuestro antojo, sin tener que preocuparnos por la talacha.

Consideremos un ejemplo. Supongamos que tenemos una clase Employee sencilla.

class Employee
{
    public Employee(string name, string role, float salary)
    {
        Name = name;
        Role = role;
        Salary = salary;
    }

    public Employee()
        : this(string.Empty, string.Empty, 0f)
    {
    }

    public string Name { get; set; }
    public string Role { get; set; }
    public float Salary { get; set; }
}

Ahora queremos crear un EmployeeCollection. Nuestro cascarón quedaría así.

class EmployeeCollection : CollectionBase
{
    public EmployeeCollection()
        : base()
    {
    }
}

Vamos ahora, por partes, a implementar nuestra colección. Lo primero que nos viene en mente es que queremos contar con un método Add que nos acepte un Employee, pero también un método Add que nos acepte un nombre, el rol y el salario. Estos métodos lucirían de esta forma.

public void Add(Employee employee)
{
    List.Add(employee);
}

public Employee Add(string name, string role, float salary)
{
    Employee employee = new Employee(name, role, salary);
    Add(employee);

    return employee;
}

Como ves, toda la labor interna la realizamos a través de la propiedad List. Nota que pudimos emplear InnerList, pero en mi opinión siempre es mejor List, por la notificación de eventos que veremos adelante.

Posteriormente, queremos implementar un método AddRange que nos permita agregar varios elementos a la vez. Digamos, un arreglo. Fácil:

public void AddRange(IEnumerable employees)
{
    if (employees == null)
        throw new ArgumentNullException("employees");

    foreach (Employee employee in employees)
        Add(employee);
}

Nota que pedimos como parámetro un IEnumerable. Dado que cualquier arreglo (como cualquier colección) implementa IEnumerable, podemos ahora agregar los elementos de cualquier colección.

Un método que no debe faltar es IndexOf. Este método nos permite obtener el índice de un elemento determinado. Añadamos una sobrecarga que tome como parámetro el nombre y nos regrese el índice del empleado cuyo nombre concuerde con el parámetro (podemos emplear Linq para hacer la búsqueda, o un simple bucle foreach).

public int IndexOf(Employee employee)
{
    return List.IndexOf(employee);
}

public int IndexOf(string name)
{
    var query = from Employee employee in this
                where employee.Name == name
                select employee;

    Employee foundEmployee = query.FirstOrDefault();
    int index = -1;
    if (foundEmployee != null)
        index = IndexOf(foundEmployee);

    return index;
}

Una vez implementados estos, podemos crear nuestros métodos Contains, como se muestran a continuación. El primero le delega la tarea a List, mientras que el segundo hace lo propio pero con IndexOf.

public bool Contains(Employee employee)
{
    return List.Contains(employee);
}

public bool Contains(string name)
{
    return IndexOf(name) >= 0;
}

Lo que sigue ahora es implementar un método Remove que tome como parámetro el Employee a remover. Pero también queremos dar la facilidad de eliminar un elemento por nombre. Easy-peacy.

public void Remove(Employee employee)
{
    List.Remove(employee);
}

public void Remove(string name)
{
    int index = IndexOf(name);
    if (index >= 0)
        RemoveAt(index);
}

No es necesario implementar un RemoveAt, que elimine por índice, ya que éste ya lo provee CollectionBase. Lo que sí queremos y no puede faltar, es un indexador que nos permita acceder a los elementos por su índice.

public Employee this[int index]
{
    get { return List[index] as Employee; }
    set { List[index] = value; }
}

Ahora bien, como decía anteriormente, CollectionBase dispara ciertos eventos (en la forma de métodos virtuales protegidos) cuando se añaden, eliminan o cambian elementos. La siguiente lista muestra estos métodos.

  • OnClear y OnClearComplete se invocan antes y después de ejecutar el vaciado de elementos, a través del método List.Clear.
  • OnInsert y OnInsertComplete se invocan antes y después de añadir un elemento, a través de List.Add y List.Insert.
  • OnRemove y OnRemoveComplete se invocan antes y después de eliminar un elemento de la colección, a través de List.Remove y List.RemoveAt.
  • OnSet y OnSetComplete se invocan antes y después de que se establece el valor de una colección, usualmente a través del indexador de List.
  • OnValidate se llama en diversas ocasiones (al insertar, remover o cambiar elementos, por ejemplo) y su finalidad es que podamos ejecutar validaciones genéricas sobre el elemento en cuestión.

Continuando con el ejemplo, supongamos que nuestro EmployeeCollection debe seguir ciertas reglas de negocio cuando agregamos elementos: no debe ser nulo, el objeto a agregar debe ser de tipo Employee, no puede estar repetido y no puede haber otro elemento que tenga el mismo nombre que el que se pretende añadir. Esto lo podemos agregar sobreescribiendo el método OnInsert y lanzando una excepción si las reglas no se cumplen.

protected override void OnInsert(int index, object value)
{
    if (value == null)
        throw new ArgumentNullException("value");
    if (index < 0 || index > Count)
        throw new IndexOutOfRangeException();

    Employee employee = value as Employee;
    if (employee == null)
        throw new InvalidCastException("The value must be of type Employee.");
    if (Contains(employee) || Contains(employee.Name))
        throw new ArgumentException("The employee already exists.");

    base.OnInsert(index, value);
}

Y ya para finalizar, añadamos un par de métodos que nos permita obtener los empleados por rol, así como aquellos que estén en un rango determiando de valores. Yo utilizo Linq, pero puedes emplear un bucle foreach junto con un yield return. Ahora sí que es a tu gusto.

public Employee[] GetBySalary(float min, float max)
{
    var query = from Employee employee in this
                where employee.Salary >= min && employee.Salary <= max
                select employee;

    return query.ToArray();
}

public Employee[] GetByRole(string role)
{
    var query = from Employee employee in this
                where employee.Role.Equals(role, StringComparison.CurrentCultureIgnoreCase)
                select employee;

    return query.ToArray();
}

Y listo, hemos creado nuestra primera colección personalizada. El siguiente código muestra todo el programa completo, así como un ejemplo de cómo podríamos usar nuestra nueva colección.

using System;
using System.Collections;
using System.Linq;

namespace Blogoso
{
    class Employee
    {
        public Employee(string name, string role, float salary)
        {
            Name = name;
            Role = role;
            Salary = salary;
        }

        public Employee()
            : this(string.Empty, string.Empty, 0f)
        {
        }

        public string Name { get; set; }
        public string Role { get; set; }
        public float Salary { get; set; }

        public override string ToString()
        {
            return string.Format("{0} - {1} - ${2}", Name, Role, Salary);
        }
    }

    class EmployeeCollection : CollectionBase
    {
        public EmployeeCollection()
            : base()
        {
        }

        public Employee this[int index]
        {
            get { return List[index] as Employee; }
            set { List[index] = value; }
        }

        public void Add(Employee employee)
        {
            List.Add(employee);
        }

        public Employee Add(string name, string role, float salary)
        {
            Employee employee = new Employee(name, role, salary);
            Add(employee);

            return employee;
        }

        public void AddRange(IEnumerable employees)
        {
            if (employees == null)
                throw new ArgumentNullException("employees");

            foreach (Employee employee in employees)
                Add(employee);
        }

        public void Remove(Employee employee)
        {
            List.Remove(employee);
        }

        public void Remove(string name)
        {
            int index = IndexOf(name);
            if (index >= 0)
                RemoveAt(index);
        }

        public int IndexOf(Employee employee)
        {
            return List.IndexOf(employee);
        }

        public int IndexOf(string name)
        {
            var query = from Employee employee in this
                        where employee.Name == name
                        select employee;

            Employee foundEmployee = query.FirstOrDefault();
            int index = -1;
            if (foundEmployee != null)
                index = IndexOf(foundEmployee);

            return index;
        }

        public bool Contains(Employee employee)
        {
            return List.Contains(employee);
        }

        public bool Contains(string name)
        {
            return IndexOf(name) >= 0;
        }

        public Employee[] GetBySalary(float min, float max)
        {
            var query = from Employee employee in this
                        where employee.Salary >= min && employee.Salary <= max
                        select employee;

            return query.ToArray();
        }

        public Employee[] GetByRole(string role)
        {
            var query = from Employee employee in this
                        where employee.Role.Equals(role, StringComparison.CurrentCultureIgnoreCase)
                        select employee;

            return query.ToArray();
        }

        protected override void OnInsert(int index, object value)
        {
            if (value == null)
                throw new ArgumentNullException("value");
            if (index < 0 || index > Count)
                throw new IndexOutOfRangeException();

            Employee employee = value as Employee;
            if (employee == null)
                throw new InvalidCastException("The value must be of type Employee.");
            if (Contains(employee) || Contains(employee.Name))
                throw new ArgumentException("The employee already exists.");

            base.OnInsert(index, value);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            EmployeeCollection employees = new EmployeeCollection();

            employees.Add(new Employee("Fernando", "Manager", 25000f));
            employees.Add("Catalina", "Manager", 30000f);

            Employee[] array = new Employee[] {
                new Employee("Antonio", "CEO", 40000f),
                new Employee("Moisés", "Diseñador", 20000f),
                new Employee("Joan", "Programador", 22000f),
                new Employee("Jorge", "Programador", 15000f)
            };
            employees.AddRange(array);

            foreach (Employee employee in employees)
                Console.WriteLine(employee);

            array = employees.GetByRole("Manager");
            foreach (Employee employee in array)
                Console.WriteLine(employee);

            array = employees.GetBySalary(20000f, 25000f);
            foreach (Employee employee in array)
                Console.WriteLine(employee);

            try {
                employees.Add("Fernando", "Manager", 25000f);
            } catch (Exception e) {
                Console.WriteLine(e.Message);
            }

            int index = employees.IndexOf("Catalina");
            Console.WriteLine("El índice para Catalina es {0}", index);

            bool contains = employees.Contains("Moisés");
            Console.WriteLine("El empleado Moisés existe: {0}", contains);

            Console.ReadKey(true);
        }
    }
}

ReadOnlyCollectionBase

Una clase muy similar a CollectionBase es ReadOnlyCollectionBase. La finalidad de ésta es que podamos crear colecciones de solo lectura. Es decir, que no podamos añadir o eliminar elementos, solo leerlos. Por ello ReadOnlyCollectionBase solo cuenta con la propiedad pública Count y el método GetEnumerator. Y cuenta además con InnerList, de tipo ArrayList. La ventaja de utilizar esta colección es que podemos agregar métodos útiles de búsqueda y ordenamiento (como GetByRole y GetBySalary del ejemplo anterior) además de otros tradicionales como IndexOf y Contains, lo cual nos da más funcionalidad del que nos provee un simple array.

El siguiente código muestra nuestra colección modificada para que sea de solo lectura. Obviamente métodos como Add, Remove y Clear ya no se implementan. Nota que el constructor acepta un arreglo de elementos, que serán los que contendrá la colección.

using System;
using System.Collections;
using System.Linq;

namespace Blogoso
{
    class Employee
    {
        public Employee(string name, string role, float salary)
        {
            Name = name;
            Role = role;
            Salary = salary;
        }

        public Employee()
            : this(string.Empty, string.Empty, 0f)
        {
        }

        public string Name { get; set; }
        public string Role { get; set; }
        public float Salary { get; set; }

        public override string ToString()
        {
            return string.Format("{0} - {1} - ${2}", Name, Role, Salary);
        }
    }

    class EmployeeCollection : ReadOnlyCollectionBase
    {
        public EmployeeCollection(IEnumerable employees)
            : base()
        {
            foreach (Employee employee in employees)
                InnerList.Add(employee);
        }

        public Employee this[int index]
        {
            get { return InnerList[index] as Employee; }
            set { InnerList[index] = value; }
        }

        public int IndexOf(Employee employee)
        {
            return InnerList.IndexOf(employee);
        }

        public int IndexOf(string name)
        {
            var query = from Employee employee in this
                        where employee.Name == name
                        select employee;

            Employee foundEmployee = query.FirstOrDefault();
            int index = -1;
            if (foundEmployee != null)
                index = IndexOf(foundEmployee);

            return index;
        }

        public bool Contains(Employee employee)
        {
            return InnerList.Contains(employee);
        }

        public bool Contains(string name)
        {
            return IndexOf(name) >= 0;
        }

        public Employee[] GetBySalary(float min, float max)
        {
            var query = from Employee employee in this
                        where employee.Salary >= min && employee.Salary <= max
                        select employee;

            return query.ToArray();
        }

        public Employee[] GetByRole(string role)
        {
            var query = from Employee employee in this
                        where employee.Role.Equals(role, StringComparison.CurrentCultureIgnoreCase)
                        select employee;

            return query.ToArray();
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Employee[] array = new Employee[] {
                new Employee("Fernando", "Manager", 25000f),
                new Employee("Catalina", "Manager", 30000f),
                new Employee("Antonio", "CEO", 40000f),
                new Employee("Moisés", "Diseñador", 20000f),
                new Employee("Joan", "Programador", 22000f),
                new Employee("Jorge", "Programador", 15000f)
            };
            EmployeeCollection employees = new EmployeeCollection(array);

            foreach (Employee employee in employees)
                Console.WriteLine(employee);

            array = employees.GetByRole("Manager");
            foreach (Employee employee in array)
                Console.WriteLine(employee);

            array = employees.GetBySalary(20000f, 25000f);
            foreach (Employee employee in array)
                Console.WriteLine(employee);

            int index = employees.IndexOf("Catalina");
            Console.WriteLine("El índice para Catalina es {0}", index);

            bool contains = employees.Contains("Moisés");
            Console.WriteLine("El empleado Moisés existe: {0}", contains);

            Console.ReadKey(true);
        }
    }
}

DictionaryBase

Hasta el momento hemos hablado de puras colecciones. Sin embargo, también existe una clase análoga a CollectionBase, solo que para diccionarios: DictionaryBase. La clase está estructurada de forma similar a su contraparte. Existen los miembros Count, Clear, CopyTo y GetEnumerator como públicos, así como los métodos para realizar validaciones(OnClear, OnInsert, OnGet, OnSet, OnRemove, OnValidate, etc.) solo que acomodados para poder recibir pares llave-valor. La diferencia más grande (aparte de que esta clase implementa IDictionary en lugar de IList) es que esta clase tiene dos propiedades públicas, Dictionary (de tipo IDictionary) e InnerHashtable (de tipo Hashtable), contrapertes de List e InnerList respectivamente. Ambas representan el diccionario subyacente de DictionaryBase.

class EmployeeCollection : DictionaryBase
{
    public EmployeeCollection()
        : base()
    {
    }

    public Employee this[int id]
    {
        get { return Dictionary[id] as Employee; }
        set { Dictionary[id] = value; }
    }

    public void Add(int id, Employee employee)
    {
        Dictionary.Add(id, employee);
    }

    public Employee Add(int id, string name, string role, float salary)
    {
        Employee employee = new Employee(name, role, salary);
        Add(id, employee);

        return employee;
    }

    public void Remove(int id)
    {
        Dictionary.Remove(id);
    }

    public bool Contains(int id)
    {
        return Dictionary.Contains(id);
    }

    public Employee[] GetBySalary(float min, float max)
    {
        var query = from Employee employee in Dictionary.Values
                    where employee.Salary >= min && employee.Salary <= max
                    select employee;

        return query.ToArray();
    }

    public Employee[] GetByRole(string role)
    {
        var query = from Employee employee in Dictionary.Values
                    where employee.Role.Equals(role, StringComparison.CurrentCultureIgnoreCase)
                    select employee;

        return query.ToArray();
    }

    protected override void  OnInsert(object key, object value)
    {
        if (value == null)
            throw new ArgumentNullException("value");

        int id = (int)key;
        if (Contains(id))
            throw new ArgumentException("El ID ya existe.");

        Employee employee = value as Employee;
        if (employee == null)
            throw new InvalidCastException("The value must be of type Employee.");
        
        base.OnInsert(id, employee);
    }
}

El código anterior muestra cómo podriamos crear un diccionario que asocie un ID numérico a un empleado. Como puedes ver, es muy similar a CollectionBase, con la diferencia (claro) de tratarse de un diccionario.

Finalmente…

Como puedes ver, las tres colecciones presentadas: CollectionBase, ReadOnlyCollectionBase y DictionaryBase nos proveen un punto de inicio para crear nuestras propias colecciones, y su finalidad es ahorrarnos trabajo. Por supuesto, hay otras opciones, como derivar de clases ya existentes (sobre todo las genéricas, de las cuales todavía no hemos hablado en esta serie). De hecho, con el advenimiento de los genéricos en .NET 2 (y colecciones como List<T>, Collection<T> y Dictionary<K, V>, las clases aquí presentadas han caído en desuso. Toma en cuenta, sin embargo, que estas clases salieron antes de los genéricos, y que en ese entonces proveían una gran ayuda. Incluso hoy en día lo son, cuando ninguna de las clases existentes satisfacen nuestras necesidades.

Y por cierto, ese será el tema de la siguiente entrada: los genéricos. Así que estate atento a esta serie. Misma hora, mismo canal.

Categorías: .NET Framework, C#, Tutorial Etiquetas:

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

noviembre 30, 2010 1 comentario

Hasta el momento, la serie de artículos dedicados a colecciones solo ha presentado un simple diccionario: Hashtable. Como habíamos visto antes, éste diccionario agrupa los pares llave-valor basándose en el hash de la llave. Ahora bien, este tipo de ordenamientos suele ser eficiente, sobre todo cuando tenemos diccionarios enormes. Pero hay situaciones en las que Hashtable no es tan eficiente, o bien bajo ciertas circunstancias algun enfoque diferente puede ser mejor. En esta entrada vamos a analizar tres diccionarios que se encuentran dentro del espacio de nombres System.Collections.Specialized, y que ofrecen dichos enfoques para escenarios muy específicos: ListDictonary, HybridDictionary y OrderedDictionary.

Comencemos por estudiar ListDictionary. A primera vista, ListDictionary luce como una implementación normal de IDictionary, y de hecho así es. No añade métodos nuevos. Cuenta con los métodos y propiedades que implementan ICollection, como Count, IsSynchronized, SyncRoot, CopyTo; y también con propiedades y métodos propios de un IDictionary, como IsFixedSize, IsReadOnly, Keys, Values, Add, Clear, Contains, Remove y el indexador para acceder a los valores dada una llave; todos estos miembros que ya hemos explicado en entradas anteriores. De hecho, incluso contiene menos métodos que Hashtable. Entonces la pregunta es: ¿para qué nos sirve ListDictionary?

La respuesta es que mientras Hashtable almacena los pares llave-valor en bloques de memoria que son accesibles vía el código hash de la llave, ListDictionary los almacena como si fuera una lista enlazada. Es decir, el primer elemento contiene el par llave-valor, más un apuntador (o referencia, en el argot de C#) al elemento siguiente, y así sucesivamente hasta llegar al final de la lista. Esto implica que ListDictionary no manipula la memoria (o al menos, no de la forma en que Hashtable lo hace) y por lo tanto el acceso a los elementos es más eficiente en términos de memoria y procesamiento.

Sin embargo esto tiene su precio. Si recuerdas tus viejas clases de estructuras de datos que llevaste en la universidad, sabrás entonces que una lista enlazada tiene un mal rendimiento cuando intentamos acceder a los elementos de forma secuencial. De hecho, esta es la diferencia más grande que hay entre un array o vector (donde los elementos se almacenan en bloques de memoria contigua, haciendo posible el acceso secuencial) y la lista enlazada, donde se comienza a navegar por la cabeza de la lista, yendo de uno en uno hasta localizar el elemento que nos interesa. Luego entonces, el rendimiento de una lista (y por ende, de ListDictionary) al querer acceder a un elemento disminuye considerablemente conforme el número de elementos contenidos aumenta.

Para probar lo anterior, consideremos el siguiente código.

using System;
using System.Collections;
using System.Collections.Specialized;

namespace Blogoso
{
    class Program
    {
        static void Main(string[] args)
        {
            const int elements = 10;

            IDictionary dic = new ListDictionary();
            for (int i = 0; i < elements; i++)
            {
                string key = i.ToString();
                string val = string.Format("Cadena {0}", i);
                dic.Add(key, val);
            }

            DateTime start = DateTime.Now;
            for (int i = 0; i < elements; i++)
            {
                var key = i.ToString();
                var val = dic[key];
                Console.WriteLine("{0}: {1}", key, val);
            }

            TimeSpan duration = DateTime.Now - start;
            Console.WriteLine("Duración: {0}", duration);

            Console.ReadKey(true);
        }
    }
}

En este pequeño programita, creamos un ListDictionary (línea 13), le añadimos un número determinado de elementos (especificado en la constante definida en la línea 11) y posteriormente recorremos todo el diccionario y lo mostramos en la consola. Para hacer el ejemplo más interesante, tomamos el tiempo de inicio (línea 21) y el tiempo final (línea 29) y mostramos el tiempo que tardó el diccionario en obtener los valores. Si ejecutamos el programa, obtendríamos un resultado similar al siguiente (puede variar dependiendo de las características de la máquina que lo ejecute).

0: Cadena 0
1: Cadena 1
2: Cadena 2
3: Cadena 3
4: Cadena 4
5: Cadena 5
6: Cadena 6
7: Cadena 7
8: Cadena 8
9: Cadena 9
Duración: 00:00:00.0020001

Después de ejecutar el programa en repetidas ocasiones, el tiempo empleado suele estar entre 2 y 3 milésimas de segundo. Ahora bien, si en lugar de diez elementos cambiamos la línea 11 para que sean mil, el tiempo de ejecución me aparece entre 186 y 199 milésimas de segundo. Para comparar, cambiemos la línea 13 para que en lugar de un ListDictionary creemos un Hashtable:

IDictionary dic = new Hashtable();

Si añadimos diez elementos, el tiempo promedio se encuentra entre 3 y 4 milésimas de segundo, un pequeño aumento con respecto a ListDictionary. Sin embargo, al añadir mil elementos, obtengo un tiempo promedio de entre los 155 y 170 milésimas. Una baja sensible con respecto a ListDictionary. Repitiendo el ejercicio pero ahora con 10,000 elementos, tenemos que mientras Hashtable me da tiempos de entre 800 milésimas y 1.06 segundos, ListDictionary me da tiempos de entre 2.9 y 3.2 segundos.

Este pequeño experimento nos permite concluir que ListDictionary es más eficiente que Hashtable cuando nuestro conjunto de datos es pequeño, pero que conforme dicho conjunto crece, el rendimiento comienza a bajar considerablemente. De hecho, el último ejercicio con diez mil elementos mostró que el rendimiento de Hashtable con respecto a ListDictionary puede ser entre 200% y 300% mayor.

Luego entonces tenemos la premisa de ListDictionary: es un diccionario optimizado para trabajar con pocos elementos (la documentación dice que no más de diez), de tal suerte que si el número de elementos es grande, es preferible utilizar otro tipo de diccionario, pero que si el número permanece bajo se obtendrán mejores resultados con ListDictionary.

Y esto precísamente nos lleva al segundo diccionario especializado a tratar. Supongamos que tenemos un escenario en el cual tenemos un diccionario que generalmente tiene pocos elementos. Pero que bajo ciertas circunstancias, éste puede crecer. Para no perder el rendimiento, tendríamos que usar ListDictionary y cuando éste crezca, copiar todos los elementos a un Hashtable. Pues bien, hay una colección que hace precísamente eso: HybridDictionary. Esta clase emplea de forma interna un ListDictionary cuando el número de elementos se mantiene bajo, pero que cambia a Hashtable cuando el número crece.

Al igual que ListDictionary, HybridDictionary implementa las mismas propiedades y métodos que definen ICollection e IDictionary, con la salvedad que el constructor nos permite especificar el número de elementos que contendrá la colección de forma inicial. Pero evidentemente la fortaleza de la clase radica en las estrategias utilizadas para optimizar el acceso a los pares llave-valor. Para mostrar esto, tomemos el ejercicio que habíamos practicado anteriormente y modifiquémoslo para utilizar HybridDictionary.

using System;
using System.Collections;
using System.Collections.Specialized;

namespace Blogoso
{
    class Program
    {
        static void Main(string[] args)
        {
            const int elements = 10;

            IDictionary dic = new HybridDictionary();
            for (int i = 0; i < elements; i++)
            {
                string key = i.ToString();
                string val = string.Format("Cadena {0}", i);
                dic.Add(key, val);
            }

            DateTime start = DateTime.Now;
            for (int i = 0; i < elements; i++)
            {
                var key = i.ToString();
                var val = dic[key];
                Console.WriteLine("{0}: {1}", key, val);
            }

            TimeSpan duration = DateTime.Now - start;
            Console.WriteLine("Duración: {0}", duration);

            Console.ReadKey(true);
        }
    }
}

Cuando ejecuto en repetidas ocasiones este código, obtengo un tiempo promedio de entre 2 y 3 milésimas, similar al que obteníamos usando ListDictionary. Pero si cambiamos el número de elementos de diez a diez mil, en lugar de obtener el promedio de entre 2.9 y 3.2 segundos, obtenemos un promedio de 0.88 a 0.92 segundos, similar al que habíamos obtenido con Hashtable. Esto es así, en efecto, debido al comportamiento interno de HybridDictionary.

El tercer diccionario especializado que vamos a tratar, a diferencia de los dos anteriores, no se distingue de Hashtable o algún otro diccionario por la optimización hecha en base al número de elementos que contenga, sino más bien porque permite el acceso aleatorio a los elementos: es decir, a través de un índice. La clase se llama OrderedDictionary y, como su nombre lo sugiere, es un diccionario que mantiene ordenados los elementos basados en la llave de los mismos en bloques de memoria contigua, de forma muy similar a como los almacena un array o un vector. La principal ventaja de esto es que podemos acceder a los elementos no solamente a través de la llave, sino también a través de un índice.

using System;
using System.Collections;
using System.Collections.Specialized;

namespace Blogoso
{
    class Program
    {
        static void Main(string[] args)
        {
            OrderedDictionary dic = new OrderedDictionary();
            dic.Add("John", "Imagine");
            dic.Add("Ringo", "Photograph");
            dic.Add("Paul", "Another day");
            dic.Add("George", "My sweet lord");

            for (int i = 0; i < dic.Count; i++)
            {
                Console.WriteLine("{0} - {1}", i, dic[i]);
            }

            Console.ReadKey(true);
        }
    }
}

El ejemplo anterior muestra cómo podemos usar el indexador de OrderedDictionary para acceder por medio del índice. Esto es algo que no podemos lograr con Hashtable, y nos proporciona la comodidad de poder iterar sobre los elementos sin tener que conocer las llaves de antemano.

Un aspecto importante de esta clase es que, como hemos mencionado, internamente mantiene ordenados los elementos. Por lo mismo, es necesario poder contar con una forma de controlar el cómo comparar elementos, y es por ello que el constructor de OrderedDictionary admite una interfaz de tipo IEqualityComparer. Esta interfaz nos permite comparar dos objetos, además de obtener un código hash para alguno de ellos. A través de esto, podemos controlar cómo la colección ordena sus elementos. Por ejemplo, si las llaves son cadenas de texto (como en el ejemplo) podríamos crear una clase que implemente dicha interfaz para que las comparaciones se hicieran sin distinguir entre mayúsculas y minúsculas. Por ejemplo:

using System;
using System.Collections;
using System.Collections.Specialized;

namespace Blogoso
{
    class StringComparer : IEqualityComparer
    {
        public StringComparer()
        {
        }

        public bool Equals(object x, object y)
        {
            string xstr = x as string;
            string ystr = y as string;

            return xstr.Equals(ystr, StringComparison.InvariantCultureIgnoreCase);
        }

        public int GetHashCode(object obj)
        {
            string str = obj as string;
            str = str.ToUpper();

            return str.GetHashCode();
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            StringComparer comparer = new StringComparer();
            OrderedDictionary dic = new OrderedDictionary(comparer);
            dic.Add("John", "Imagine");
            dic.Add("Ringo", "Photograph");
            dic.Add("Paul", "Another day");
            dic.Add("George", "My sweet lord");

            for (int i = 0; i < dic.Count; i++)
            {
                Console.WriteLine("{0} - {1}", i, dic[i]);
            }

            Console.ReadKey(true);
        }
    }
}

La clase StringComparer que creamos compara dos cadenas de texto sin distinguir entre mayúsculas y minúsculas (método Equals), mientras que GetHashCode regresa el código hash de la cadena de texto en mayúsculas, de tal suerte que “John”, “JOHN” y “john” regresen el mismo código. Y ahora sí, inicializamos nuestro OrderedDictionary con una instancia de este comparador.

A lo largo de esta entrada analizamos los tres diccionarios especializados que nos ofrece .NET, cada uno con sus ventajas, virtudes y desventajas. Vimos cómo ListDictionary y HybridDictionary nos pueden ayudar a optimizar nuestro código, mientras que OrderedDictionary nos permite acceder a valores mediante un índice, sin necesidad de conocer las llaves de antemano. Así las cosas, la regla a seguir pudiera expresarse así. Si un diccionario tiene un número bajo de elementos, ListDictionary es la opción con mejor rendimiento. Si el diccionario tiene un número inicial bajo de elementos, pero es posible que éste crezca, la mejor opción es utilizar HybridDictionary. Por otra parte, si el número de elementos inicial es alto, es mejor utilizar Hashtable desde un principio. Finalmente, si tenemos un diccionario cuyas llaves no son conocidas, o en algún punto no se tiene alguna forma de inferirlas, podemos optar por emplear OrderedDictionary, que nos provee esta funcionalidad.

Después de leer esta entrada, pudieras pensar que a final de cuentas los tres diccionarios especializados son una pérdida de tiempo. Al fin y al cabo, Hashtable puede hacer todo el trabajo, y quizás una diferencia de dos segundos no parezca significativa para un programa de negocios tradicional. Pero estas tres clases muestran la riqueza del .NET Framework, dado que proveen opciones para escenarios muy específicos. Y ciertamente, vale la pena, creo, conocerlos para poder sacarles provecho cuando nos encontremos en una situación similar.

Ahora que por supuesto, estos tres diccionarios no son los únicos que hay. En entradas futuras veremos otros tipos de diccionarios diseñados para otras situaciones específicas, pero también algunos de propósito general. No te pierdas las siguientes entradas en la serie, para conocer más sobre este fascinante y amplio tema de las colecciones en .NET.

Categorías: .NET Framework, C#, Tutorial Etiquetas:

Todo lo que siempre quisiste saber sobre colecciones y tenías miedo de preguntar… Algunas colecciones para trabajar con texto


A lo largo de esta serie “Todo lo que siempre quisiste saber sobre colecciones y tenías miedo de preguntar” hemos tratado con implementaciones de colecciones y diccionarios que vieron la luz del día en la primera versión del .NET Framework, cuando todavía no había clases genéricas. En aquella época, la norma era que cuando se necesitaba una colección fuertemente tipada el programador creaba una clase que implementara ICollection o IDictionary, internamente utilizara un ArrayList o un Hashtable, y actuara como envoltorio sobre ésta. Por supuesto, con la llegada de Generics en .NET 2.0, ésta práctica comenzó a caer en desuso (por ejemplo, ahora es más común derivar de List<T>, de Collection<T> o Dictionary<TKey, TValue>, pero todavía falta para que veamos Generics en esta serie).

Sin embargo, los propios programadores de .NET crearon una serie de clases para este escenario, sobre todo que utilizaran mucho. Por ejemplo, la clase System.Web.HttpRequest tiene una propiedad llamada QueryString, que se utiliza para acceder a un diccionario en donde la llave y el valor son ambos cadenas de texto.

Así, en este apunte vamos a explorar algunas de las clases que podemos utilizar, en particular, para trabajar con cadenas de texto: StringCollection, StringDictionary y NameValueCollection, todas dentro del espacio de nombres System.Collections.Specialized.

La primera clase es StringCollection. Como seguramente habrás inferido, ésta representa una colección de cadenas de texto, y está particularmente optimizada para trabajar con cadenas de texto. Recordemos que en .NET las cadenas de texto son inmutables, por lo que el empleo de éstas debe realizarse de forma cuidadosa para evitar caer en problemas de rendimiento.

Como cabría esperar, la colección cuenta con varias propiedades estándares, como SyncRoot e IsSynchronized para temas de concurrencia y sincronización en el acceso a elementos; Count para obtener el número de elementos que tiene la colección; IsReadOnly para saber si podemos agregar o eliminar elementos, y finalmente un indexador, a través del cuál podemos acceder a las cadenas de texto dado un índice determinado.

Además, también cuenta con los métodos tradicionales: Add y Remove para añadir o eliminar una cadena determinada; Insert para añadir una cadena en una determinada posición; AddRange para añadir un array de cadenas de texto de un solo golpe, y Clear para remover todas las cadenas existentes; CopyTo para copiar el contenido de la colección a un array unidimensional e IndexOf para obtener el índice de un elemento determinado (o –1 si no encuentra algo).

Realmente es una colección sencilla, y su razón de ser es precisamente ser una colección fuertemente tipada. El siguiente programita muestra un sencillo ejemplo sobre el uso de StringCollection.

using System;
using System.Collections.Specialized;
using System.Linq;

namespace Fermasmas.Wordpress.Com
{
    class Program
    {
        static void Main(string[] args)
        {
            StringCollection strings = new StringCollection();
            strings.Add("Jetzt laden die Vampire zum Tanz");

            string[] strs = new string[] {
                "Steckt den Himmel im Brand",
                "und streut Luzifer Rosen!",
                "die Welt gehört den Lüngern",
                "und den Rücksichtlosen"
            };
            strings.AddRange(strs);
            strings.Insert(1, "Wir wollen alles und Ganz!");

            if (strings.Contains("Jetzt laden die Vampire zum Tanz"))
            {
                int index = strings.IndexOf("Jetzt laden die Vampire zum Tanz");
                Console.WriteLine(strings[index]);
            }

            foreach (string str in strings)
                Console.WriteLine(str);
            Console.WriteLine();

            var query = from string str in strings
                        where str.Contains("und")
                        select str;
            foreach (string str in query)
                Console.WriteLine(str);

            strings.Clear();

            Console.ReadKey(true);
        }
    }
}

StringCollection no cuenta con métodos sofisticados para realizar búsquedas. Sin embargo, nada nos detiene de utilizar algún método en particular, digamos LINQ, como se muestra en las líneas 33 a 37 del ejemplo anterior. A continuación, la salida del programa.

Jetzt laden die Vampire zum Tanz
Jetzt laden die Vampire zum Tanz
Wir wollen alles und Ganz!
Steckt den Himmel im Brand
und streut Luzifer Rosen!
die Welt gehört den Lüngern
und den Rücksichtlosen

Wir wollen alles und Ganz!
und streut Luzifer Rosen!
und den Rücksichtlosen

La segunda clase que me gustaría mostrar es StringDictionary. Así como StringCollection es una implementación similar a ArrayList pero fuertemente tipada, StringDictionary implementa un Hashtable pero fuertemente tipado para emplear cadenas de texto.

Por ende, podemos encontrar métodos similares a los que tenemos en StringCollection: Add y Remove para añadir y quitar pares llave-valor del diccionario, ContainsKey y ContainsValue para saber si una llave o un valor están determinadas, y las propiedades Count, Keys y Values para contar el número de elementos, obtener todas las llaves y todos los valores, respectivamente.

using System;
using System.Collections.Specialized;
using System.Linq;
using System.Collections;

namespace Fermasmas.Wordpress.Com
{
    class Program
    {
        static void Main(string[] args)
        {
            StringDictionary dic = new StringDictionary();
            dic.Add("Chorus 1", "Steckt den Himmel in Brand");
            dic.Add("Chorus 2", "und streut Luzifer Rosen!");
            dic.Add("Chorus 3", "die Welt gehört den Lüngern");
            dic.Add("Chorus 4", "und den Rücksichtlosen");

            Console.WriteLine("Chorus 1: {0}", dic["Chorus 1"]);
            Console.WriteLine();

            foreach (DictionaryEntry entry in dic)
                Console.WriteLine("{0}: {1}", entry.Key, entry.Value);

            foreach (string key in dic.Keys)
                Console.WriteLine(key);

            Console.ReadKey(true);
        }
    }
}

El programa anterior genera esta salida.

Chorus 1: Steckt den Himmel in Brand

chorus 3: die Welt gehört den Lüngern
chorus 2: und streut Luzifer Rosen!
chorus 4: und den Rücksichtlosen
chorus 1: Steckt den Himmel in Brand
chorus 3
chorus 2
chorus 4
chorus 1

Nota que en la salida, cuando pintamos las llaves, nos regresa un valor en minúsculas, cuando nosotros las añadimos en mayúsculas. Esto es importante, dado que internamente StringDictionary no distingue entre mayúsculas y minúsculas para las llaves, una característica a tener en cuenta.

Finalmente, tenemos la colección NameValueCollection. En la práctica, ésta se comporta de forma muy similar a StringDictionary: nos permite agrupar pares llave-valor. Pero esta clase es una colección, no un diccionario, y ahí radica la principal diferencia. Por principio, NameValueCollection nos permite acceder a los elementos vía un índice, además de la llave. Además cuenta con constructores un poco más versátiles que nos permiten especificar el comparador (si es sensible a mayúsculas y minúsculas, por ejemplo).

using System;
using System.Collections.Specialized;
using System.Linq;
using System.Collections;

namespace Fermasmas.Wordpress.Com
{
    class Program
    {
        static void Main(string[] args)
        {
            NameValueCollection col = new NameValueCollection();
            col.Add("Chorus 1", "Steckt den Himmel in Brand");
            col.Add("Chorus 2", "und streut Luzifer Rosen!");
            col.Add("Chorus 3", "die Welt gehört den Lüngern");
            col.Add("Chorus 4", "und den Rücksichtlosen");

            Console.WriteLine("Chorus 1: {0}", col["Chorus 1"]);
            Console.WriteLine();

            foreach (string key in col.AllKeys)
                Console.WriteLine(key);
            Console.WriteLine();

            for (int i = 0; i < col.Count; i++)
                Console.WriteLine("{0}: {1}", col.GetKey(i), col.Get(i));

            Console.ReadKey(true);
        }
    }
}

Su salida:

Chorus 1: Steckt den Himmel in Brand

Chorus 1
Chorus 2
Chorus 3
Chorus 4

Chorus 1: Steckt den Himmel in Brand
Chorus 2: und streut Luzifer Rosen!
Chorus 3: die Welt gehört den Lüngern
Chorus 4: und den Rücksichtlosen

Notarás que la forma de emplear esta clase difiere un poquito de StringDictionary, pero se obtiene prácticamente el mismo resultado.

NameValueCollection se utiliza en muchos lugares del .NET Framework, como en los parámetros de consulta de una página web o bien para leer archivos de configuración. De ahí la importancia que tiene.

¿Cómo saber cuándo usar NameValueCollection y StringDictionary? En esencia, la respuesta se base en si necesitas tener una colección (ICollection) o un diccionario (IDictionary) respectivamente. Aunque se ha de reconocer que NameValueCollection es un poquitín más versátil de StringDictionary.

Ya para terminar, me gustaría que leyeras este pequeño artículo sobre cómo leer la configuración de la aplicación en un archivo web.config, que se encuentra en MSDN, para que veas una aplicación más de NameValueCollection.

Jetzt laden die Vampire zum Tanz, wir wollen alles und ganz!

Categorías: .NET Framework, C#, Tutorial Etiquetas: , ,

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


Para esta serie de “todo lo que siempre quisiste saber sobre colecciones y tenías miedo de preguntar” me he dado a la tarea de documentar y tratar de explicar las clases que se utilizan para las colecciones, con el propósito de que conozcamos todo lo que el .NET Framework tiene que darnos, en aras de evitar escribir código más estandarizado y robusto. En esta ocasión hablaré de una pequeña clase poco utilizada que bien nos puede salvar de problemas cuando tengamos que trabajar con muchos valores lógicos (booleanos): BitArray.

Esta clase, ubicada en el espacio de nombres System.Collections, nos permite manipular valores booleanos de forma fácil y sencilla. Esto podría no parecer de mucha utilidad, pero hay veces en los que necesitamos crear sistemas que manipulen el estado de un objeto (por ejemplo, una máquina de estados). Cada estado puede ser representado por un valor booleano, o bandera: verdadero o falso, prendido o apagado, etc. Por supuesto, lo primero que viene a la mente es crear una variable de tipo bool para cada bandera. Pero esto puede ser engorroso si tenemos muchas banderas: no solo hace nuestro manejo de banderas algo verboso, sino que hacer operaciones lógicas puede hacernos escribir grandes líneas de código. Es aquí donde BitArray puede sernos de utilidad.

Comencemos por exponer información sobre la clase. Un BitArray, en primer lugar, no permite agregar o añadir elementos: algo contra-intuitivo tratándose de una colección (BitArray implementa ICollection, después de todo). Pero piénsalo de esta forma: un conjunto de estados de un objeto determinado suele estar definido de antemano. En consecuencia, el tamaño del arreglo se determina en el constructor. Veamos algunos constructores.

BitArray bits1 = new BitArray(10);

BitArray bits2 = new BitArray(bits1);

boo[] boolArray = new bool[] { true, true, false, true, false, false, false };
BitArray bits3 = new BitArray(boolArray);

BitArray bits4 = new BitArray(10, true);

byte[] byteArray = new byte[] { 0xff, 0x00 };
BitArray bits5 = new BitArray(byteArray);

La primera llamada nos genera un array con diez bits. La segunda llamada nos genera un array el cual copia el tamaño y los bits contenidos en el primer array. El tercer arreglo se crea con siete bits de la forma 1101000, y la siguiente llamada nos genera un arreglo de diez bits, todos inicializados a 1. Finalmente, la quinta llamada nos genera un array de 16 bits, con los primeros ocho bits establecidos a 1 y los siguientes ocho establecidos a 0.

Como puedes ver, crear un BitArray no es nada complicado. Una vez creado, podemos emplear métodos útiles, de los cuales algunos se implementan de ICollection y otros nos permiten manipular los bits y hacer cálculos sobre ellos. Por ejemplo, Clone nos permite crear un nuevo BitArray copiando los valores del actual, mientras que CopyTo nos permite copiar los valores a un array cualquiera. Get y Set nos permiten leer y escribir un bit en determinada posición, mientras que SetAll establece todos los bits a 1 o 0.

Asimismo, BitArray cuenta con algunos métodos para hacer operaciones lógicas a nivel de bits. And y Or nos permite hacer una comparación lógica conjuntiva y disyuntiva, respectivamente, mientras que Not niega cada uno de los bits. Xor es similar a Or, pero es una disyunción excluyente. Nota que al hacer estas operaciones, se modifica el objeto actual con el resultado. Un ejemplo:

BitArray bits1 = new BitArray(new byte[] { 0xF });
var val = new bool[] { true, true, false, true, true, false, false, true })
BitArray bits2 = new BitArray(val);

bits1.Print();              // 11110000
bits2.Print();              // 11011001
bits1.And(bits2).Print();   // 11010000
bits1.Xor(bits2).Print();   // 00001001
bits1.Or(bits2).Print();    // 11011001
bits1.Not().Print();        // 00100110

El siguiente ejemplo muestra cómo podemos utilizar un BitArray para almacenar el estado de un objeto. Para este caso, creamos una clase llamada Order, que simula una orden de trabajo. Algunos métodos cambian el estado del mismo, por lo que podemos apreciar cómo va evolucionando.

using System;
using System.Collections;
using System.Text;

namespace Fermasmas.Wordpress.Com
{
    class Order
    {
        private BitArray _status;
        private string _number;

        public Order()
        {
            _number = string.Empty;
            _status = new BitArray(8, false);
            _status.Set(0, true);
        }

        public bool IsNew { get { return _status.Get(0); } }
        public bool IsDirty { get { return _status.Get(1); } }
        public bool IsDeleted { get { return _status.Get(2); } }
        public bool IsShipping { get { return _status.Get(3); } }
        public bool IsCanceled { get { return _status.Get(4); } }
        public bool IsComplete { get { return _status.Get(5); } }
        public bool IsError { get { return _status.Get(6); } }
        public bool IsClosed { get { return _status.Get(7); } }

        public override string ToString()
        {
            return new StringBuilder()
                .AppendFormat("New: {0}\n", IsNew)
                .AppendFormat("Dirty: {0}\n", IsDirty)
                .AppendFormat("Deleted: {0}\n", IsDeleted)
                .AppendFormat("Shipping: {0}\n", IsShipping)
                .AppendFormat("Canceled: {0}\n", IsCanceled)
                .AppendFormat("Complete: {0}\n", IsComplete)
                .AppendFormat("Error: {0}\n", IsError)
                .AppendFormat("Closed: {0}\n\n", IsClosed)
                .ToString();
        }

        public string Number
        {
            get { return _number; }
            set
            {
                _number = value;
                _status.Set(1, true);
            }
        }

        public void Delete()
        {
            _status.Set(2, true);
        }

        public void Save()
        {
            _status.Set(1, false);
        }

        public void ShipOrder()
        {
            _status.Set(3, true);
        }

        public void Cancel()
        {
            _status.Set(4, true);
        }

        public void Complete()
        {
            _status.Set(5, true);
        }

        public void ReportError()
        {
            _status.Set(6, true);
        }

        public void Close()
        {
            _status.Set(7, true);
        }

    }

    class Program
    {
        static void Main(string[] args)
        {
            Order order = new Order();
            Console.WriteLine(order);

            order.Number = "99901";
            Console.WriteLine(order);
            order.Save();
            order.ShipOrder();
            Console.WriteLine(order);
            order.ReportError();
            order.Cancel();
            order.Close();
            Console.WriteLine(order);

            Console.ReadKey(true);
        }
    }
}

La salida de este programa es la siguiente.

New: True
Dirty: False
Deleted: False
Shipping: False
Canceled: False
Complete: False
Error: False
Closed: False


New: True
Dirty: True
Deleted: False
Shipping: False
Canceled: False
Complete: False
Error: False
Closed: False


New: True
Dirty: False
Deleted: False
Shipping: True
Canceled: False
Complete: False
Error: False
Closed: False


New: True
Dirty: False
Deleted: False
Shipping: True
Canceled: True
Complete: False
Error: True
Closed: True

Como puedes ver, BitArray es una clase útil de conocer, aunque concedido, no siempre se utiliza. Pero también puedes ver que cuando trabajamos con muchos estados puede ser una forma más eficiente de solucionar el problema.

Eso es todo por el momento. Noi ci vediamo!

ADDENDUM

BitArray tiene una clase muy similar, hermana diría yo: BitVector32, ubicada en el espacio de nombres System.Collections.Specialized. Al igual que BitArray, esta clase se utiliza para guardar valores booleanos (banderas), pero con ciertas diferencias.

En primer lugar, BitVector32 es una estructura, y por tanto, un tipo-valor. Esto quiere decir que si yo asigno una instancia a una variable, dicha instancia será copiada. Y en segundo lugar, BitVector32 tiene un tamaño fijo: 32 bits. Esto la hace más eficiente en ciertos escenarios, dado que BitArray mantiene estados para hacer crecer el tamaño de bits que utiliza.

BitVector32 cuenta, además, con un método (estático) utilísimo cuando tratamos con banderas: CreateMask. Este método genera máscaras de bits en base a una máscara anterior. Por ejemplo, CreateMask() regresa un BitVector con la máscara 00000001. Si llamo otra vez a CreateMask, pasándole como parámetro la primera máscara, me regresa 00000010. Una nueva llamada en forma similar, regresaría 00000100. Y así sucesivamente.

No dejes de darle una revisada a la documentación en MSDN.

Categorías: .NET Framework, C#, Tutorial Etiquetas: ,