2008-09-15 18 views
25

¿Hay alguna forma factible de usar genéricos para crear una biblioteca Math que no dependa del tipo base elegido para almacenar datos?Creación de una biblioteca Math usando Generics en C#

En otras palabras, supongamos que quiero escribir una clase Fraction. La fracción se puede representar con dos entradas o dos dobles o lo que sea. Lo importante es que las cuatro operaciones aritméticas básicas están bien definidas. Entonces, me gustaría poder escribir Fraction<int> frac = new Fraction<int>(1,2) y/o Fraction<double> frac = new Fraction<double>(0.1, 1.0).

Desafortunadamente, no existe una interfaz que represente las cuatro operaciones básicas (+, -, *, /). ¿Alguien ha encontrado una forma factible y factible de implementar esto?

Respuesta

25

Aquí es una manera de abstraer los operadores que es relativamente indolora.

abstract class MathProvider<T> 
    { 
     public abstract T Divide(T a, T b); 
     public abstract T Multiply(T a, T b); 
     public abstract T Add(T a, T b); 
     public abstract T Negate(T a); 
     public virtual T Subtract(T a, T b) 
     { 
      return Add(a, Negate(b)); 
     } 
    } 

    class DoubleMathProvider : MathProvider<double> 
    { 
     public override double Divide(double a, double b) 
     { 
      return a/b; 
     } 

     public override double Multiply(double a, double b) 
     { 
      return a * b; 
     } 

     public override double Add(double a, double b) 
     { 
      return a + b; 
     } 

     public override double Negate(double a) 
     { 
      return -a; 
     } 
    } 

    class IntMathProvider : MathProvider<int> 
    { 
     public override int Divide(int a, int b) 
     { 
      return a/b; 
     } 

     public override int Multiply(int a, int b) 
     { 
      return a * b; 
     } 

     public override int Add(int a, int b) 
     { 
      return a + b; 
     } 

     public override int Negate(int a) 
     { 
      return -a; 
     } 
    } 

    class Fraction<T> 
    { 
     static MathProvider<T> _math; 
     // Notice this is a type constructor. It gets run the first time a 
     // variable of a specific type is declared for use. 
     // Having _math static reduces overhead. 
     static Fraction() 
     { 
      // This part of the code might be cleaner by once 
      // using reflection and finding all the implementors of 
      // MathProvider and assigning the instance by the one that 
      // matches T. 
      if (typeof(T) == typeof(double)) 
       _math = new DoubleMathProvider() as MathProvider<T>; 
      else if (typeof(T) == typeof(int)) 
       _math = new IntMathProvider() as MathProvider<T>; 
      // ... assign other options here. 

      if (_math == null) 
       throw new InvalidOperationException(
        "Type " + typeof(T).ToString() + " is not supported by Fraction."); 
     } 

     // Immutable impementations are better. 
     public T Numerator { get; private set; } 
     public T Denominator { get; private set; } 

     public Fraction(T numerator, T denominator) 
     { 
      // We would want this to be reduced to simpilest terms. 
      // For that we would need GCD, abs, and remainder operations 
      // defined for each math provider. 
      Numerator = numerator; 
      Denominator = denominator; 
     } 

     public static Fraction<T> operator +(Fraction<T> a, Fraction<T> b) 
     { 
      return new Fraction<T>(
       _math.Add(
        _math.Multiply(a.Numerator, b.Denominator), 
        _math.Multiply(b.Numerator, a.Denominator)), 
       _math.Multiply(a.Denominator, b.Denominator)); 
     } 

     public static Fraction<T> operator -(Fraction<T> a, Fraction<T> b) 
     { 
      return new Fraction<T>(
       _math.Subtract(
        _math.Multiply(a.Numerator, b.Denominator), 
        _math.Multiply(b.Numerator, a.Denominator)), 
       _math.Multiply(a.Denominator, b.Denominator)); 
     } 

     public static Fraction<T> operator /(Fraction<T> a, Fraction<T> b) 
     { 
      return new Fraction<T>(
       _math.Multiply(a.Numerator, b.Denominator), 
       _math.Multiply(a.Denominator, b.Numerator)); 
     } 

     // ... other operators would follow. 
    } 

Si usted no puede aplicar un tipo que utiliza, usted recibirá un fallo en tiempo de ejecución en lugar de en tiempo de compilación (que es malo). La definición de las implementaciones MathProvider<T> siempre será la misma (también mala). Sugeriría que evite hacer esto en C# y use F # o algún otro idioma que se adapte mejor a este nivel de abstracción.

Editar: Definiciones fijas de sumar y restar para Fraction<T>. Otra cosa interesante y simple de hacer es implementar un MathProvider que opera en un árbol de sintaxis abstracta. Esta idea apunta inmediatamente a hacer cosas como la diferenciación automática: http://conal.net/papers/beautiful-differentiation/

+0

nice an clean :) –

+1

De manera general, creo que MathProvider debería convertirse en una interfaz y restar en un método de interfaz común o podría implementarse como un método de extensión. Eso, por otro lado, no permitiría anularlo. – dalle

+0

Me pregunto sobre el rendimiento de su solución ... Funciona muy bien solo si todo está en línea ... –

1

Primero, su clase debe limitar el parámetro genérico a primitivas (Fracción de clase pública donde T: struct, new()).

En segundo lugar, es probable que necesite crear implicit cast overloads para que pueda manejar la conversión de un tipo a otro sin que el compilador lloriquee.

En tercer lugar, también puede sobrecargar los cuatro operadores básicos para hacer que la interfaz sea más flexible al combinar fracciones de diferentes tipos.

Por último, debe considerar cómo maneja la aritmética por encima y por debajo de los flujos. Una buena biblioteca va a ser extremadamente explícita en la forma en que maneja los desbordamientos; de lo contrario, no puedes confiar en el resultado de las operaciones de diferentes tipos de fracciones.

+1

El problema es que ni siquiera puedo hacer sumas así porque las estructuras no tienen un operador adicional definido. – Sklivvz

+0

http://msdn.microsoft.com/en-us/library/aa691324(VS.71).Las implementaciones aspx "definidas por el usuario pueden introducirse incluyendo declaraciones de operador en clases y estructuras" – Will

2

Aquí hay un problema sutil que viene con los tipos genéricos. Supongamos que un algoritmo implica división, digamos eliminación gaussiana para resolver un sistema de ecuaciones. Si transfiere enteros, obtendrá una respuesta incorrecta porque llevará a cabo entero división. Pero si pasa argumentos dobles que suceden tienen valores enteros, obtendrá la respuesta correcta.

Lo mismo ocurre con las raíces cuadradas, como en la factorización de Cholesky. Factorizar una matriz entera saldrá mal, mientras que factorizar una matriz de dobles que tenga valores enteros estará bien.