Ir al contenido principal

Sobrecarga de métodos y operadores

La sobrecarga es una característica fundamental de la programación orientada a objetos que permite definir múltiples versiones de métodos o operadores con el mismo nombre, pero con diferentes parámetros o comportamientos. Esta funcionalidad mejora significativamente la legibilidad del código y proporciona una interfaz más intuitiva para los usuarios de nuestras clases. En C#, podemos sobrecargar tanto métodos como operadores, lo que nos permite crear clases que se comportan de manera natural y expresiva.

La sobrecarga de métodos nos permite crear múltiples versiones de una función que realizan tareas similares pero con diferentes tipos o números de parámetros. Por otro lado, la sobrecarga de operadores nos permite definir cómo se comportan los operadores tradicionales (+, -, *, ==, etc.) cuando se aplican a objetos de nuestras clases personalizadas. Estas técnicas son especialmente útiles para crear tipos de datos que se integren naturalmente con el lenguaje y proporcionen una experiencia de programación más fluida y expresiva.

Sobrecarga de métodos

La sobrecarga de métodos permite definir múltiples métodos con el mismo nombre pero con diferentes firmas (combinación de parámetros). El compilador determina qué versión del método llamar basándose en los argumentos proporcionados.

Reglas para la sobrecarga de métodos

Criterio Descripción
Nombre del método Debe ser idéntico en todas las versiones
Número de parámetros Puede variar entre las sobrecargas
Tipo de parámetros Puede variar entre las sobrecargas
Orden de parámetros Puede variar entre las sobrecargas
Tipo de retorno No se considera para la sobrecarga
Modificadores No se consideran para la sobrecarga

Ejemplos básicos de sobrecarga de métodos

public class Calculadora
{
    // Sobrecarga 1: Suma de dos enteros
    public int Sumar(int a, int b)
    {
        Console.WriteLine($"Sumando enteros: {a} + {b}");
        return a + b;
    }
    
    // Sobrecarga 2: Suma de tres enteros
    public int Sumar(int a, int b, int c)
    {
        Console.WriteLine($"Sumando tres enteros: {a} + {b} + {c}");
        return a + b + c;
    }
    
    // Sobrecarga 3: Suma de dos números decimales
    public double Sumar(double a, double b)
    {
        Console.WriteLine($"Sumando decimales: {a} + {b}");
        return a + b;
    }
    
    // Sobrecarga 4: Suma de un array de enteros
    public int Sumar(params int[] numeros)
    {
        Console.WriteLine($"Sumando array de {numeros.Length} números");
        return numeros.Sum();
    }
    
    // Sobrecarga 5: Concatenación de cadenas (diferente comportamiento)
    public string Sumar(string a, string b)
    {
        Console.WriteLine($"Concatenando cadenas: '{a}' + '{b}'");
        return a + b;
    }
}

Ejemplo de uso de métodos sobrecargados

class Program
{
    static void Main()
    {
        Calculadora calc = new Calculadora();
        
        // Cada llamada resuelve a una sobrecarga diferente
        Console.WriteLine($"Resultado: {calc.Sumar(5, 3)}");           // int, int
        Console.WriteLine($"Resultado: {calc.Sumar(5, 3, 2)}");        // int, int, int
        Console.WriteLine($"Resultado: {calc.Sumar(5.5, 3.2)}");       // double, double
        Console.WriteLine($"Resultado: {calc.Sumar(1, 2, 3, 4, 5)}");  // params int[]
        Console.WriteLine($"Resultado: {calc.Sumar("Hola", " Mundo")}"); // string, string
    }
}

Sobrecarga de constructores

public class Persona
{
    public string Nombre { get; set; }
    public string Apellido { get; set; }
    public int Edad { get; set; }
    public string Email { get; set; }
    
    // Constructor 1: Solo nombre
    public Persona(string nombre)
    {
        Nombre = nombre;
        Apellido = "";
        Edad = 0;
        Email = "";
        Console.WriteLine($"Persona creada con nombre: {nombre}");
    }
    
    // Constructor 2: Nombre y apellido
    public Persona(string nombre, string apellido)
    {
        Nombre = nombre;
        Apellido = apellido;
        Edad = 0;
        Email = "";
        Console.WriteLine($"Persona creada: {nombre} {apellido}");
    }
    
    // Constructor 3: Nombre, apellido y edad
    public Persona(string nombre, string apellido, int edad)
    {
        Nombre = nombre;
        Apellido = apellido;
        Edad = edad;
        Email = "";
        Console.WriteLine($"Persona creada: {nombre} {apellido}, {edad} años");
    }
    
    // Constructor 4: Todos los datos
    public Persona(string nombre, string apellido, int edad, string email)
    {
        Nombre = nombre;
        Apellido = apellido;
        Edad = edad;
        Email = email;
        Console.WriteLine($"Persona creada completa: {nombre} {apellido}");
    }
    
    public override string ToString()
    {
        return $"{Nombre} {Apellido} ({Edad} años) - {Email}";
    }
}

Sobrecarga de operadores

La sobrecarga de operadores permite definir cómo se comportan los operadores estándar cuando se aplican a instancias de nuestras clases personalizadas. Esto hace que nuestros tipos personalizados se comporten de manera más natural e intuitiva.

Operadores que se pueden sobrecargar

Categoría Operadores
Aritméticos +, -, *, /, %
Comparación ==, !=, <, >, <=, >=
Lógicos &, `
Unarios +, -, !, ~, ++, --
Otros true, false

Operadores que NO se pueden sobrecargar

Operadores no sobrecargables
&&, `
[] (indexador, se implementa de forma diferente)
() (conversión, se implementa como operador de conversión)
=, +=, -=, etc. (asignación compuesta)
?., ??, ??= (operadores null-conditional y null-coalescing)

Ejemplo práctico: Clase Vector

public class Vector
{
    public double X { get; set; }
    public double Y { get; set; }
    
    public Vector(double x, double y)
    {
        X = x;
        Y = y;
    }
    
    // Sobrecarga del operador + (suma de vectores)
    public static Vector operator +(Vector v1, Vector v2)
    {
        return new Vector(v1.X + v2.X, v1.Y + v2.Y);
    }
    
    // Sobrecarga del operador - (resta de vectores)
    public static Vector operator -(Vector v1, Vector v2)
    {
        return new Vector(v1.X - v2.X, v1.Y - v2.Y);
    }
    
    // Sobrecarga del operador * (multiplicación por escalar)
    public static Vector operator *(Vector v, double escalar)
    {
        return new Vector(v.X * escalar, v.Y * escalar);
    }
    
    // Sobrecarga del operador * (multiplicación escalar por vector)
    public static Vector operator *(double escalar, Vector v)
    {
        return v * escalar; // Reutiliza la sobrecarga anterior
    }
    
    // Sobrecarga del operador - unario (negación)
    public static Vector operator -(Vector v)
    {
        return new Vector(-v.X, -v.Y);
    }
    
    // Sobrecarga del operador == (igualdad)
    public static bool operator ==(Vector v1, Vector v2)
    {
        if (ReferenceEquals(v1, v2)) return true;
        if (v1 is null || v2 is null) return false;
        return Math.Abs(v1.X - v2.X) < 0.0001 && Math.Abs(v1.Y - v2.Y) < 0.0001;
    }
    
    // Sobrecarga del operador != (desigualdad)
    // IMPORTANTE: Si sobrecargamos ==, también debemos sobrecargar !=
    public static bool operator !=(Vector v1, Vector v2)
    {
        return !(v1 == v2);
    }
    
    // Sobrecarga de operadores de comparación
    public static bool operator <(Vector v1, Vector v2)
    {
        return v1.Magnitud() < v2.Magnitud();
    }
    
    public static bool operator >(Vector v1, Vector v2)
    {
        return v1.Magnitud() > v2.Magnitud();
    }
    
    public static bool operator <=(Vector v1, Vector v2)
    {
        return v1.Magnitud() <= v2.Magnitud();
    }
    
    public static bool operator >=(Vector v1, Vector v2)
    {
        return v1.Magnitud() >= v2.Magnitud();
    }
    
    // Método auxiliar para calcular la magnitud
    public double Magnitud()
    {
        return Math.Sqrt(X * X + Y * Y);
    }
    
    // Override de Equals (recomendado cuando se sobrecarga ==)
    public override bool Equals(object obj)
    {
        if (obj is Vector other)
            return this == other;
        return false;
    }
    
    // Override de GetHashCode (recomendado cuando se sobrecarga Equals)
    public override int GetHashCode()
    {
        return HashCode.Combine(X, Y);
    }
    
    public override string ToString()
    {
        return $"({X:F2}, {Y:F2})";
    }
}

Uso de operadores sobrecargados

class Program
{
    static void Main()
    {
        Vector v1 = new Vector(3, 4);
        Vector v2 = new Vector(1, 2);
        
        Console.WriteLine($"Vector 1: {v1}");
        Console.WriteLine($"Vector 2: {v2}");
        Console.WriteLine();
        
        // Operaciones aritméticas
        Vector suma = v1 + v2;
        Vector resta = v1 - v2;
        Vector multiplicacion = v1 * 2;
        Vector negacion = -v1;
        
        Console.WriteLine($"v1 + v2 = {suma}");
        Console.WriteLine($"v1 - v2 = {resta}");
        Console.WriteLine($"v1 * 2 = {multiplicacion}");
        Console.WriteLine($"-v1 = {negacion}");
        Console.WriteLine();
        
        // Operaciones de comparación
        Console.WriteLine($"v1 == v2: {v1 == v2}");
        Console.WriteLine($"v1 != v2: {v1 != v2}");
        Console.WriteLine($"v1 > v2: {v1 > v2}");
        Console.WriteLine($"v1 < v2: {v1 < v2}");
        Console.WriteLine();
        
        // Magnitudes
        Console.WriteLine($"Magnitud de v1: {v1.Magnitud():F2}");
        Console.WriteLine($"Magnitud de v2: {v2.Magnitud():F2}");
    }
}

Operadores de conversión

Los operadores de conversión permiten definir conversiones implícitas y explícitas entre tipos personalizados y otros tipos.

Conversiones implícitas y explícitas

public class Temperatura
{
    public double Celsius { get; private set; }
    
    public Temperatura(double celsius)
    {
        Celsius = celsius;
    }
    
    // Conversión implícita de double a Temperatura
    public static implicit operator Temperatura(double celsius)
    {
        return new Temperatura(celsius);
    }
    
    // Conversión explícita de Temperatura a double
    public static explicit operator double(Temperatura temp)
    {
        return temp.Celsius;
    }
    
    // Conversión implícita de Temperatura a string
    public static implicit operator string(Temperatura temp)
    {
        return $"{temp.Celsius}°C";
    }
    
    // Conversión de Fahrenheit (como método estático adicional)
    public static implicit operator Temperatura(Fahrenheit fahr)
    {
        double celsius = (fahr.Valor - 32) * 5.0 / 9.0;
        return new Temperatura(celsius);
    }
    
    public override string ToString()
    {
        return $"{Celsius}°C";
    }
}

public class Fahrenheit
{
    public double Valor { get; private set; }
    
    public Fahrenheit(double fahrenheit)
    {
        Valor = fahrenheit;
    }
    
    // Conversión implícita de double a Fahrenheit
    public static implicit operator Fahrenheit(double fahrenheit)
    {
        return new Fahrenheit(fahrenheit);
    }
    
    public override string ToString()
    {
        return $"{Valor}°F";
    }
}

Ejemplo de uso de conversiones

class Program
{
    static void Main()
    {
        // Conversión implícita de double a Temperatura
        Temperatura temp1 = 25.0;
        Console.WriteLine($"Temperatura 1: {temp1}");
        
        // Conversión explícita de Temperatura a double
        double grados = (double)temp1;
        Console.WriteLine($"Grados: {grados}");
        
        // Conversión implícita de Temperatura a string
        string descripcion = temp1;
        Console.WriteLine($"Descripción: {descripcion}");
        
        // Conversión entre tipos personalizados
        Fahrenheit tempF = 77.0; // 77°F
        Temperatura tempC = tempF; // Conversión automática a Celsius
        Console.WriteLine($"77°F = {tempC}");
    }
}

Ejemplo avanzado: Clase Fraccion

public class Fraccion
{
    public int Numerador { get; private set; }
    public int Denominador { get; private set; }
    
    public Fraccion(int numerador, int denominador = 1)
    {
        if (denominador == 0)
            throw new ArgumentException("El denominador no puede ser cero");
            
        // Simplificar la fracción
        int mcd = MCD(Math.Abs(numerador), Math.Abs(denominador));
        Numerador = numerador / mcd;
        Denominador = denominador / mcd;
        
        // Asegurar que el denominador sea positivo
        if (Denominador < 0)
        {
            Numerador = -Numerador;
            Denominador = -Denominador;
        }
    }
    
    // Operadores aritméticos
    public static Fraccion operator +(Fraccion f1, Fraccion f2)
    {
        int num = f1.Numerador * f2.Denominador + f2.Numerador * f1.Denominador;
        int den = f1.Denominador * f2.Denominador;
        return new Fraccion(num, den);
    }
    
    public static Fraccion operator -(Fraccion f1, Fraccion f2)
    {
        int num = f1.Numerador * f2.Denominador - f2.Numerador * f1.Denominador;
        int den = f1.Denominador * f2.Denominador;
        return new Fraccion(num, den);
    }
    
    public static Fraccion operator *(Fraccion f1, Fraccion f2)
    {
        return new Fraccion(f1.Numerador * f2.Numerador, f1.Denominador * f2.Denominador);
    }
    
    public static Fraccion operator /(Fraccion f1, Fraccion f2)
    {
        return new Fraccion(f1.Numerador * f2.Denominador, f1.Denominador * f2.Numerador);
    }
    
    // Operadores de comparación
    public static bool operator ==(Fraccion f1, Fraccion f2)
    {
        return f1.Numerador * f2.Denominador == f2.Numerador * f1.Denominador;
    }
    
    public static bool operator !=(Fraccion f1, Fraccion f2)
    {
        return !(f1 == f2);
    }
    
    public static bool operator <(Fraccion f1, Fraccion f2)
    {
        return f1.Numerador * f2.Denominador < f2.Numerador * f1.Denominador;
    }
    
    public static bool operator >(Fraccion f1, Fraccion f2)
    {
        return f1.Numerador * f2.Denominador > f2.Numerador * f1.Denominador;
    }
    
    // Conversiones
    public static implicit operator Fraccion(int entero)
    {
        return new Fraccion(entero);
    }
    
    public static explicit operator double(Fraccion fraccion)
    {
        return (double)fraccion.Numerador / fraccion.Denominador;
    }
    
    // Método auxiliar para calcular el MCD
    private static int MCD(int a, int b)
    {
        while (b != 0)
        {
            int temp = b;
            b = a % b;
            a = temp;
        }
        return a;
    }
    
    public override bool Equals(object obj)
    {
        if (obj is Fraccion other)
            return this == other;
        return false;
    }
    
    public override int GetHashCode()
    {
        return HashCode.Combine(Numerador, Denominador);
    }
    
    public override string ToString()
    {
        if (Denominador == 1)
            return Numerador.ToString();
        return $"{Numerador}/{Denominador}";
    }
}

Uso de la clase Fraccion

class Program
{
    static void Main()
    {
        Fraccion f1 = new Fraccion(1, 2);    // 1/2
        Fraccion f2 = new Fraccion(1, 3);    // 1/3
        Fraccion f3 = 2;                     // Conversión implícita: 2/1
        
        Console.WriteLine($"f1 = {f1}");
        Console.WriteLine($"f2 = {f2}");
        Console.WriteLine($"f3 = {f3}");
        Console.WriteLine();
        
        // Operaciones aritméticas
        Console.WriteLine($"f1 + f2 = {f1 + f2}");
        Console.WriteLine($"f1 - f2 = {f1 - f2}");
        Console.WriteLine($"f1 * f2 = {f1 * f2}");
        Console.WriteLine($"f1 / f2 = {f1 / f2}");
        Console.WriteLine();
        
        // Operaciones con enteros (conversión implícita)
        Console.WriteLine($"f1 + 1 = {f1 + 1}");
        Console.WriteLine($"f3 * f1 = {f3 * f1}");
        Console.WriteLine();
        
        // Comparaciones
        Console.WriteLine($"f1 == f2: {f1 == f2}");
        Console.WriteLine($"f1 > f2: {f1 > f2}");
        Console.WriteLine($"f1 < f3: {f1 < f3}");
        Console.WriteLine();
        
        // Conversión a decimal
        Console.WriteLine($"f1 como decimal: {(double)f1:F4}");
        Console.WriteLine($"f2 como decimal: {(double)f2:F4}");
    }
}

Buenas prácticas para la sobrecarga

Recomendaciones generales

Práctica Descripción
Consistencia Los operadores sobrecargados deben comportarse de manera intuitiva
Simetría Si sobrecarga ==, también sobrecargue !=
Coherencia Los operadores relacionados deben ser coherentes entre sí
Inmutabilidad Los operadores no deben modificar los operandos originales
Documentación Documente comportamientos no obvios
Rendimiento Considere el impacto en el rendimiento de las operaciones

Errores comunes a evitar

// ❌ MALO: Comportamiento no intuitivo
public static Punto operator +(Punto p1, Punto p2)
{
    // No hagas esto: la suma debería sumar coordenadas, no restarlas
    return new Punto(p1.X - p2.X, p1.Y - p2.Y);
}

// ✅ BUENO: Comportamiento intuitivo
public static Punto operator +(Punto p1, Punto p2)
{
    return new Punto(p1.X + p2.X, p1.Y + p2.Y);
}

// ❌ MALO: Modificar operandos originales
public static Vector operator +(Vector v1, Vector v2)
{
    v1.X += v2.X; // ¡No modifiques el operando original!
    v1.Y += v2.Y;
    return v1;
}

// ✅ BUENO: Crear nuevo objeto
public static Vector operator +(Vector v1, Vector v2)
{
    return new Vector(v1.X + v2.X, v1.Y + v2.Y);
}

Resumen

La sobrecarga de métodos y operadores es una característica poderosa de C# que permite crear clases más expresivas y fáciles de usar. La sobrecarga de métodos nos permite proporcionar múltiples formas de realizar operaciones similares con diferentes parámetros, mientras que la sobrecarga de operadores permite que nuestros tipos personalizados se integren naturalmente con los operadores del lenguaje.

Al implementar sobrecarga, es fundamental mantener la consistencia y seguir las convenciones esperadas por los usuarios del código. Los operadores sobrecargados deben comportarse de manera intuitiva y coherente, y siempre debemos considerar la implementación de operadores relacionados como pares (== con !=, < con >, etc.). Estas técnicas, cuando se aplican correctamente, resultan en código más legible y expresivo que se integra perfectamente con el ecosistema de C#.