Archivo

Posts Tagged ‘Colecciones’

Filtrando contenedores mediante boost::filter_iterator


Cuando creamos nuestro modelo de datos, a veces nos encontramos con que necesitamos crear clases que actúan como contenedores. En estos casos lo natural que expongamos una forma a través de la cual podamos acceder a los objetos contenidos. Pero la mayoría de las veces (a menos que se trate de un contenedor trivial) también querremos exponer una forma para filtrar el contenido por criterios definidos.

En el caso de que el contenedor sea trivial y sólo necesites acceder al contenido, nos basta entonces con usar alguno de los contenedores estándares que la librería estándar de C++ pone a nuestra disposición, como std::list o std::vector. Pero ¿y cuando queremos añadir filtros? Por supuesto que hay varias formas de hacerlo, pero eso lo veremos más adelante. Comencemos por armar nuestro ejemplo base.

Pensemos en una clase empleado que tiene algunos atributos: nombre, edad, salario y un puntero hacia el jefe (o nulo si no lo tiene); y un método que nos permita imprimir el nombre del empleado hacia la consola. Nothing too fancy:


class employee
{
    public:
        employee()
            : income(0.0), age(0), manager(nullptr)
        { }
        employee(const string& name, double income, int age, employee* manager)
            : name(name), income(income), age(age), manager(manager)
        { }
        employee(const employee& copy)
            : name(copy.name), income(copy.income), age(copy.age), 
              manager(copy.manager)
        { }

        string manager_name() const { 
            return manager != nullptr ? manager->name : "[Sin jefe]"; 
        }

        void print() const { cout << name << endl; }

        string name;
        double income;
        int age;
        employee* manager;
};

Nada del otro mundo. Ahora pensemos en cómo sería nuestra clase contenedora. La librería estándar de C++ nos provee los contenedores de la STL, como std::list o std::vector. Todos estos contenedores se basan en la idea de separar el acceso a los datos del algoritmo empleado para almacenarlos. En otras palabras, la forma en la que obtenemos los objetos no varía respecto de la forma en la que éstos se almacenan (en nodos, en bloques de memoria adyacentes, etc.).

La forma de acceder a los objetos se hace mediante iteradores. El concepto de un iterador es el de un puntero hacia el siguiente elemento de un contenedor (iterador de avance), hacia el siguiente y el anterior (iterador bidireccional), o hacia un elemento particular (iterador de acceso aleatorio). De esta forma, dado un elemento cualquiera dentro de la colección siempre será posible acceder al que le sigue, sin importar la forma en la que esté implementado el algoritmo interno.

Dado lo anterior, es una buena idea seguir el mismo patrón para nuestra clase contenedora. Aunque internamente usaremos un std::list para almacenar el contenido.



class catalog
{
    public:
        typedef list<employee> employee_list;
        typedef employee_list::size_type size_type;
        typedef employee_list::iterator iterator;
        typedef employee_list::const_iterator const_iterator;

    private:
        employee_list _employees;
        
    public:

        catalog() { }

        size_type size() const { return _employees.size(); }
        void clear() { _employees.clear(); }

        void add(const employee& emp) { _employees.push_back(emp); }
        void add(const string& name, double income, int age, employee* mgr) {
            add(employee(name, income, age, mgr));
        }
};

Bueno, sólo son algunos typedefs y algunos métodos comúnmente encontrados en este tipo de clases. Ahora bien, queremos exponer los elementos del catálogo, así que bien haríamos con añadir un método begin_all y end_all que nos devuelva el iterador inicial y el final.


        iterator begin_all() { return _employees.begin(); }
        iterator end() { return _employees.end(); }

Bueno, ahora sí. ¿Cómo le hacemos para añadir filtros? Por ejemplo, ¿cómo le hacemos si quisiéramos, de alguna forma, exponer a aquellos empleados cuyo nombre contiene alguna letra o cuya edad se encuentra en un rango determinado?

Una forma, por supuesto, sería crear un método similar a este:

...

void fill_by_age(list& lst, int min, int max)
{
    for (auto i = employees.begin(); i != employees.end(); ++i)
    {
        if (min <= age && i->age <= max)
            lst.push_back(*i);
    }
}
...

Aunque correcto, este código presenta ciertas deficiencias de diseño. Para empezar tenemos que pasar una referencia al contenedor donde se guardarán. Para seguir, tenemos que especificar el tipo de contenedor que usaremos (std::list). Si mañana quisiéramos cambiar de un list a un vector o a un map, rompería código ya escrito y habría que rehacerlo. Y por último, aquí estamos rompiendo la regla de separar implementación del acceso.

No hay mucho qué hacer para el primer problema. Pero para resolver el segundo, podríamos crear un método plantilla y que ahí se decida el tipo de contenedor. Y aun si obviáramos los problemas de estar manejando plantillas, no resolveríamos el tercer problema.

¿Qué podemos hacer? Bueno, una alternativa es regresar iteradores, similares al begin_all y end_all que hicimos hace rato. Pero en aquel momento simplemente pasamos los iteradores del contenedor interno (std::list). ¿Qué hacer ahora? ¿Crear nuestros propios iteradores, unos que hagan el filtro en automático?

Evidentemente es una opción, aunque es cierto que es una un tanto compleja y laboriosa. Y aquí el estándar de C++ nos deja sin opciones. Afortunadamente aquí es donde entra Boost. Esta biblioteca cuenta con una clase que nos ayuda, precísamente, a hacer esto que queremos. Estamos hablando de boost::filter_iterator<P, I>.

No voy a entrar en una discusión completa sobre esta clase (para eso, mejor revisar la documentación de Boost). Pero sí diré que es una clase que toma un predicado o functor (de tipo P), y un iterador de tipo I. La clase recorre el iterador, de inicio a fin, y para cada elemento ejecuta el predicado / functor en cuestión. Si éste regresa verdadero, filter_iterador añade el elemento a un iterador temporal. Si no, lo deja fuera. Luego, puede recorrerse este iterador temporal y todo lo que obtendremos serán los elementos filtrados.

¡Yay! ¡Suena como a algo que necesitamos! ¿Cómo lo implementamos?

Lo primero que necesitamos es un functor. Es decir, una clase con el operador () sobrecargado, y que tome como parámetro un objeto del contenedor, es decir, de tipo employee. Esta sobrecarga deberá regresar true para cuando queramos que el objeto pasado como parámetro sea incluido en el filtro. Así que podemos probar con un functor para filtrar la edad por un rango mínimo (ésto último pasados como parámetros al constructor del functor).

struct range_func
{
    range_func()
        : _min(0), _max(0)
    { }
    range_func(const range_func& copy)
        : _min(copy._min), _max(copy._max) 
    { }
    range_func(int min, int max)
        : _min(min), _max(max) 
    { }

    bool operator()(const employee& emp) const {
        return _min <= emp.age && emp.age <= _max;
    }

    int _min;
    int _max;
};

Sencillo, ¿no? Bueno, ahora lo que haremos es crear nuestro boost::filter_iterator para devolver un iterador de inicio y uno de fin. Para ello, usaremos la función boost::make_filter_iterator, en donde le pasaremos el functor ya creado y luego el iterador. Para el begin(), le pasamos begin() y end() de la lista, y para el end(), pasamos dos veces el end() de la lista. Algo así.

typedef filter_iterator range_filter_iterator;
...

range_filter_iterator begin_age_range(int min, int max)  {
	return begin_age_range(range_func(min, max));
}
		
range_filter_iterator begin_age_range(const range_func& func) {
	return make_filter_iterator<range_func>((func, 
        _employees.begin(), _employees.end());
}

range_filter_iterator end_age_range(int min, int max) {
    return end_age_range(range_func(min, max));
}

range_filter_iterator end_age_range(const range_func& func) {
    return make_filter_iterator<range_func>((func, 
        _employees.end(), _employees.end());
}

Un typedef al inicio para facilitar las cosas. Por lo demás, make_filter_iterator se encarga del resto. Ahora podemos usar nuestra clase de una forma similar a esta.

int main(int argc, char* argv[])
{
    catalog cat;
    employee fer("Fernando", 10000, 29, nullptr);
    cat.add(fer);
    cat.add("Gabriela", 13500, 26, &fer);
    employee jime("Jimena", 9500, 22, &fer);
    cat.add(jime);
    cat.add("Mario", 4300, 37, &jime);
    employee caty("Catalina", 15000, 31, nullptr);
    cat.add(caty);
    employee gis("Gisela", 14750, 30, &caty);
    cat.add(gis);
    cat.add("Omar", 12700, 30, &gis);

    auto func = [](const employee& emp) { 
        emp.print(); 
    };

    cout << "Edad entre 25 y 30" << endl;
    for_each(cat.begin_age_range(25, 30), cat.end_age_range(25, 30), func);
    cout << endl;

    ...
    return 0;
}

Como podemos ver, filter_iterator es una forma fácil de generar filtros y de paso resolver los tres problemas planteados hace un momento: no se expone el tipo del contenedor, y mantenemos separada la implementación.

Ya para terminar, dejo un programita de ejemplo completo: filtros por edad, nombre y jefe.



#include <iostream>
#include <string>
#include <list>
#include <functional>
#include <algorithm>
#include <boost/iterator/filter_iterator.hpp>

using std::string;
using std::cout;
using std::endl;
using std::list;
using std::function;
using std::for_each;
using std::shared_ptr;
using boost::filter_iterator;
using boost::make_filter_iterator;

class employee;

class employee
{
    public:
        employee()
            : income(0.0), age(0), manager(nullptr)
        { }
        employee(const string& name, double income, int age, employee* manager)
            : name(name), income(income), age(age), manager(manager)
        { }
        employee(const employee& copy)
            : name(copy.name), income(copy.income), age(copy.age), 
                manager(copy.manager)
        { }

        string manager_name() const { 
            return manager != nullptr ? manager->name : "[Sin jefe]"; 
        }

        void print() const { cout << name << endl; }

        string name;
        double income;
        int age;
        employee* manager;
};

struct range_func
{
    range_func()
        : _min(0), _max(0)
    { }
    range_func(const range_func& copy)
        : _min(copy._min), _max(copy._max) 
    { }
    range_func(int min, int max)
        : _min(min), _max(max) 
    { }

    bool operator()(const employee& emp) const {
        return _min <= emp.age && emp.age <= _max;
    }

    int _min;
    int _max;
};

struct substr_func
{
    substr_func() 
    { }
    substr_func(const string& str) 
        : _str(str)
    { }
    substr_func(const substr_func& copy)
        : _str(copy._str)
    { }

    bool operator()(const employee& emp) const {
        return emp.name.find(_str) != string::npos;
    }

    string _str;
};

struct mgr_func
{
    mgr_func()
        : _ptr(nullptr)
    { }
    mgr_func(employee* ptr)
        : _ptr(ptr)
    { }
    mgr_func(const mgr_func& copy)
        : _ptr(copy._ptr)
    { }

    bool operator()(const employee& emp) const {
        bool val = false;
        if (_ptr == nullptr)
            val = emp.manager == nullptr;
        else 
            val = emp.manager_name() == _ptr->name;
        return val;
    }

    employee* _ptr;
};


class catalog
{
    public:
        typedef list<employee> employee_list;
        typedef employee_list::size_type size_type;
        typedef employee_list::iterator iterator;
        typedef employee_list::const_iterator const_iterator;
        typedef filter_iterator<range_func, iterator> range_filter_iterator;
        typedef filter_iterator<substr_func, iterator> substr_filter_iterator;
        typedef filter_iterator<mgr_func, iterator> mgr_filter_iterator;

    private:
        employee_list _employees;
        
    public:

        catalog() { }

        size_type size() const { return _employees.size(); }
        void clear() { _employees.clear(); }

        void add(const employee& emp) { _employees.push_back(emp); }
        void add(const string& name, double income, int age, employee* mgr) {
            add(employee(name, income, age, mgr)); 
        }

        iterator begin_all() { return _employees.begin(); }
        iterator end() { return _employees.end(); }

        range_filter_iterator begin_age_range(int min, int max)  {
			return begin_age_range(range_func(min, max));
		}
		
		range_filter_iterator begin_age_range(const range_func& func) {
			return make_filter_iterator<range_func>(func, _employees.begin(), 
                _employees.end());
		}

        range_filter_iterator end_age_range(int min, int max) {
            return end_age_range(range_func(min, max));
        }

        range_filter_iterator end_age_range(const range_func& func) {
            return make_filter_iterator<range_func>(func, _employees.end(), 
                _employees.end());
        }

        substr_filter_iterator begin_substr(const string& value) {
            return begin_substr(substr_func(value));
        }

        substr_filter_iterator begin_substr(const substr_func& func) {
            return make_filter_iterator(func, _employees.begin(), 
                _employees.end());
        }

        substr_filter_iterator end_substr(const string& value) {
            return end_substr(substr_func(value));
        }

        substr_filter_iterator end_substr(const substr_func& func) {
            return make_filter_iterator(func, _employees.end(),
                 _employees.end());
        }

        mgr_filter_iterator begin_mgr(employee* mgr) {
            return begin_mgr(mgr_func(mgr));
        }

        mgr_filter_iterator begin_mgr(const mgr_func& func) {
            return make_filter_iterator(func, _employees.begin(), 
                _employees.end());
        }

        mgr_filter_iterator end_mgr(employee* mgr) {
            return end_mgr(mgr_func(mgr));
        }

        mgr_filter_iterator end_mgr(const mgr_func& func) {
            return make_filter_iterator(func, _employees.end(), 
                _employees.end());
        }
};

int main(int argc, char* argv[])
{
    catalog cat;
    employee fer("Fernando", 10000, 29, nullptr);
    cat.add(fer);
    cat.add("Gabriela", 13500, 26, &fer);
    employee jime("Jimena", 9500, 22, &fer);
    cat.add(jime);
    cat.add("Mario", 4300, 37, &jime);
    employee caty("Catalina", 15000, 31, nullptr);
    cat.add(caty);
    employee gis("Gisela", 14750, 30, &caty);
    cat.add(gis);
    cat.add("Omar", 12700, 30, &gis);

    auto func = [](const employee& emp) { 
        emp.print(); 
    };

    cout << "*** Todos los empleados *** " << endl;
    for_each(cat.begin_all(), cat.end(), func);
    cout << endl;

    cout << "***** POR EDAD *****" << endl;

    cout << "Edad entre 25 y 30" << endl;
    for_each(cat.begin_age_range(25, 30), cat.end_age_range(25, 30), func);
    cout << endl;

    cout << "Edad menor a 25" << endl;
    for_each(cat.begin_age_range(0, 25), cat.end_age_range(0, 25), func);
    cout << endl;

    cout << "Edad mayor a 30" << endl;
    for_each(cat.begin_age_range(30, 100), cat.end_age_range(30, 100), func);
    cout << endl;

    cout << "***** POR NOMBRE *****" << endl;

    cout << "Nombre que contenga 'na'" << endl;
    for_each(cat.begin_substr("na"), cat.end_substr("na"), func);
    cout << endl;

    cout << "Nombre que contenga 'ri'" << endl;
    for_each(cat.begin_substr("ri"), cat.end_substr("ri"), func);
    cout << endl;

    cout << "***** POR JEFE *****" << endl;

    cout << "Le reportan a Fernando" << endl;
    for_each(cat.begin_mgr(&fer), cat.end_mgr(&fer), func);
    cout << endl;

    cout << "Le reportan a Caty" << endl;
    for_each(cat.begin_mgr(&caty), cat.end_mgr(&caty), func);
    cout << endl;

    system("pause");
    return 0;
}


Y al ejecutarlo, tenemos este resultado:

*** Todos los empleados ***
Fernando
Gabriela
Jimena
Mario
Catalina
Gisela
Omar

***** POR EDAD *****
Edad entre 25 y 30
Fernando
Gabriela
Gisela
Omar

Edad menor a 25
Jimena

Edad mayor a 30
Mario
Catalina
Gisela
Omar

***** POR NOMBRE *****
Nombre que contenga 'na'
Fernando
Jimena
Catalina

Nombre que contenga 'ri'
Gabriela
Mario

***** POR JEFE *****
Le reportan a Fernando
Gabriela
Jimena

Le reportan a Caty
Gisela

Press any key to continue . . .

Ahora, intenta hacer algo similar sin functores, sino con lambdas… o mediante std::function. ¡Disfruta!

Categorías:Apunte, Boost, C++, Independiente Etiquetas: , ,

Filtrando colecciones con functores y lambdas


La librería estándar de C++ nos provee varias clases que nos permiten almacenar datos. Por ejemplo, std::list para listas, std::map para diccionarios, std::vector para elementos contíguos, etcétera. Todas estas clases de colecciones, también llamadas contenedores, están diseñadas para que su contenido pueda ser accedido mediante clases que enumeran cada elemento. A éstas se les llama iteradores. Los métodos begin y end nos proveen el primer y último iterador. Así, cualquier contenedor puede recorrerse de manera similar al siguiente ejemplo:

list<int> nums;
...

for (list<int>::iterator i = nums.begin(); i != nums.end(); ++i)
{
    cout << *i << endl;
}

O también podemos usar la nueva palabra reservada "auto":

list<int> nums;
...

for (auto i = nums.begin(); i != nums.end(); ++i)
{
    cout << *i << endl;
}

Por otra parte, recorrer un contenedor suele ser tan común que la librería estándar nos propone una función que toma tres parámetros: el iterador de inicio, el iterador de fin, y como tercer parámetro “algo” que tome un parámetro y ejecute una acción. Digo “algo” porque dado que es una plantilla, puede ser una función o, mejor aún, un functor. Un functor, antes que preguntes, es una clase que sobreescribe el operador () de tal suerte que puede invocarse como si fuera una función.

Para ejemplificar estos conceptos, me gustaría antes proponer una clase. Supongamos que vamos a trabajar con empleados. Creamos una clase similar a ésta.

class employee
{
    public:
        enum sex { sexMale, sexFemale };

        employee()
        {
            _age = 0;
            _gender = sexMale;
        }

        employee(const employee& copy)
        {
            _name = copy._name;
            _age = copy._age;
            _gender = copy._gender;
        }

        employee(wstring name, sex gender, int age)
        {
            _name = name;
            _age = age;
            _gender = gender;
        }

        wstring _name;
        sex _gender;
        int _age;
        
        bool operator== (const employee& employee)
        {
            return _name == employee._name;
        }

        bool operator< (const employee& employee)
        {
            return _name < employee._name;
        }
};

Ahora, en nuestra función main, supongamos que creamos un contenedor de esta forma.

employee emp1(L"Fernando", employee::sexMale, 28);
employee emp2(L"Catalina", employee::sexFemale, 31);
employee emp3(L"Moisés", employee::sexMale, 31);
employee emp4(L"Emmanuel", employee::sexMale, 27);
employee emp5(L"Paula", employee::sexFemale, 18);
employee emp6(L"Pedro", employee::sexMale, 24);

list<employee> employees;
employees.push_back(emp1);
employees.push_back(emp2);
employees.push_back(emp3);
employees.push_back(emp4);
employees.push_back(emp5);
employees.push_back(emp6);

Bueno bueno. Luego entonces, decíamos que podemos usar functores. Por ejemplo, creemos uno para imprimir todo el contenido de la lista.

class print_employee
{
    public:
        print_employee()
        {
            _print_all = false;
        }

        print_employee(bool print_all)
        {
            _print_all = print_all;
        }

        void operator()(const employee& employee)
        {
            wcout << employee._name << endl;
            if (_print_all)
            {
                wcout << L"Gender: "  
                    << (employee._gender == employee::sexMale ? L"Male" : L"Female") 
                    << endl;
                wcout << L"Age: " << employee._age << endl;
                wcout << L"*****\n" << endl;
            }
        }

        bool _print_all;
};

 

Ahora sí, usar la función for_each de esta forma.

print_employee print(false);
for_each(employees.begin(), employees.end(), print);

Como puedes ver, print_employee es una clase functor: ve cómo sobrecargamos el operador (). De hecho, podrías invocar directamente print(empleado), como si fuera una función; eso es justamente lo que hace for_each.

Ahora bien, una de las tareas que frecuentemente tendremos que hacer es filtrar un contenedor. Por ejemplo, podríamos querer filtrar los empleados por género, por edad o por nombre. Pues bien, lo que haremos será crear una clase con un functor. Esta clase tendrá un constructor que tomará como parámetro una referencia hacia el contenedor donde guardaremos los empleados que cumplan la condición del filtro. Veamos.

class filter_employee
{
    public:
        filter_employee(list& results)
            : _results(results)
        {
            _by_age = false;
            _min_age = 0;
            _max_age = 0;
            _by_name = false;
        }

        void by_age(int min, int max)
        {
            _by_age = true;
            _min_age = min;
            _max_age = max;
        }

        void by_name(const wstring& name)
        {
            _by_name = true;
            _name = name;
        }

        void operator()(const employee& value)
        {
            bool found = false;
            if (_by_age && !found)
            {
                found = value._age >= _min_age && 
                        value._age <= _max_age;
            }

            if (_by_name && !found)
            {
                found = value._name.find(_name) != wstring::npos;
            }

            if (found)
                _results.push_back(value);
        }

        list& _results;

    private:
        bool _by_age;
        int _min_age;
        int _max_age;
        bool _by_name;
        wstring _name;
};

Si invocamos by_age entonces el functor filtrará por un rango de edades. Si invocamos by_name, entonces buscará una subcadena de texto. Ahora, podemos usar la clase así, para filtrar empleados entre 28 y 42 años:

list results;

filter_employee filter(results);
filter.by_age(28, 42);
for_each(employees.begin(), employees.end(), filter);
for_each(results.begin(), results.end(), print);

Del código anterior, la primera línea inicializa una lista de empleados, donde el functor guardará los resultados obtenidos. La tercera línea inicializa el functor y la cuarta establece que se filtre por edad entre 28 y 42. La quinta ejecuta el filtrado: el functor será llamado para cada elemento y sólo los que cumplan la condición se almacenarán en la lista results. Por último, invocamos el functor de impresión que creamos anteriormente y pasamos los iteradores de results.

Y así, Charlie Brown, es como podemos usar functores para realizar filtros sobre colecciones. La verdad es que for_each nos ayuda bastante, aunque es cierto que crear una clase functor puede resultar tedioso. En los ejemplos anteriores, los functores son clases un poco más complicadas ya que permiten realizar ciertas configuraciones (al menos con filter_employee). Habrá casos, por supuesto, en los que no valga la pena crear un functor. Por ejemplo, print_employee sale sobrando. Para estas situaciones podemos usar lambdas.

Los lambdas son funciones anónimas, introducidas en C++ a partir del estándar C++11 (es decir, el del 2011). Aunque son recientes, afortunadamente Visual C++ 2010 ya las implementa, por lo que podemos hacer uso de ellas (obvio, si usas VC++; aunque supongo que otros compiladores de C++ ya habrán implementado los lambdas).

El siguiente código muestra un lambda para imprimir el empleado, así como su uso junto con for_each.

auto print_func = [](const employee& value) { wcout << value._name << endl; };

for_each(employees.begin(), employees.end(), print_func);

Como puedes ver, print_func es una función lambda. Los corchetes [] así lo indican, seguido de los parámetros (en este caso, una referencia constante a un employee), seguido del cuerpo de la función: en este caso, la impresión hacia la consola del nombre. Por supuesto, los lambdas pueden ser más complejos: podemos crear una también para filtrar por nombre, como en el siguiente código.

auto filter_func = [&results](const employee& value) { 
    if (value._name.find(L"P") != wstring::npos)
        results.push_back(value);
};
auto print_func = [](const employee& value) { wcout << value._name << endl; };
for_each(employees.begin(), employees.end(), filter_func);
for_each(results.begin(), results.end(), print_func);

En la primera línea del código anterior declaramos nuestro lambda. Nota que en esta ocasión entre los corchetes hemos colocado “&results”. Recordamos que results es la lista donde almacenamos los resultados. Así, esa expresión significa: pasa una referencia hacia results como parámetro a la función lambda. Si no ponemos nada entre corchetes, no podemos hacer referencia hacia results. Si dejamos el ámperson nada más (i.e. [&](const employee& value)) entonces indicamos que todas las variables que se declaren fuera de la lambda sean asequibles por referencia. Si colocamos el nombre results sin el ámperson, entonces quiere decir que lo pasamos por valor.

Así, como puedes ver, podemos usar cualquiera de los dos enfoques para realizar filtrado sobre un contenedor. Por supuesto que hay otros, pero estos son los que mejor me han funcionado. Mi recomendación es que juegues un poco con el código anterior y practiques.

¡Suerte!

Categorías:Apunte, C++ Etiquetas: , ,

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