Al igual que ocurría con las conversiones, el ecosistema .Net viene preparado con una serie de operaciones que es posible realizar entre los tipos del propio framework. Un ejemplo trivial es una suma:
int total = 10 + 11;
En cambio, al igual que pasaba con las conversiones, cuando definimos nuestras propias clases tenemos que definir también sus operaciones (si es que las tiene). En caso contrario, vamos a ver un error que nos dice que no se puede aplicar un determinado operador:
A simple vista, puede parecer que estas operaciones tienen poca utilidad. Yo mismo he tenido que pensar bastante en un ejemplo cristalinamente claro con el que explicarlo. Pero hay uno que lo es. De hecho, es todo un campo: ¡las matemáticas!. Y más en concreto, el manejo de fracciones.
Para este articulo he elegido las fracciones por ser algo fácil de entender, aunque el modo de trabajo sería igual para matrices, números imaginarios, etc...
El framework no nos provee de ningún tipo nativo para trabajar con fracciones, así que podemos definir nuestro propio tipo. Podría ser algo así:
class Fraccion
{
public Fraccion(int Numerador, int Denominador)
{
this.Numerador = Numerador;
this.Denominador = Denominador;
}
public int Numerador { get; }
public int Denominador { get; }
}
Aunque las fracciones se pueden operar calculando su valor decimal, eso puede generarnos un error por la precisión de la división que se va a acumular en cada operación. Por esa razón es habitual trabajar con ellas y solo calcular la división como último paso. Siguiendo esta filosofía de mantener la fracción hasta el final, podríamos ir operando las fracciones de esta manera. Por ejemplo, para multiplicar dos fracciones tan solo se deben multiplicar los numerados y los denominadores entre sí:
Fraccion frac1 = new Fraccion(1,2); //Representa 1/2, o sea 0.5
Fraccion frac2 = new Fraccion(1,4); //Representa 1/4, o sea 0.25
Fraccion Multiplicacion = new Fraccion((frac1.Numerador * frac2.Numerador),(frac1.Denominador * frac2.Denominador));
Este código es perfectamente válido, pero si lo que queremos es sumarlas, la cosa se complica ya que hay que sacar factor común y luego sumar los numeradores:
Fraccion frac1 = new Fraccion(1,2);
Fraccion frac2 = new Fraccion(1,4);
Fraccion Suma = new Fraccion((frac1.Numerador * frac2.Denominador + frac2.Numerador * frac1.Denominador),(frac1.Denominador * frac2.Denominador));
Nota: en este caso no hacemos la simplificación final de la fracción, sacando común denominador, para no complicar la explicación. En el ejemplo final sí lo tienes añadido.
Es aquí donde entra la sobrecarga de operadores. Con esta posibilidad que nos ofrece el framework podemos crear nuestras propias operaciones entre tipos sin ningún problema. Por ejemplo, el caso anterior podríamos definir el operador de suma de fracciones así:
class Fraccion
{
public Fraccion(int Numerador, int Denominador)
{
this.Numerador = Numerador;
this.Denominador = Denominador;
}
public int Numerador { get; }
public int Denominador { get; }
public static Fraccion operator +(Fraccion frac1, Fraccion frac2)
{
var nuevoNumerador = frac1.Numerador * frac2.Denominador + frac2.Numerador * frac1.Denominador;
var nuevoDenominador = frac1.Denominador * frac2.Denominador;
return new Fraccion(nuevoNumerador, nuevoDenominador);
}
}
//---------
Fraccion frac1 = new Fraccion(1,2);
Fraccion frac2 = new Fraccion(1,4);
Fraccion Suma = frac1 + frac2;
Como vemos, se definen de manera similar a una función. Deben ser métodos estáticos y se utiliza la palabra clave operator
delante del operador que vamos a sobrecargar. Como argumentos tomarán tantos como operandos, es decir, como elementos con los que se va a operar. Generalmente serán 2, pero no te olvides de que existen también operadores unarios con un solo operando.
Hay que decir también, que es necesario que definamos cada operación que queremos soportar. Esto incluye cada uno de los parámetros de la operación y su orden, por ejemplo, con la clase anterior, no podríamos sumar la fracción con un escalar (sumarle un número entero):
Para que el código anterior pueda funcionar, tendremos que escribir otras 2 sobrecargas del operador +
(ponemos solo las nuevas sobrecargas, no la clase entera):
class Fraccion
{
//...
public static Fraccion operator +(int escalar, Fraccion frac)
{
var nuevoNumerador = escalar * frac.Denominador + frac.Numerador;
return new Fraccion(nuevoNumerador, frac.Denominador);
}
public static Fraccion operator +(Fraccion frac, int escalar)
{
var nuevoNumerador = escalar * frac.Denominador + frac.Numerador;
return new Fraccion(nuevoNumerador, frac.Denominador);
}
}
Es necesario sobrescribir las dos porque hay operaciones donde el orden sí importa. La resta es un buen ejemplo.
La lista de operadores que se pueden sobrecargar es bastante grande, y no es necesario sobrecargar más de los que nuestra aplicación necesita o los que queramos soportar si estamos creando una librería.
En resumen
La posibilidad de sobrecargar los operadores combinada con la de definir nuestras propias conversiones dota de una gran potencia al lenguaje y nos permite adaptarnos a las necesidades que tengamos en cada momento. Es precisamente la unión de estas dos opciones la que más potencia nos ofrece.
Siguiendo con nuestro ejemplo de fracciones, podemos aprovechar las conversiones personalizadas para evitar tener que escribir un montón de código y poder usar operadores como >
, <
, >=
, <=
, ==
, !=
, etc. sin tener que definirlos, aprovechando la conversión desde Fraccion
a double
, y en sentido contrario de int
a Fraccion
.
Te dejo un repl.it a continuación con el código completo de la clase y ejemplos para todos los operadores y conversiones, de modo que puedas probarlo:
Este ejemplo es, por supuesto, mejorable y se le pueden añadir más cosas, pero nos aporta una idea clara de la potencia que nos ofrece el framework al poder escribir nuestras conversiones y sobrecargar operadores y a efectos de ilustrar lo que necesitábamos creo que te será de gran utilidad.