2008-09-15 5 views
25

Gibt es eine Möglichkeit, Generics zum Erstellen einer Math-Bibliothek zu verwenden, die nicht vom Basistyp abhängt, der zum Speichern von Daten ausgewählt wurde?Erstellen einer Math-Bibliothek mithilfe von Generics in C#

Mit anderen Worten, nehmen wir an, ich möchte eine Fraktionsklasse schreiben. Der Bruch kann durch zwei Ints oder zwei Doppel oder dergleichen dargestellt werden. Wichtig ist, dass die vier Grundrechenarten gut definiert sind. Also würde ich gerne Fraction<int> frac = new Fraction<int>(1,2) und/oder Fraction<double> frac = new Fraction<double>(0.1, 1.0) schreiben können.

Leider gibt es keine Schnittstelle für die vier Grundoperationen (+, -, *, /). Hat jemand eine praktikable, praktikable Art gefunden, dies zu implementieren?

Antwort

25

Hier ist ein Weg, um die Betreiber zu abstrahieren, die relativ schmerzlos ist.

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

Wenn Sie eine Art Verpflichtung zur Durchführung nicht, die Sie verwenden, werden Sie einen Fehler zur Laufzeit erhalten statt zum Zeitpunkt der Kompilierung (das ist schlecht). Die Definition der MathProvider<T> Implementierungen wird immer gleich (auch schlecht) sein. Ich würde vorschlagen, dass Sie es einfach vermeiden, dies in C# zu tun und F # oder eine andere Sprache zu verwenden, die für dieses Abstraktionsniveau besser geeignet ist.

Bearbeiten: Feste Definitionen von addieren und subtrahieren für Fraction<T>. Eine weitere interessante und einfache Aufgabe ist die Implementierung eines MathProviders, der in einem abstrakten Syntaxbaum arbeitet. Diese Idee zeigt sofort auf Dinge wie automatische Differenzierung: http://conal.net/papers/beautiful-differentiation/

+0

schön ein sauber :) –

+1

In der allgemeinen Weise denke ich, MathProvider sollte in eine Schnittstelle gemacht werden und subtrahieren in eine gewöhnliche Schnittstelle Methode oder es könnte als eine Extension-Methode implementiert werden. Das würde andererseits es außer Kraft setzen. – dalle

+0

Ich frage mich über die Leistung Ihrer Lösung ... Es funktioniert nur gut, wenn alles inline ist ... –

1

Zunächst sollte Ihre Klasse den generischen Parameter auf Primitive beschränken (öffentliche Klasse Fraction, wobei T: struct, new()).

Zweitens müssen Sie wahrscheinlich erstellen implicit cast overloads, damit Sie Casting von einem Typ zum anderen ohne den Compiler weinen können.

Drittens können Sie auch die vier Grundoperatoren überladen, um die Schnittstelle flexibler zu machen, wenn Sie Bruchteile verschiedener Typen kombinieren.

Schließlich müssen Sie überlegen, wie Sie mit arithmetischen Über- und Unterschreitungen umgehen. Eine gute Bibliothek wird extrem explizit sein, wenn es um Überläufe geht; Andernfalls können Sie dem Ergebnis von Operationen verschiedener Fraktionstypen nicht vertrauen.

+1

Das Problem ist, dass ich nicht einmal Summen wie diese tun kann, da Strukturen keinen Additionsoperator definiert haben. – Sklivvz

+0

http://msdn.microsoft.com/en-us/library/aa691324(VS.71).aspx "benutzerdefinierte Implementierungen können durch Einbeziehung von Operatordeklarationen in Klassen und Strukturen eingeführt werden" – Will

2

Hier ist ein subtiles Problem, das mit generischen Typen kommt. Angenommen, ein Algorithmus beinhaltet eine Division, sagen wir Gaußsche Eliminierung, um ein Gleichungssystem zu lösen. Wenn Sie Ganzzahlen übergeben, erhalten Sie eine falsche Antwort, weil Sie Integer Division ausführen. Wenn Sie jedoch doppelte Argumente mit ganzzahligen Werten übergeben, erhalten Sie die richtige Antwort.

Das Gleiche passiert mit Quadratwurzeln, wie bei der Cholesky-Faktorisierung. Das Factoring einer Integer-Matrix wird schiefgehen, während die Faktorisierung einer Matrix von Doubles, die zufällig ganzzahlige Werte haben, in Ordnung ist.