Question

Existe-t-il un moyen réaliste d'utiliser des génériques pour créer une bibliothèque mathématique ne dépendant pas du type de base choisi pour stocker les données?

En d'autres termes, supposons que je veuille écrire une classe de fractions. La fraction peut être représentée par deux ints ou deux doubles ou autres. L'important est que les quatre opérations arithmétiques de base soient bien définies. Donc, j'aimerais pouvoir écrire Fraction<int> frac = new Fraction<int>(1,2) et / ou Fraction<double> frac = new Fraction<double>(0.1, 1.0).

Malheureusement, il n’existe aucune interface représentant les quatre opérations de base (+, -, *, /). Quelqu'un at-il trouvé un moyen pratique et réalisable de le mettre en œuvre?

Était-ce utile?

La solution

Voici un moyen d’abriter les opérateurs relativement simple.

    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 vous ne parvenez pas à implémenter le type que vous utilisez, vous obtiendrez un échec au moment de l'exécution et non au moment de la compilation (ce qui est mauvais). La définition des MathProvider<T> implémentations sera toujours la même (aussi mauvaise). Je suggérerais que vous évitiez simplement de faire cela en C # et que vous utilisiez F # ou un autre langage mieux adapté à ce niveau d'abstraction.

Modifier : définitions fixes de l'addition et de la soustraction pour Fraction<T>. Une autre chose simple et intéressante à faire est de mettre en œuvre un MathProvider qui fonctionne sur un arbre de syntaxe abstrait. Cette idée renvoie immédiatement à la différenciation automatique: http://conal.net/papers/beautiful-differentiation /

Autres conseils

Je crois que cela répond à votre question:

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

Voici un problème subtil associé aux types génériques. Supposons qu'un algorithme implique une division, par exemple une élimination gaussienne pour résoudre un système d'équations. Si vous passez des nombres entiers, vous obtiendrez une mauvaise réponse car vous effectuerez une division entier . Mais si vous transmettez des arguments doubles ayant des valeurs entières, vous obtiendrez la bonne réponse.

La même chose se produit avec les racines carrées, comme dans la factorisation de Cholesky. La factorisation d'une matrice entière va mal se faire, alors que la factorisation d'une matrice de doublons ayant des valeurs entières se passera bien.

Tout d’abord, votre classe devrait limiter le paramètre générique aux primitives (classe publique Fraction où T: struct, new ()).

Deuxièmement, vous devrez probablement créer implicite surchargez les castes afin que vous puissiez gérer la diffusion d'un type à l'autre sans les pleurs du compilateur.

Troisièmement, vous pouvez également surcharger les quatre opérateurs de base pour rendre l'interface plus flexible lors de la combinaison de fractions de types différents.

Enfin, vous devez examiner la manière dont vous gérez les dépassements et les dépassements arithmétiques. Une bonne bibliothèque va être extrêmement explicite dans la façon dont elle gère les débordements; sinon, vous ne pouvez pas faire confiance au résultat des opérations de différents types de fraction.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top