Archivo

Archive for 23 diciembre 2010

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: ,

Usa DateTimeControl para capturar fechas en SharePoint


En estos momentos me encuentro haciendo un WebPart para un proyecto de SharePoint (WSS 3.0). Me habían pedido que la captura de unos datos, de tipo fecha, se hiciera a través de controles que desplegaran la fecha, en lugar de un texto plano. Yo, cándidamente, puse un control de calendario. Cumple los requerimientos, pero tiene un no-sé-qué-qué-sé-yo que no me gusta. Muy… grande, digamos. 

Así que me dí a la tarea de investigar alternativas. La primera idea fue revisar cómo le hacía SharePoint, y explorando mis listas me encontré que utiliza una caja de texto seguida de un iconito de calendario, el cual al ser pulsado, despliega un bontio… ahm… calendario. Ejem. Mejor una imagen.

DateTimeControl1 

Y así las cosas, me dije a mí mismo: “Mí-mismo, seguro eso es un control”. Bueno, ya saben que a SharePoint le gusta encapsular todo dentro de controles web, una buena práctica a mi parecer. Así que me agarré el Reflector, abrí la librería Microsoft.SharePoint.dll y me puse a explorar el espacio de nombres Microsoft.SharePoint.WebControls. Y encontré la joya que buscaba.

El control en cuestión es DateTimeControl. La documentación existente apesta, como suele pasar con SharePoint. Pero en el caso de este control no hay mucho que averiguar, y en el peor de los casos nuestro buen amigo Reflector nos dice todo lo que tenemos que saber.

En primer lugar, la propiedad más importante y la más obvia es SelectedDate. Esta propiedad obtiene o establece la fecha que el calendario tenga seleccionada. MinDate y MaxDate establecen los rangos válidos para una fecha. Por supuesto, no podía faltar AutoPostBack para determinar si el control debe hacer un post back cuando haya un cambio en la fecha seleccionada.

Tambén existen algunas propiedades para controlar la forma en que se captura y muestra la fecha. Por ejemplo, ShowWeekNumber controla si se muestra o no el número de semana del año, DateOnly y TimeOnly indican si solo se mostrarán fechas o tiempo, HoursMode24 indica si el tiempo mostrado será en formato de 24 horas, etc.

En el caso de mi WebPart, mi código luce algo así:

protected override void CreateChildControls()
{
    try
    {            
        _startDateText = new DateTimeControl();
        _startDateText.ID = "_startDateText";
        _startDateText.DateOnly = true;
        _startDateText.SelectedDate = DateTime.Now;
        ...etc....
    }
    catch
    {
        ...etc....
    }
}

Y así es como se ve el WebPart en acción.

DateTimeControl2

¡Así sí gana la gente! Moraleja: siempre revisa los controles que SharePoint tiene antes de crear los tuyos propios… vale la pena, creeme.

Categorías:C#, SharePoint, Tips y trucos 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: