1. Introducción
Seguro que ya estás acostumbrado a usar operadores como + para sumar números o == para comparar. Estos operadores funcionan genial con los tipos de datos integrados. Pero, ¿qué pasa si creas tu propia clase, por ejemplo, Vector (vector), y quieres que la suma de dos vectores se vea natural, tipo vector1 + vector2? ¿O que comparar dos objetos Money (dinero) funcione como moneyAmount1 == moneyAmount2? Justo para estos casos existe la sobrecarga de operadores en C#.
La sobrecarga de operadores te permite definir cómo los operadores estándar (como +, -, *, /, ==, !=, >, <, ++, -- y muchos más) deben funcionar con instancias de tus propias clases o structs. Esto hace tu código más intuitivo, legible y expresivo, permitiéndote usar la sintaxis habitual con tus tipos personalizados.
Básicos de la sobrecarga de operadores
Para sobrecargar un operador, declaras un método estático en tu clase o struct usando la palabra clave operator, seguida del símbolo del operador que quieres sobrecargar.
- Método estático: El método de sobrecarga de operador siempre debe ser public static.
- Nombre del método: El nombre del método es la palabra clave operator seguida del símbolo del operador (por ejemplo, operator +, operator ==).
- Parámetros: El número de parámetros depende del tipo de operador (unario o binario).
- Operadores unarios (por ejemplo, +, -, !, ++, --): aceptan un parámetro del tipo de la clase/struct donde están definidos.
- Operadores binarios (por ejemplo, +, -, *, /, ==, !=): aceptan dos parámetros, al menos uno debe ser del tipo de la clase/struct donde están definidos.
- Tipo de retorno: El tipo que devuelve la operación.
2. Sobrecarga de operadores binarios (Binary Operators)
Los operadores binarios aceptan dos operandos. Los más comunes son los operadores aritméticos (+, -, *, /) y los operadores de comparación (==, !=, >, <, >=, <=).
Ejemplo: Sobrecarga del operador suma (+)
public struct Point
{
public int X { get; set; }
public int Y { get; set; }
public Point(int x, int y) => (X, Y) = (x, y);
// Sobrecarga del operador +
public static Point operator +(Point p1, Point p2)
{
return new Point(p1.X + p2.X, p1.Y + p2.Y);
}
public override string ToString() => $"({X}, {Y})";
}
// Uso:
Point point1 = new Point(1, 2);
Point point2 = new Point(3, 4);
Point sumPoint = point1 + point2; // Aquí se llama al operador sobrecargado +
Console.WriteLine($"Suma de puntos: {sumPoint}"); // Salida: Suma de puntos: (4, 6)
Aquí hemos definido que la suma de dos objetos Point debe devolver un nuevo objeto Point cuyas coordenadas son la suma de las coordenadas correspondientes de los puntos originales.
Ejemplo: Sobrecarga del operador multiplicación (*) por un escalar (número)
public struct Vector
{
public double X { get; set; }
public double Y { get; set; }
public Vector(double x, double y) => (X, Y) = (x, y);
// Sobrecarga del operador * (vector por número)
public static Vector operator *(Vector vec, double scalar)
{
return new Vector(vec.X * scalar, vec.Y * scalar);
}
// Sobrecarga del operador * (número por vector) - para simetría
public static Vector operator *(double scalar, Vector vec)
{
return new Vector(vec.X * scalar, vec.Y * scalar);
}
public override string ToString() => $"<{X}, {Y}>";
}
// Uso:
Vector vec1 = new Vector(2, 3);
Vector scaledVec1 = vec1 * 5; // Llama a Vector * double
Vector scaledVec2 = 5 * vec1; // Llama a double * Vector
Console.WriteLine($"Vector escalado 1: {scaledVec1}"); // Salida: Vector escalado 1: <10, 15>
Console.WriteLine($"Vector escalado 2: {scaledVec2}"); // Salida: Vector escalado 2: <10, 15>
Fíjate que para el operador de multiplicación lo sobrecargamos dos veces para que funcione de forma simétrica: Vector * double y double * Vector.
Ejemplo: Sobrecarga de los operadores de comparación (== y !=)
Al sobrecargar los operadores == y != hay que cumplir un contrato importante: si sobrecargas uno, debes sobrecargar el otro también. Además, es muy recomendable que el comportamiento de estos operadores sea coherente con el método Equals() y GetHashCode().
public class Money
{
public decimal Amount { get; set; }
public string Currency { get; set; }
public Money(decimal amount, string currency)
{
Amount = amount;
Currency = currency;
}
// Es obligatorio sobrescribir Equals y GetHashCode si sobrecargas == / !=
public override bool Equals(object? obj)
{
if (obj is not Money other) return false;
return Amount == other.Amount && Currency == other.Currency;
}
public override int GetHashCode() => HashCode.Combine(Amount, Currency);
// Sobrecarga del operador ==
public static bool operator ==(Money? m1, Money? m2)
{
// Comprobación de null para tipos de referencia
if (ReferenceEquals(m1, null)) return ReferenceEquals(m2, null);
return m1.Equals(m2); // Usamos nuestro Equals sobrescrito
}
// Sobrecarga del operador != (obligatorio si sobrecargas ==)
public static bool operator !=(Money? m1, Money? m2)
{
return !(m1 == m2);
}
}
// Uso:
Money cash1 = new Money(100, "USD");
Money cash2 = new Money(100, "USD");
Money cash3 = new Money(50, "USD");
Money cash4 = new Money(100, "EUR");
Console.WriteLine($"cash1 == cash2: {cash1 == cash2}"); // True
Console.WriteLine($"cash1 == cash3: {cash1 == cash3}"); // False
Console.WriteLine($"cash1 == cash4: {cash1 == cash4}"); // False
Para los tipos de valor (structs), los operadores == y != por defecto hacen una comparación bit a bit, lo cual suele estar bien. Pero para clases (class), por defecto == compara referencias, así que sobrecargarlo es muy útil para comparar por valor.
3. Sobrecarga de operadores unarios (Unary Operators)
Los operadores unarios trabajan con un solo operando. Ejemplos: + (más unario), - (menos unario), ! (NOT lógico), ~ (NOT bit a bit), ++ (incremento), -- (decremento).
Ejemplo: Sobrecarga del menos unario (-)
public struct Vector3D
{
public double X, Y, Z;
public Vector3D(double x, double y, double z) => (X, Y, Z) = (x, y, z);
// Sobrecarga del operador unario -
public static Vector3D operator -(Vector3D vec)
{
return new Vector3D(-vec.X, -vec.Y, -vec.Z);
}
public override string ToString() => $"<{X}, {Y}, {Z}>";
}
// Uso:
Vector3D originalVec = new Vector3D(1, -2, 3);
Vector3D invertedVec = -originalVec; // Llama al operador unario sobrecargado -
Console.WriteLine($"Vector original: {originalVec}"); // Salida: Vector original: <1, -2, 3>
Console.WriteLine($"Vector invertido: {invertedVec}"); // Salida: Vector invertido: <-1, 2, -3>
Ejemplo: Sobrecarga de los operadores de incremento (++) y decremento (--)
Estos operadores modifican el operando y devuelven el valor modificado.
public struct Counter
{
public int Value { get; set; }
public Counter(int value) => Value = value;
// Sobrecarga del operador de incremento ++
public static Counter operator ++(Counter c)
{
// Importante: devolvemos un NUEVO objeto si el struct es inmutable,
// o modificamos el actual y lo devolvemos si es clase
return new Counter(c.Value + 1);
}
// Sobrecarga del operador de decremento --
public static Counter operator --(Counter c)
{
return new Counter(c.Value - 1);
}
public override string ToString() => $"[Contador: {Value}]";
}
// Uso:
Counter myCounter = new Counter(5);
myCounter++; // Incremento postfijo
Console.WriteLine(myCounter); // Salida: [Contador: 6]
++myCounter; // Incremento prefijo
Console.WriteLine(myCounter); // Salida: [Contador: 7]
myCounter--;
Console.WriteLine(myCounter); // Salida: [Contador: 6]
Los operadores sobrecargados ++ y -- siempre son prefijos (primero modifican, luego devuelven). El compilador genera automáticamente el comportamiento correcto para la forma postfija.
4. Sobrecarga de operadores de conversión de tipos
Puedes definir cómo las instancias de tu tipo pueden convertirse explícita o implícitamente a otro tipo, y viceversa.
- implicit (conversión implícita): Se usa cuando la conversión siempre es segura y no hay pérdida de datos (por ejemplo, int a long).
- explicit (conversión explícita): Se usa cuando la conversión puede causar pérdida de datos o error, y requiere indicarlo explícitamente ((Type)obj).
Ejemplo: Conversión implícita de Score a int
public struct Score
{
public int Points { get; set; }
public Score(int points) => Points = points;
// Conversión implícita de Score a int
public static implicit operator int(Score s)
{
return s.Points;
}
}
// Uso:
Score examScore = new Score(95);
int scoreValue = examScore; // Conversión implícita
Console.WriteLine($"Puntos: {scoreValue}"); // Salida: Puntos: 95
Ejemplo: Conversión explícita de Celsius a Fahrenheit
public struct Celsius
{
public double Degrees { get; set; }
public Celsius(double degrees) => Degrees = degrees;
}
public struct Fahrenheit
{
public double Degrees { get; set; }
public Fahrenheit(double degrees) => Degrees = degrees;
// Conversión explícita de Fahrenheit a Celsius
public static explicit operator Celsius(Fahrenheit f)
{
return new Celsius((f.Degrees - 32) * 5 / 9);
}
// Conversión explícita de Celsius a Fahrenheit
public static explicit operator Fahrenheit(Celsius c)
{
return new Fahrenheit(c.Degrees * 9 / 5 + 32);
}
}
// Uso:
Celsius c = new Celsius(25);
Fahrenheit f = (Fahrenheit)c; // Conversión explícita
Console.WriteLine($"25°C = {f.Degrees}°F"); // Salida: 25°C = 77°F
Fahrenheit f2 = new Fahrenheit(212);
Celsius c2 = (Celsius)f2; // Conversión explícita
Console.WriteLine($"212°F = {c2.Degrees}°C"); // Salida: 212°F = 100°C
5. Limitaciones y recomendaciones
No todos los operadores se pueden sobrecargar: No puedes sobrecargar operadores como &&, ||, ?., new, typeof, is, as, == (para string, porque ya está sobrecargado en la clase string y tiene un comportamiento especial), () (llamada de método), = (asignación) y algunos otros.
Simetría: Si sobrecargas un operador binario, muchas veces es útil sobrecargarlo también para el orden inverso de los operandos, si tiene sentido (como en el ejemplo de Vector * double).
Contratos: Cumple siempre los contratos, especialmente para los operadores de comparación (==, !=, >, <, >=, <=) y su relación con Equals() y GetHashCode(). Romper estos contratos puede causar comportamientos raros y errores, sobre todo en colecciones.
Legibilidad e intuición: Sobrecarga operadores solo cuando haga el código más intuitivo y legible. Si el comportamiento del operador no es obvio o puede confundir, mejor usa métodos normales con nombres claros (por ejemplo, Add() en vez de +).
Objetos mutables: Ten cuidado al sobrecargar operadores para tipos mutables. Por ejemplo, Vector v1 = v2 + v3; por defecto crea un nuevo objeto, no modifica v2. Si tu operador modifica el objeto existente, puede ser poco claro.
Structs vs Clases: La sobrecarga de operadores se usa más a menudo en structs, que suelen representar valores (por ejemplo, Point, ComplexNumber), y para los que las operaciones matemáticas o comparaciones de valores son naturales.
GO TO FULL VERSION