La sobrecarga de operadores es una poderosa herramienta de programación. Con ella podemos por ejemplo, obtener estadísticas de todas las operaciones que se realizan en un pograma de forma muy sencilla, de manera que conozcamos que coste en sumas, multiplicaciones, divisiones, etc... tienen determinados algoritmos en la práctica (no en todos los algoritmos es sencillo calcular analíticamente su coste concreto en operaciones por lo que se deben usar análisis empíricos).
Vamos a implementar (en C#) una clase que permita realizar y controlar todas las operaciones que habitualmente se realizan sobre números en coma flotante, y ésto, sin que debamos realizar ningún tipo de esfuerzo mientras implementamos los algoritmos sobre los que usamos este tipo de números.
Una de las cosas que se suelen hacer cuando se programa en C++ es abstraer el tipo de representación de coma flotante que se va a usar para los cálculos numéricos. Cada representación (principalmente double o float aunque pueden ser otros) tiene sus ventajas y sus inconvenientes, esto habitualmente se hace de tres formas:
En C++, cualquiera de las dos últimas soluciones permitirían definir mediante sobrecarga de operadores lo que pretendemos (pues en la sobrecarga de operadores es independiente de que se usen clases o no), no obstante la más adecuada como veremos es la última.
En C# sólo podemos utilizar el último método, pues ninguno de los dos anteriores están disponibles, sin embargo, para nuestros propósitos no pasa nada, pues igualmente pensábamos definir una clase que encapsule toda nuestra sobrecarga.
Vamos a crear un nuevo tipo que encapsule todas las operaciones usuales que se realizan sobre los reales permitíendonos modificar fácilmente el tipo de real a usar en diferentes implementaciones y registrar estadísticas de las operaciones que se realizan sin que debamos modificar ni realizar tareas adicionales en las aplicaciones en las que usemos este tipo.
El registrar estadísticas de todas las operaciones (matemáticas) que realiza un programa conlleva un coste adicional (el que supone realizar ese registro), lógicamente podremos configurar la clase para que las registre o no y, en tal caso, no afecte en absoluto a las versiones release.
Si estás interesado en conocer los detalles de las herramientas de programación que se usan en esta implementación te sugiero amplíes en otras fuentes (por ejemplo en MSDN) pues sólo las expondré superficialmente.
Puesto que pretendemos realizar una representación de los números reales, llamaremos a la clase Real y como no podemos heredar (es realmente una pena) del tipo básico double (ni float) debemos establecer un miembro (privado, por supuesto) que almacene el valor en cuestión. Este es el aspecto que tomará nuestra clase.
Las estadísticas deberán basarse lógicamente en las operaciones que se han realizado y como lo que más nos va a importar es el número de operaciones realizadas de cada tipo (podría ser interesante también la distribución de éstas en el tiempo, el código, según los datos, etc... pero esto es algo mucho más ambicioso) tendremos que mantener un contador para cada una de ellas. Además, estos contadores deben ser globales a toda la aplicación por tanto dichos miembros deberán ser estáticos.
Como querremos que se pueda acceder a estos datos desde fuera de la clase, sin modificarlos, estableceremos dichos valores como propiedades de sólo lectura.
Algo bastante habitual será que nos interese un contador general que aglutine una familia de operaciones, obviamente ese contador no existe (se calcula como la suma de otros), pero será interesante tener una propiedad (de sólo lectura) que calcule dicho valor y no sea preciso hacerlo cada vez.
Ya puestos a publicar, nada cuesta crear un método que devuelva formateados los resúmenes de los contadores, así poco o nada tendremos que hacer para tener una información aproximada.
Ni que decir tiene que tendremos que poder resetear todos los contadores (por ejemplo cuando pasamos de testear una implementación a otra).
Como veremos, la forma más habitual en la que se construirán los objetos de tipo Real en nuestros programas será mediante la conversión implícita, mucho más cómoda; sin embargo, no cuesta nada y parece más correcto crear algunos. Puesto que los vamos a definir, aquí incrementaremos los contadores de conversión.
Como vemos, utilizamos la definición del compilador DEBUG que nos indica cuando se está compilando para depuración, no obstante si quisiéramos contabilizar también las operaciones en una compilación release no cuesta nada cambiar el nombre por cualquier otro, lo que pasa es que será habitual querer sólo las estadísticas en un entorno de desarrollo en el que se prueban los algoritmos.
Algunas conversiones de tipo son implementadas mediante métodos en la clase, como los constructores de antes, no son necesarios, pero dejan más claro y aglutinan otras conversiones menos claras. Tamibién éstas serán las encargadas de incrementar los contadores de conversión.
Unas conversiones mucho más interesantes son las siguientes:
Que las tres primeras conversiones sean implícitas tiene sentido mientras la dirección en la que va la conversión sea sin pérdida (y/o sin un coste computacional elevado), en otro caso, como en la conversión desde una cadena (en la que se puede producir un problema importante de rendimiento y claridad) las conversiones es mejor que sean explícitas, es decir, el programador si desea realizar ese tipo de conversiones debe indicar explícitamente que desea realizar ese tipo de conversión.
Similar ocurre con las conversiones en la otra dirección, en éste caso, sólo permitimos la conversión implícita del tipo en el que se almacena la información, pues en todos los casos existe pérdida de información o de rendimiento.
La clase también es un buen sitio para publicar las constantes más importantes relacionadas con los Reales (de hecho así lo hace la clase estática del Framework .NET).
Para cada instancia, publicamos algunas propiedades interesantes que de las que también deberemos realizar alguna contabilidad como el valor absoluto, si es "casi" cero, si es positivo, etc.
Algo poco habitual pero que me parece interesante es que, ya que definimos la clase para cada instancia, las funciones más comunes que la transforma estén en la misma clase (lo contrario que en el Framework .NET). Así definimos dentro de la misma clase funciones como coseno, seno, tangente, etc.
Pero sin duda, la parte más importante de nuestra clase tiene que ver con la sobrecarga de los operadores que utilizamos normalmente para realizar las operaciones matemáticas (suma, resta, multiplicación, etc.). Y lógicamente las dejaremos contabilizadas en sus respectivos contadores.
Por un lado tenemos los operadores unarios, aquellos en los que sólo interviene una instancia.
Y por el otro, tenemos los operadores binarios, aquellos en los que intervienen dos instancias.
¡Y ya tenemos nuestra clase lista!.
Desgraciadamente, no es habitual que se pueda especificar la precedencia de los operadores sobrecargados. Concretamente en C# no es posible establecer la precedencia de operadores y no es posible por tanto sobrecargar el operador ^ de forma que tenga mayor prioridad que el producto y la división (sí se puede sobrecargar, pero no forzar una prioridad de precedencia mayor). En C# la tabla de precedencia es la siguiente (al menos en Visual C# 2005 versión 8.0):
Aunque podríamos usar otros operadores con mayor prioridad (como []) pierde el sentido de claridad y por tanto es mejor usar un método como x.Pow( y ).
Utilizar nuestra clase dentro de cualquier programa es trivial, podemos usarla exactamente de la misma forma que utilizamos los tipos básicos double o float. Este es un ejemplo trivial y su salida:
No se han terminado aún todas las posibilidades de esta clase, cualquier cosa que esté directamente relacionada con los Reales es susceptible de ser incluida en la clase, la creación de los siguientes métodos parecen buenas ideas: