Inicio > .NET Framework, C#, Tutorial > Todo lo que siempre quisiste saber sobre colecciones y tenías miedo de preguntar… Colecciones y diccionarios base

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:
  1. Claudio Becerra
    febrero 17, 2011 a las 7:07 am

    tremendos aportes amigo!!!!!!!!

    Te estoy muy agradecido, estoy empesando en esto de las colecciones y tus tutoriales me sacaron de muchas dudas, aunque aun hay cosas que no tengo muy claras…

    pero bueno, sigue asi, estare atento a nuevas publicaciones…

    • febrero 18, 2011 a las 9:00 am

      ¡Muchas gracias! Si te quedas con duda, ¡pregunta! Puedo enriquecer los artículos con respuestas de preguntas que todos tengamos…

      Saludos.

  2. Claudio Becerra
    febrero 18, 2011 a las 9:17 am

    justamente tengo un problema con las colecciones…

    tengo unas clases con CollectionBase, cargan los datos y los paso a un gridview de esta forma…

    Public OSiniestradosList As New SiniestradosList()

    Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
    If Not IsPostBack Then
    ‘Dim OSiniestradosList As New SiniestradosList()
    OSiniestradosList.CargarTodos()
    Me.GVSiniestrados.DataSource = OSiniestradosList
    Me.GVSiniestrados.DataBind()
    End If
    End Sub

    hasta aca todo bien, el problema es que una vez que sale del Page_Load la variable OSiniestradosList pierde los datos y no logro entender el porque, se te ocurre alguna idea de como solucionar eso ?

    • febrero 18, 2011 a las 4:34 pm

      Hola Claudio. En primera instancia, pensaría que el problema no va relacionado con la lista. Es decir, en algún momento creas tu colección, y si estás en ASP.NET, apostaría a que tiene que ver con el hecho de que no la estás persistiendo. Es decir, cada que se carga la página regeneras la lista, pero no con las mismas condiciones que en el PostBack.

      Mi sugerencia: intenta guardar la colección en los objetos de sesión (Page.Session), a ver si con eso queda.

      Saludos.

  3. kacobp
    febrero 18, 2011 a las 5:40 pm

    mmmm, te agadeceria me pudieras comentar como hago eso de guardarlo en los objetos de sesión, que hace poco me estoy iniciando en .net y no tengo idea de como hacer lo que me comentas….

    gracias!!!!!!

  4. kacobp
    febrero 18, 2011 a las 6:05 pm

    otra cosa, si por casualidad tienes algun tutorial de como crear la persistencia te agradeceria mandaras el link…

    por cierto soy c. becerra…

    Gracias!!!!!!!!!! nuevamente….

  5. Claudio Becerra
    febrero 22, 2011 a las 6:10 am

    gracias por la info!!!!!
    modifique las clases y ahora no se pierden los datos…

  1. No trackbacks yet.

Responder

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

Logo de WordPress.com

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

Imagen de Twitter

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

Foto de Facebook

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

Google+ photo

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

Conectando a %s