Pregunta

¿Existe alguna forma viable de utilizar genéricos para crear una biblioteca matemática que no dependa del tipo de base elegido para almacenar datos?

En otras palabras, supongamos que quiero escribir una clase Fracción.La fracción se puede representar mediante dos enteros o dos dobles o cualquier otra cosa.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).

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

¿Fue útil?

Solución

Aquí hay una manera de abstraer a los operadores que es relativamente sencilla.

    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 no implementa un tipo que usa, obtendrá una falla en tiempo de ejecución en lugar de en tiempo de compilación (eso es malo).La definición de la MathProvider<T> las implementaciones siempre serán las mismas (también malas).Le sugeriría que evite hacer esto en C# y use F# o algún otro lenguaje más adecuado para este nivel de abstracción.

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

Otros consejos

Creo que esto responde a tu pregunta:

http://www.codeproject.com/KB/cs/genericnumerics.aspx

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 pasa números enteros, obtendrá una respuesta incorrecta porque realizará entero división.Pero si pasas argumentos dobles que tienen valores enteros, obtendrás la respuesta correcta.

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

Primero, su clase debe limitar el parámetro genérico a primitivos (clase pública Fracción donde T:estructura, nuevo()).

En segundo lugar, probablemente necesitarás crear sobrecargas de conversión implícitas para que puedas manejar la conversión de un tipo a otro sin que el compilador llore.

En tercer lugar, también puede sobrecargar los cuatro operadores básicos para 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.Una buena biblioteca será extremadamente explícita en cómo maneja los desbordamientos;de lo contrario no se puede confiar en el resultado de operaciones con diferentes tipos de fracciones.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top