12.30.2010

IComparer<T>, IComparable<T> y el SortedList<TKey, TValue>

Revisando la implementación del SortedList y como alterar los criterios de comparación me di a la tarea de buscar alternativas para resolver un problema que tenía en un software en el que estaba trabajando.

Resulta que cuando uno utiliza un SortedList tiene dos opciones para manejar el ordenamiento de la misma: IComparable e IComparer. ¿Y cuál es la diferencia? Bueno desde el punto de vista desarrollo de aplicaciones 2:

  1. Tengo acceso al código de la clase que quiero ordenar.
  2. La clase esta en un componente a y no tengo acceso a su código.

IComparable<T>

En primera instancia, supongamos que tenemos acceso al código de la clase que queremos tener dentro de nuestro SortedList. En este caso utilizamos la interface IComparable<T> ya que el SortedList utiliza el método CompareTo de la implementación de IComparable<T> para realizar la ordenación; esta interface además es la que le da el servicio para ordenar al método Sort del Array. Para poder utilizar esta interface debemos implementarla en la clase que vamos a utilizar tal y como lo muestra el siguiente código.

public class Estudiante : IComparable<Estudiante>
{
public int Id { get; set; }
public string Nombre { get; set; }
public string Apellidos { get; set; }

public Estudiante( int pId, string pNombre, string pApellidos )
{
Id = pId;
Nombre = pNombre;
Apellidos = pApellidos;
}

public int CompareTo( Estudiante other )
{
return this.Apellidos.CompareTo( other.Apellidos );
}

}

Con esta implementación estamos haciendo que cuando se use la clase Estudiante en colecciones genéricas se ordene por los apellidos del estudiante. Si vemos la siguiente lista y la recorremos, veremos que los ítems fueron ordenados por medio de los apellidos.
SortedList<Estudiante, Estudiante> _listaEstudiantes = new SortedList<Estudiante, Estudiante>();

_listaEstudiantes.Add( _estudiante1, _estudiante1 );
_listaEstudiantes.Add( _estudiante2, _estudiante2 );
_listaEstudiantes.Add( _estudiante3, _estudiante3 );
_listaEstudiantes.Add( _estudiante4, _estudiante4 );
_listaEstudiantes.Add( _estudiante5, _estudiante5 );
_listaEstudiantes.Add( _estudiante6, _estudiante6 );
_listaEstudiantes.Add( _estudiante7, _estudiante7 );

foreach (KeyValuePair<Estudiante, Estudiante> item in _listaEstudiantes)
{
Console.WriteLine( "Apellidos: {0} | Nombre: {1}", item.Value.Apellidos, item.Value.Nombre );
}
Al ejecutar este código el resultado será el siguiente:
image

IComparer<T>


Entonces cual es el uso que le vamos a dar a IComparer<T>. Bueno, en realidad lo que nos da es un mecanismo extra de comparación para ordenar la lista. Este mecanismo es útil cuando por ejemplo queremos utilizar una lista ordenada para la clase pero no tenemos el código de la clase y por lo tanto no podemos implementar la interface IComparable<T>. Supongamos la siguiente clase Estudiante:
public class Estudiante 
{
public int Id { get; set; }
public string Nombre { get; set; }
public string Apellidos { get; set; }

public Estudiante( int pId, string pNombre, string pApellidos )
{
Id = pId;
Nombre = pNombre;
Apellidos = pApellidos;
}

}

Supongamos que viene en un dll que no podemos modificar y sin embago la queremos ordenar por el criterio que nosotros deseamos. En este caso podemos utilizar la interface IComparer<T>. Lo que tenemos que hacer es crear una clase que implemente la interface IComparer<T> y establecer ahi la forma en que queremos hacer la comparación, en este caso por apellidos.

public class ComparadorEstudiante : IComparer<Estudiante>
{
public int Compare( Estudiante x, Estudiante y )
{
return x.Apellidos.CompareTo(y.Apellidos);
}
}
Seguidamente procedemos a crear la instancia de la clase SortedList<T> indicándole que utilice este comparador en el constructor.

SortedList<Estudiante, Estudiante> _estudiantes = new SortedList<Estudiante, Estudiante>( new ComparadorEstudiante() );

Estudiante _estudiante1 = new Estudiante( 1, "Ana", "Hernandez" );
Estudiante _estudiante2 = new Estudiante( 2, "Luis", "Ruiz" );
Estudiante _estudiante3 = new Estudiante( 3, "Esther", "Benavidez" );
Estudiante _estudiante4 = new Estudiante( 4, "Ulises", "Alonso" );
Estudiante _estudiante5 = new Estudiante( 5, "Ana", "Lopez" );
Estudiante _estudiante6 = new Estudiante( 6, "Mario", "Estrada" );
Estudiante _estudiante7 = new Estudiante( 7, "Clara", "Rodriguez" );

_estudiantes.Add( _estudiante1, _estudiante1 );
_estudiantes.Add( _estudiante2, _estudiante2 );
_estudiantes.Add( _estudiante3, _estudiante3 );
_estudiantes.Add( _estudiante4, _estudiante4 );
_estudiantes.Add( _estudiante5, _estudiante5 );
_estudiantes.Add( _estudiante6, _estudiante6 );
_estudiantes.Add( _estudiante7, _estudiante7 );

foreach (KeyValuePair<Estudiante, Estudiante> item in _estudiantes)
{
Console.WriteLine( "Apellidos: {0} | Nombre: {1}", item.Value.Apellidos, item.Value.Nombre );
}
Ahora la comparación se hará a través de la clase ComparadorEstudiante y el resultado será el siguiente:
image


Algunos podrán decir que para que tanta complicación si se podría pasar en el TKey del KeyValuePair el campo por el que quiero la llave y a partir de ahi que se genere la ordenación por el comparador por defecto del mismo, sin embargo; en el caso donde uno requiere este tipo de ordenación es principalmente cuando la clase que queremos ordenar tiene una relación de 1 a 1 con una clase X y es la que nos va a permtir hacer esta relación; un ejemplo típico es una clase proyecto asociada con una clase ProjectManager y queremos los proyectos ordenados por el project manager.


3 comentarios:

Anónimo dijo...

Es más rápido el ordenamiento utilizando el SortedList que usando una expresión OrderBy?

Luis D. Rojas dijo...

Hola, gracias por leer el blog.
Son cosas distintas, las listas ordenadas ya de facto estan ordenadas, entonces cada vez que se modifica, agrega o elimina un ítem de la lista se usa el comparador para mantenerla ordenada, osea esta siempre ordenada - se paga una penalidad por este feature - en cambio el order by - si es de linq - es una operación que se ejecuta en cada ejecución de la sentencia.

Anónimo dijo...

Gracias por la aclaración