Sintaxis general de redefinición de operador
La forma en que se redefine un operador depende del tipo de operador del que se trate, ya que no es lo mismo definir un operador unario que uno binario. Sin embargo, como regla general podemos considerar que se definiendo un método público y estático cuyo nombre sea el símbolo del operador a redefinir y venga precedido de la palabra reservada operator. Es decir, se sigue una sintaxis de la forma:
public static <tipoDevuelto> operator <símbolo>(<operandos>) {
<cuerpo> }
Los modificadores public y static pueden permutarse si se desea, lo que es importante es que siempre aparezcan en toda redefinición de operador. Se puede redefinir tanto operadores unarios como binarios, y en <operandos> se ha de incluir tantos parámetros como operandos pueda tomar el operador a redefinir, ya que cada uno representará a uno de sus operandos. Por último, en <cuerpo> se ha de escribir las instrucciones a ejecutar cada vez que se aplique la operación cuyo operador es <símbolo> a operandos de los tipos indicados en <operandos>
<tipoDevuelto> no puede ser void, pues por definición toda operación tiene un resultado, por lo que todo operador ha de devolver algo. Además, permitirlo complicaría innecesariamente el compilador y éste tendría que admitir instrucciones poco intuitivas (como a+b; si el + estuviese redefinido con valor de retorno void para los tipos de a y b)
Además, los operadores no pueden redefinirse con total libertad ya que ello dificultaría inncesariamente la legibilidad del código, por lo que se han introducido las siguientes restricciones al redefinirlos:
• Al menos uno de los operandos ha de ser del mismo tipo de dato del que sea miembro la redefinición del operador. Como puede deducirse, ello implica que aunque puedan sobrecargarse los operadores binarios nunca podrá hacerse lo mismo con los binarios ya que su único parámetro sólo puede ser de un único tipo (el tipo dentro del que se defina) Además, ello también provoca que no pueden redefinirse las conversiones ya incluidas en la BCL porque al menos uno de los operandos siempre habrá de ser de algún nuevo tipo definido por el usuario.
• No puede alterarse sus reglas de precedencia, asociatividad, ubicación y número de operandos, pues si ya de por sí es difícil para muchos recordarlas cuando son fijas, mucho más lo sería si pudiesen modificarse según los tipos de sus operandos.
• No puede definirse nuevos operadores ni combinaciones de los ya existentes con nuevos significados (por ejemplo ** para representar exponenciación), pues ello complicaría innecesariamente el compilador, el lenguaje y la legibilidad del código cuando en realidad es algo que puede simularse definiendo métodos.
El lenguaje de programación C# Tema 11: Redefinición de operadores
• No todos los operadores incluidos en el lenguaje pueden redefinirse, pues muchos de ellos (como ., new, =, etc.) son básicos para el lenguaje y su redefinición es inviable, poco útil o dificultaría innecesariamente la legibilidad de los fuentes. Además, no todos los redefinibles se redefinen usando la sintaxis general hasta ahora vista, aunque en su momento se irán explicando cuáles son los redefinibles y cuáles son las peculiaridades de aquellos que requieran una redefinición especial. A continuación se muestra cómo se redefiniría el significado del operador + para los objetos Complejo del ejemplo anterior:
class Complejo; {
public float ParteReal; public float ParteImaginaria;
public Complejo (float parteReal, float parteImaginaria) {
this.ParteReal = parteReal;
this.ParteImaginaria = parteImaginaria; }
public static Complejo operator +(Complejo op1, Complejo op2) {
Complejo resultado = new Complejo();
resultado.ParteReal = op1.ParteReal + op2.ParteReal;
resultado.ParteImaginaria = op1.ParteImaginaria + op2.ParteImaginaria; return resultado;
} }
Es fácil ver que lo que en el ejemplo se ha redefinido es el significado del operador + para que cuando se aplique entre dos objetos de clase Complejo devuelva un nuevo objeto Complejo cuyas partes real e imaginaria sea la suma de las de sus operandos. Se considera erróneo incluir la palabra reservada new en la redefinición de un operador, ya que no pueden ocultarse redefiniciones de operadores en tanto que estos no se aplican a usando el nombre del tipo en que estén definidos. Las únicas posibles coincidencias se daría en situaciones como la del siguiente ejemplo:
using System; class A {
public static int operator +(A obj1, B obj2) { Console.WriteLine(“Aplicado + de A”); return 1; } } class B:A {
public static int operator +(A obj1, B obj2) {
return 1; }
public static void Main() { A o1 = new A(); B o2 = new B(); Console.WriteLine(“o1+o2={0}”, o1+o2); } }
Sin embargo, más que una ocultación de operadores lo que se tiene es un problema de ambigüedad en la definición del operador + entre objetos de tipos A y B, de la que se informará al compilar ya que el compilador no sabrá cuál versión del operador debe usar para traducir o1+o2 a código binario.
Redefinición de operadores unarios
Los únicos operadores unarios redefinibles son: !, +, -, ~, ++, --, true y false, y toda
redefinición de un operador unario ha de tomar un único parámetro que ha de ser del mismo tipo que el tipo de dato al que pertenezca la redefinición.
Los operadores ++ y -- siempre ha de redefinirse de manera que el tipo de dato del objeto devuelto sea el mismo que el tipo de dato donde se definen. Cuando se usen de forma prefija se devolverá ese objeto, y cuando se usen de forma postifja el compilador lo que hará será devolver el objeto original que se les pasó como parámetro en lugar del indicado en el return. Por ello es importante no modificar dicho parámetro si es de un tipo referencia y queremos que estos operadores tengan su significado tradicional. Un ejemplo de cómo hacerlo es la siguiente redefinición de ++ para el tipo Complejo:
public static Complejo operator ++ (Complejo op) {
Complejo resultado = new Complejo(op.ParteReal + 1, op.ParteImaginaria); return resultado;
}
Nótese que si hubiésemos redefinido el ++ de esta otra forma: public static Complejo operator ++ (Complejo op)
{
op.ParteReal++; return op; }
entonces el resultado devuelto al aplicárselo a un objeto siempre sería el mismo tanto si fue aplicado de forma prefija como si lo fue de forma postifija, ya que en ambos casos el objeto devuelto sería el mismo. Sin embargo, eso no ocurriría si Complejo fuese una estructura, ya que entonces op no sería el objeto original sino una copia de éste y los cambios que se le hiciesen en el cuerpo de la redefinición de ++ no afectarían al objeto original, que es el que se devuelve cuando se usa ++ de manera postfija.
El lenguaje de programación C# Tema 11: Redefinición de operadores
Respecto a los operadores true y false, estos indican respectivamente, cuando se ha de considerar que un objeto representa el valor lógico cierto y cuando se ha de considerar que representa el valor lógico falso, por lo que su redefiniciones siempre han de devolver un objeto de tipo bool que indique dicha situación. Además, si se redefine uno de estos operadores, entonces es obligatorio redefinir también el otro, en tanto que siempre es posible usar indistintamente uno u otro para determinar el valor lógico que un objeto de ese tipo represente.
En realidad los operadores true y false no pueden usarse directamente en el código fuente, sino que redefinirlos para un tipo de dato es útil porque permitir usar objetos de ese tipo en expresiones condicionales tal y como si de un valor lógico se tratase. Por ejemplo, podemos redefinir estos operadores en el tipo Complejo de modo que consideren cierto a todo complejo distinto de 0 + 0i y falso a 0 + 0i:
public static bool operator true(Complejo op) {
return (op.ParteReal != 0 || op.ParteImaginaria != 0); }
public static bool operator false(Complejo op) {
return (op.ParteReal == 0 && op.ParteImaginaria == 0); }
Con estas redefiniciones, un código como el que sigue mostraría por pantalla el mensaje Es cierto:
Complejo c1 = new Complejo(1, 0); // c1 = 1 + 0i if (c1)
System.Console.WriteLine(“Es cierto”);
Redefinición de operadores binarios
Los operadores binarios redefinibles son +,-,*,/, %,&,|,^,<<,>>,==,!=,>,<,>= y <= Toda redefinición que se haga de ellos ha de tomar dos parámetros tales que al menos uno de ellos sea del mismo tipo que el tipo de dato del que es miembro la redefinición. Hay que tener en cuenta que aquellos de estos operadores que tengan complementario siempre han de redefinirse junto con éste. Es decir, siempre que se redefina en un tipo el operador > también ha de redefinirse en él el operador <, siempre que se redefina >= ha de redefinirse <=, y siempre que se redefina == ha de redefinirse !=.
También hay que señalar que, como puede deducirse de la lista de operadores binarios redefinibles dada, no es redefinir directamente ni el operador de asignación = ni los operadores compuestos (+=, -=, etc.) Sin embargo, en el caso de estos últimos dicha redefinición ocurre de manera automática al redefinir su parte “no =” Es decir, al redefinir + quedará redefinido consecuentemente +=, al redefinir * lo hará *=, etc.
Por otra parte, también cabe señalar que no es posible redefinir diréctamente los operadores && y ||. Esto se debe a que el compilador los trata de una manera especial
que consiste en evaluarlos perezosamente. Sin embargo, es posible simular su redefinición redefiniendo los operadores unarios true y false, los operadores binarios & y | y teniendo en cuenta que && y || se evalúan así:
• &&: Si tenemos una expresión de la forma x && y, se aplica primero el operador false a x. Si devuelve false, entonces x && y devuelve el resultado de evaluar x; y si no, entonces devuelve el resultado de evaluar x & y
• ||: Si tenemos una expresión de la forma x || y, se aplica primero el operador true a x. Si devuelve true, se devuelve el resultado de evaluar x; y si no, se devuelve el de evaluar x | y.