CodeGym /Cursos /C# SELF /Interfaz IComparer<T&g...

Interfaz IComparer<T>

C# SELF
Nivel 30 , Lección 1
Disponible

1. Introducción

Imagina que eres el decano de una universidad con un montón de estudiantes. Necesitas listas ordenadas por diferentes criterios todo el tiempo:

  1. Por nombre (para encontrar rápido a un estudiante por orden alfabético).
  2. Por nota media (para dar becas a los mejores).
  3. Por edad (para estadísticas, concursos, etc.).
  4. Por curso, y dentro del curso — por apellido.

Si solo usáramos IComparable<T>, nuestra clase Estudiante tendría que implementar solo una forma de comparar. Por ejemplo, podríamos decidir que el "orden natural" del estudiante es por nota media. Genial, ¡List.Sort() ya funciona! Pero ¿y si queremos una lista ordenada por nombre? La clase Estudiante ya está "ocupada" comparando por nota. No puede tener dos "órdenes naturales" a la vez. Es como si tuvieras solo una instrucción de "cómo ser el mejor en boxeo", pero de repente necesitas ser el mejor en ajedrez y tratas de usar la instrucción de boxeo. No creo que te vaya muy bien, ¿verdad?

Justo para estos casos, cuando necesitas ordenar el mismo tipo de objetos de varias formas y no quieres meter la lógica de comparación en la propia clase, existe la interfaz IComparer<T>. Es útil si no tienes acceso al código fuente de la clase o si la clase no debería saber todos los modos de ordenación posibles.

2. Interfaz IComparer<T>

Si IComparable<T> es como el instinto interno del objeto, el saber cómo compararse él mismo con otro, entonces IComparer<T> es un juez externo, independiente que coge a dos jugadores (objetos) y los compara según sus propias reglas.

Imagina que eres el entrenador de un equipo de fútbol. Tienes que elegir quién será el capitán.

  • IComparable<T>: Cada jugador dice: "¡Yo soy mejor que ese porque corro más rápido!" (Su propia regla interna).
  • IComparer<T>: Tú, como entrenador, dices: "Vale chicos, hoy elegimos capitán por precisión de pase. ¡Pedro, pasa! ¡Juan, pasa! Perfecto, Pedro es más preciso. Hoy él es el capitán." (Es tu regla externa que aplicas a cualquier par de jugadores).

Definición: IComparer<T> es una interfaz en .NET que nos permite definir lógica externa de comparación para dos objetos del tipo T. O sea, la clase que implementa IComparer<T> no es uno de los objetos comparados; simplemente ofrece el método Compare, que recibe dos objetos y decide su orden relativo.

Sintaxis

La sintaxis de la interfaz IComparer<T> es bastante sencilla:


public interface IComparer<T>
{
    // Método que compara dos objetos de tipo T
    // x - primer objeto a comparar
    // y - segundo objeto a comparar
    int Compare(T x, T y);
}

El método Compare(T x, T y) funciona igual que el método CompareTo(T other) en IComparable<T>:

  • Devuelve un número negativo si x es "menor" que y.
  • Devuelve cero (0) si x es "igual" a y.
  • Devuelve un número positivo si x es "mayor" que y.

En nuestro caso, "menor", "igual", "mayor" dependen solo de la lógica de comparación que definamos en la implementación de Compare.

¿En qué se diferencia IComparer<T> de IComparable<T>?

Clase / Interfaz Dónde se implementa Para qué sirve Ejemplo de uso
IComparable<T>
Directamente en el tipo (clase/struct) Una forma estandarizada de comparar Ordenar por ID ascendente
IComparer<T>
En una clase aparte Cualquier cantidad de formas de comparar Ordenar por nombre, fecha

3. Implementando IComparer<T> en la práctica

Vamos a seguir con nuestra "gran aplicación" — un modelo sencillo de usuarios. Supón que tenemos esta clase:


// Nuestra clase de usuario
public class User
{
    public string Name { get; set; }
    public int Age { get; set; }
    public string Email { get; set; }
}

Ordenar por nombre: implementamos un comparador aparte

Vamos a escribir una clase especial que implemente IComparer<User> y compare usuarios por nombre:


// Clase comparadora para ordenar por nombre
public class UserNameComparer : IComparer<User>
{
    public int Compare(User x, User y)
    {
        // Comprobamos null (¡para evitar sorpresas!)
        if (ReferenceEquals(x, y)) return 0;
        if (x is null) return -1;   // null es "menor" que cualquier objeto
        if (y is null) return 1;

        // Comparamos por nombre (usando el estándar de ordenación de strings)
        return string.Compare(x.Name, y.Name, StringComparison.OrdinalIgnoreCase);
    }
}

Usamos el comparador para ordenar la lista:


List<User> users = new List<User>
{
    new User { Name = "Iván", Age = 20, Email = "ivan@mail.com" },
    new User { Name = "Ana", Age = 32, Email = "anna@gmail.com" },
    new User { Name = "Borís", Age = 28, Email = "boris@work.org" },
    new User { Name = "Ruslán", Age = 19, Email = "ruslan@yandex.ru" }
};

// Ordenamos por nombre usando IComparer
users.Sort(new UserNameComparer());

users.ForEach(u => Console.WriteLine(u.Name)); // Ana, Borís, Iván, Ruslán

¿Ves qué limpio y elegante? El comparador se convierte en un ciudadano independiente de tu programa — puedes usarlo para cualquier otra lista de usuarios.

4. Varios modos de comparación: creamos diferentes comparadores

Lo guay es que puedes crear tantos comparadores como quieras. Por ejemplo, vamos a implementar ordenación por edad:


// Comparador para ordenar por edad
public class UserAgeComparer : IComparer<User>
{
    public int Compare(User x, User y)
    {
        if (ReferenceEquals(x, y)) return 0;
        if (x is null) return -1;
        if (y is null) return 1;

        // Ordenamos por edad ascendente
        return x.Age.CompareTo(y.Age);
    }
}

Y ahora:


users.Sort(new UserAgeComparer());
users.ForEach(u => Console.WriteLine($"{u.Name} ({u.Age})"));
// Salida: Ruslán (19) Iván (20) Borís (28) Ana (32)

Si quieres ordenar por edad descendente, solo cambia el orden de los argumentos:


// Comparador para ordenar por edad descendente
public class UserAgeDescendingComparer : IComparer<User>
{
    public int Compare(User x, User y)
    {
        if (ReferenceEquals(x, y)) return 0;
        if (x is null) return -1;
        if (y is null) return 1;

        // Cambiamos el orden: y.CompareTo(x)
        return y.Age.CompareTo(x.Age);
    }
}

5. Detalles útiles

¿Cómo funciona por dentro?

Cuando llamas a Sort() con un comparador, la lista pasa cada elemento a ese comparador y le pregunta: "¿A quién pongo antes?". Tu Compare responde: "a este, a este otro o da igual". La ordenación repite este diálogo para todos los pares hasta que la lista queda ordenada.

¿Qué hacer si los valores son iguales? Simplemente devuelve 0 — así el orden de los elementos no cambia (o lo decide el mecanismo interno de ordenación).

¿Dónde más se usa IComparer<T>?

La interfaz IComparer<T> se usa mucho más allá de las listas. Algunos ejemplos en .NET:

Por ejemplo:


var sortedSet = new SortedSet<User>(new UserAgeComparer());

¡Ahora SortedSet siempre mantendrá el orden por edad automáticamente!

Un apunte sobre seguridad con null

Uno de los errores más comunes de los novatos es el NullReferenceException. No olvides comprobar si los objetos son null dentro de Compare, sobre todo si la lista puede tener esos valores.

Un patrón habitual (lo repetimos para que quede claro):


if (ReferenceEquals(x, y)) return 0;
if (x is null) return -1;
if (y is null) return 1;

Es una buena costumbre: te ayuda a evitar que el programa se caiga en el peor momento.

Ventajas y limitaciones del enfoque con IComparer<T>

  • Separar claramente la lógica de comparación de los datos. La clase usuario no tiene que preocuparse de cómo ni por qué se le va a ordenar.
  • Reutilizar fácilmente la lógica de comparación en diferentes sitios.
  • Escalabilidad: puedes crear todos los modos de ordenación que quieras sin tocar el tipo original.

Pero ojo, no caigas en el error de "me olvidé del null" o "la lógica de comparación no es coherente". Por ejemplo, si Compare(x, y) devuelve 0, entonces Compare(y, x) también debe devolver 0; si Compare(x, y) devuelve >0, entonces Compare(y, x) debe devolver <0, etc.

Guía visual: ¿cuándo usar qué?

Tarea Qué usar Dónde poner la lógica
Un solo orden "natural"
IComparable<T>
En el propio tipo (clase/struct)
Diferentes modos de ordenación
IComparer<T>
En una clase aparte
Rápido, puntual, "al vuelo"
Comparison<T>
/ lambda
En el parámetro del método Sort, usando un delegado
Lógica compleja y reutilizable
IComparer<T>
Como clase comparadora aparte

En la próxima lección vas a ver cómo usar y combinar delegados y expresiones lambda para comparar objetos. Por ahora, prueba a implementar varios comparadores para tu aplicación y disfruta de una arquitectura elegante, donde la ordenación está fuera de la clase principal y la lógica de criterios es flexible y ampliable.

6. Errores típicos al implementar comparadores

Error nº1: no comprobar null.
Si uno de los objetos a comparar es null y no lo tienes en cuenta en el código, el programa puede petar con un NullReferenceException.

Error nº2: valores incorrectos -1, 0, +1.
El método Compare debe devolver un número negativo si el primer objeto es menor que el segundo, cero si son iguales y positivo si es mayor. Saltarse esto hace que la ordenación funcione "raro".

Error nº3: lógica de comparación asimétrica.
Si al comparar x y y devuelves una cosa, y al comparar y y x devuelves lo mismo (en vez del valor opuesto), el resultado será impredecible.

Error nº4: usar Sort() sin comparador para un tipo personalizado.
Si el tipo no implementa IComparable ni IComparable<T>, llamar a Sort() sin un comparador explícito lanzará una excepción InvalidOperationException.

Cómo evitarlo:
Comprueba los casos límite, cubre las partes críticas del código con tests unitarios (¡ya hablaremos de esto!), no tengas miedo de mirar la documentación — y que tus comparadores funcionen como un reloj suizo.

Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION