Inicio > .NET Framework, C#, Tutorial > Todo lo que siempre quisiste saber sobre colecciones pero tenías miedo de preguntar… Genéricos: lo mismo pero más barato

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.

Anuncios
Categorías:.NET Framework, C#, Tutorial Etiquetas: ,
  1. Aún no hay comentarios.
  1. No trackbacks yet.

Responder

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

Logo de WordPress.com

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

Imagen de Twitter

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

Foto de Facebook

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

Google+ photo

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

Conectando a %s