Pergunta

Existe alguma maneira viável de usar genéricos para criar uma biblioteca Math que não dependa do tipo base escolhido para armazenar dados?

Em outras palavras, vamos supor que eu queira escrever uma classe Fraction.A fração pode ser representada por dois inteiros ou dois duplos ou outros enfeites.O importante é que as quatro operações aritméticas básicas estejam bem definidas.Então, eu gostaria de poder escrever Fraction<int> frac = new Fraction<int>(1,2) e/ou Fraction<double> frac = new Fraction<double>(0.1, 1.0).

Infelizmente não existe nenhuma interface que represente as quatro operações básicas (+,-,*,/).Alguém encontrou uma maneira viável e viável de implementar isso?

Foi útil?

Solução

Aqui está uma maneira de abstrair os operadores que é relativamente simples.

    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.
    }

Se você não conseguir implementar um tipo que usa, ocorrerá uma falha em tempo de execução em vez de em tempo de compilação (isso é ruim).A definição do MathProvider<T> as implementações sempre serão as mesmas (também ruins).Eu sugeriria que você evitasse fazer isso em C# e usasse F# ou alguma outra linguagem mais adequada a esse nível de abstração.

Editar: Definições fixas de adição e subtração para Fraction<T>.Outra coisa interessante e simples de fazer é implementar um MathProvider que opera em uma árvore de sintaxe abstrata.Essa ideia aponta imediatamente para fazer coisas como diferenciação automática: http://conal.net/papers/beautiful-diferenciation/

Outras dicas

Acredito que isso responde à sua pergunta:

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

Aqui está um problema sutil que surge com tipos genéricos.Suponha que um algoritmo envolva divisão, digamos, eliminação gaussiana para resolver um sistema de equações.Se você passar números inteiros, obterá uma resposta errada porque realizará inteiro divisão.Mas se você passar argumentos duplos que tenham valores inteiros, você obterá a resposta certa.

O mesmo acontece com raízes quadradas, como na fatoração de Cholesky.Fatorar uma matriz inteira dará errado, enquanto fatorar uma matriz dupla que tenha valores inteiros será bom.

Primeiro, sua classe deve limitar o parâmetro genérico a primitivos ( public class Fraction where T :estrutura, new() ).

Segundo, você provavelmente precisará criar sobrecargas de conversão implícitas para que você possa lidar com a conversão de um tipo para outro sem que o compilador chore.

Terceiro, você também pode sobrecarregar os quatro operadores básicos para tornar a interface mais flexível ao combinar frações de tipos diferentes.

Por último, você deve considerar como está lidando com overflows e underflows aritméticos.Uma boa biblioteca será extremamente explícita na forma como lida com overflows;caso contrário, você não poderá confiar no resultado de operações de diferentes tipos de frações.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top