CodeGym /Cursos /C# SELF /Interfaz IComparable<T&...

Interfaz IComparable<T>

C# SELF
Nivel 30 , Lección 0
Disponible

1. Introducción

Imagina que le pides a un colega que coloque libros en las estanterías. Si los libros tienen números en el lomo (1, 2, 3…), tu colega lo hará sin problema: "Vale, 1 va antes que 2, 2 antes que 3". Eso es el orden natural de los números (por orden). Igual, si los libros empiezan por "A", "B", "C", los pondrá en orden alfabético. También es el "orden natural" para cadenas (alfabético).

¿Pero qué pasa si en los lomos solo hay apellidos de autores? Y le dices: "Ponlos en orden". Tu colega preguntará: "¿En qué orden? ¿Por apellido? ¿Por año de publicación? ¿Por número de páginas?" Aquí empieza el lío. Para tus entidades únicas no hay un orden natural obvio.

En programación pasa igual. Cuando intentamos ordenar una lista de enteros List<int>, C# sabe perfectamente cómo hacerlo. Para List<string> también, usando el orden lexicográfico (alfabético). Pero si tenemos List<Student>, donde cada Student es un objeto con nombre, apellido, edad, id y mil cosas más, C# se queda pillado. No sabe por qué comparar dos estudiantes. ¿Por nombre? ¿Por ID? ¿Por nota media? Este es el problema que vimos en la lección de "Ordenación de colecciones", cuando List<T>.Sort() dio error al intentar ordenar una colección de tipos personalizados.

Para resolver este puzzle, tenemos que darle a C# una instrucción clara de por qué criterio comparar nuestros objetos. Para eso existe la interfaz IComparable<T>.

2. Interfaz IComparable<T>

Así que, para que nuestros objetos "sepan" su sitio respecto a otros objetos iguales, usamos la interfaz IComparable<T>. Es como un contrato. Cuando tu clase o struct implementa esta interfaz, le dices al compilador: "¡Ey! Mis objetos se pueden comparar entre sí y aquí tienes las instrucciones para hacerlo."

¿Cómo funciona?

La interfaz IComparable<T> define un solo método:

public interface IComparable<in T>
{
    int CompareTo(T other);
}

Este método recibe un objeto del tipo T (el mismo tipo que el objeto actual) y debe devolver:

  • un número negativo (< 0), si el objeto actual es "menor" que el comparado;
  • cero (0), si son "iguales" (en cuanto a ordenación);
  • un número positivo (> 0), si el objeto actual es "mayor".

En el fondo, es como los jueces en una competición, por ejemplo, en un combate de boxeo: si lo haces mejor, ganas el asalto y tienes más puntos; si peor, menos; si está igualado, el marcador es igual. Pero aquí, en vez de jueces, es aún más simple: solo un número con signo.

¿Por qué así?

Los métodos de ordenación (por ejemplo, List<T>.Sort()) llaman a CompareTo para los elementos de la lista, para saber a quién poner antes y a quién después. ¡Si tu clase implementa esta interfaz, se puede ordenar!

3. Práctica

Supón que tenemos esta clase de usuario (User):

public class User
{
    public string Name { get; set; }
    public int Age { get; set; }
}

Vamos a intentar ordenar una lista de usuarios:

List<User> users = new List<User>
{
    new User { Name = "Sammy", Age = 31 },
    new User { Name = "Maria", Age = 22 },
    new User { Name = "Anton", Age = 27 }
};

users.Sort(); // ¡BUM! InvalidOperationException

Sale un error: "At least one object must implement IComparable" (Al menos un objeto debe implementar IComparable).

Solucionamos: implementamos IComparable<User>

Añadimos la interfaz a nuestra clase. Por ahora, que ordene por edad — de menor a mayor:

public class User : IComparable<User>
{
    public string Name { get; set; }
    public int Age { get; set; }

    public int CompareTo(User other)
    {
        // Protección: si other == null, nuestro usuario es "mayor"
        if (other == null) return 1;
        return this.Age.CompareTo(other.Age); // ordenamos por edad
    }
}

Ahora creamos la lista, llamamos a users.Sort(); y mostramos el resultado por consola:

List<User> users = new List<User>
{
    new User { Name = "Sammy", Age = 31 },
    new User { Name = "Maria", Age = 22 },
    new User { Name = "Anton", Age = 27 }
};

// Ordenación por edad (usa CompareTo)
users.Sort();

// Mostrar usuarios ordenados
foreach (User user in users)
{
    Console.WriteLine($"{user.Name}, {user.Age}");
}

La lista se ordena por edad:

Maria, 22
Anton, 27
Sammy, 31

Visualización: antes y después

Nombre Edad
Sammy 31
Maria 22
Anton 27

Después de ordenar:

Nombre Edad
Maria 22
Anton 27
Sammy 31

4. Detalles importantes y errores comunes

Protección contra null

Dentro de CompareTo es muy importante comprobar que other no es null. Si te olvidas, puedes tener un NullReferenceException — tan traicionero como un bug justo antes del release. Normalmente, si el objeto comparado es null, se considera que el actual es "mayor":

public int CompareTo(User other)
{
    if (other == null) return 1;
    // ...
}

Cumple la transitividad

Si A < B y B < C, entonces A debe ser < C. Si no cumples esto, la ordenación se comportará de forma impredecible (o sea, divertida, ¡pero mal!).

Si la ordenación es por varios campos

Supón que primero quieres ordenar por edad, pero si dos usuarios tienen la misma edad, los ordenas por nombre alfabéticamente. Se hace así:

public int CompareTo(User other)
{
    if (other == null) return 1;

    int ageCompare = this.Age.CompareTo(other.Age);
    if (ageCompare != 0) return ageCompare;

    // Si la edad es igual — comparamos por nombre
    return this.Name.CompareTo(other.Name);
}

5. Ordenar: ahora también con tus objetos

Todo lo que hace List<int> — ahora lo hace tu clase

Ahora puedes usar cualquier método que requiera comparación: Sort, BinarySearch, incluso insertar en colecciones ordenadas (como SortedSet<T>).

users.Sort();
// users ahora está ordenado por edad (y por nombre si la edad es igual)

Ejemplo en el contexto de una app del curso

Supón que antes ya habías hecho una app para gestionar usuarios. Ahora podemos darles un orden "natural" de ordenación directamente en el código existente. Así quedaría:

// Nuestra clase User ya implementa IComparable<User>

List<User> users = new List<User>
{
    new User { Name = "Ivan", Age = 45 },
    new User { Name = "Jinny", Age = 27 },
    new User { Name = "George", Age = 27 }
};

// Ordenamos por edad, luego por nombre
users.Sort();
foreach (var u in users)
{
    Console.WriteLine($"{u.Name} - {u.Age}");
}

Resultado:

Jinny - 27
George - 27
Ivan - 45

Jinny y George tienen la misma edad, así que se ordenan por nombre si la edad es igual.

6. Cómo funciona CompareTo: matemáticas puras

Vamos a recalcar el estándar de retornos:

  • Número negativo (por ejemplo, -1): el objeto actual va antes que el comparado.
  • Cero: se consideran iguales para la ordenación.
  • Número positivo (por ejemplo, 1): el objeto actual va después del comparado.

Los tipos integrados (por ejemplo, Age.CompareTo(other.Age)) ya siguen este estándar, devolviendo siempre -1, 0 o 1.

Tabla de retornos para el método CompareTo

Valor devuelto ¿Qué significa? Ejemplo
< 0 Menor (va antes)
20.CompareTo(21): -1
0 Iguales
23.CompareTo(23): 0
> 0 Mayor (va después)
42.CompareTo(15): 1

7. Ordenación múltiple: combinando campos

A veces necesitas una ordenación más compleja: por ejemplo, por apellido, nombre y edad. Usando el truco de antes, puedes comparar en orden:

public class Student : IComparable<Student>
{
    public string LastName { get; set; }
    public string FirstName { get; set; }
    public int Grade { get; set; }

    public int CompareTo(Student other)
    {
        if (other == null) return 1;
        int lastNameCompare = this.LastName.CompareTo(other.LastName);
        if (lastNameCompare != 0) return lastNameCompare;
        int firstNameCompare = this.FirstName.CompareTo(other.FirstName);
        if (firstNameCompare != 0) return firstNameCompare;
        return this.Grade.CompareTo(other.Grade);
    }
}

8. Cuándo NO deberías implementar IComparable<T>

Situación real (¡de programador!): si un objeto no tiene un orden "natural" de ordenación, mejor no implementes IComparable<T>. Por ejemplo, si tienes una clase Point y no sabes si ordenar por X, por Y o por distancia al origen — mejor que la ordenación venga de fuera usando una función de comparación (IComparer<T>). De eso hablaremos en la próxima lección.

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