C# – Classe Rationnel

Pour un exemple semblable (mais plus complet) en C++, voir ceci.

Cet article se veut un petit exemple illustrant les propriétés et certains opérateurs en C#, le tout à partir d'une classe s'y prêtant : une classe Rationnel, dont chaque instance représente un élément de la forme . J'ai escamoté la gestion du signe d'un Rationnel pour que le tout reste simple (mais vous pouvez corriger cet irritant si vous le souhaitez!).

Un petit exemple de programme de test pour tout cela serait celui proposé à droite (https://dotnetfiddle.net/hPBpWN) . Ce n'est, bien sûr, qu'une ébauche sans prétention.

using System;
using System.Collections.Generic;

Rationnel r0 = new (2, 3),
          r1 = new (5, 2);
Console.WriteLine($"{r0}, {r1}"); // utilise Rationnel.ToString()
Console.WriteLine("{0}", new Rationnel(3, 6).Simplifier());
r0 = new (3, 6);
r1 = new (1, 2);
Console.WriteLine($"{r0} {(r0 == r1 ? "==" : "!=")} {r1}");
r0 = new (1, 4);
r1 = new (1, 2);
Console.WriteLine($"{r0} {(r0 == r1 ? "==" : "!=")} {r1}");
Console.WriteLine($"Symbolique : {r0 + r1}, décimal : {(float)(r0 + r1)}");

Puisqu'un Rationnel représente une division entière symbolique, le problème potentiel de la division par zéro doit être pris en charge. Sans grande surprise, dans ce programme, une tentative de division par zéro sur un Rationnel mènera à une levée d'exception.

class DivisionParZéroException : Exception { }

Il arrivera que nous souhaitions simplifier un Rationnel , et il se trouve que cette simplification se calcule bien par une application d'un algorithme de recherche du plus grand commun diviseur entre le numérateur et le dénominateur .

Plus précisément : si , alors . Ainsi, la méthode privée Pgcd(a,b) nous aidera à réaliser cette opération lorsque cela s'avèrera opportun.

class Rationnel : IEquatable<Rationnel>
{
   //
   // pour faciliter la simplification
   //
   private static int Pgcd(int a, int b) =>
      b != 0 ? Pgcd(a, a % b) : a;

Le numérateur est implémenté ici sous forme de propriété automatique, ne demandant aucune validation. Le dénominateur, lui, est plus détaillé puisque son implémentation implique une validation (il ne peut être nul).

   private int dénominateur;
   public int Numérateur { get; init; }
   public int Dénominateur
   {
      get => dénominateur;
      init
      {
         if (value == 0)
            throw new DivisionParZéroException();
         dénominateur = value;
      }
   }

Nous supportons deux formes de construction, soit celle recevant à la fois un numérateur et un dénominateur et celle ne recevant qu'un numérateur (le dénominateur étant alors implicitement 1). Notez que, tel que mentionné plus haut, nous avons fait abstraction de la question de la gestion du signe.

   public Rationnel(int num, int dénom)
   {
      Numérateur = num;
      Dénominateur = dénom;
   }
   public Rationnel(int num)
   {
      Numérateur = num;
      Dénominateur = 1;
   }

La simplification d'un Rationnel se fait tel que décrit plus haut (trouve un plus grand commun diviseur du numérateur et du dénominateur, puis retourner un Rationnel identique à l'original, à ceci près que son numérateur et son dénominateur sont tous deux divisés par cette valeur.

Ce n'est pas à strictement parler nécessaire, mais c'est amusant.

   public Rationnel Simplifier()
   {
      int pgcd = Pgcd(Numérateur, Dénominateur);
      return new Rationnel(Numérateur / pgcd, Dénominateur / pgcd);
   }

Les opérateurs sont des méthodes de classe (qualifiées static). Le langage C# est un peu... oppressant, du moins à mes yeux, en ce que pour implémenter ==, il faut implémenter != (ce qui est raisonnable), mais aussi (du moins si on souhaite respecter les usages) Equals(object) (ce qui est acceptable) et GetHashCode() (ce qui, sans être inacceptable, est un peu abusif).

Les méthodes polymorphiques (qui, comme en C++, doivent être explicitement qualifiées virtual, même si notre exemple ne le montre pas ici), lorsque surchargées, doivent l'être de manière explicite, avec le mot clé override (que vous voyez ici apposé à GetHashCode() et à Equals()).

Dans l'implémentation d'Equals(object o), remarquez le test pour valider que le paramètre o soit non nul, et (puisque Equals() prend un object en paramètre) la validation (downcast) que obj mène au moins vers un Rationnel, à l'aide de l'opérateur is.

   public static bool operator ==(Rationnel r0, Rationnel r1) =>
      r0.Equals(r1);
   //
   // écrire == oblige d'écrire !=
   //
   public static bool operator !=(Rationnel r0, Rationnel r1) =>
      !(r0 == r1);
   //
   // écrire == exige d'écrire Equals(object) pour qui
   // ne se limitera pas à C# sur la plateforme .NET
   // Notez que la surcharge polymorphique doit être explicite
   //
   public override bool Equals(object obj) =>
      obj is Rationnel r && Equals(r);
   //
   // IEquatable<Rationnel>
   //
   public bool Equals(Rationnel r)
   {
      Rationnel r0 = Simplifier(),
                r1 = r.Simplifier();
      return r0.Numérateur == r1.Numérateur &&
             r0.Dénominateur == r1.Dénominateur;
   }
   //
   // écrire Equals(object) mène à écrire GetHashCode()... >soupir!<
   // Notez que la surcharge polymorphique doit être explicite
   //
   public override int GetHashCode() =>
      Numérateur.GetHashCode() ^ Dénominateur.GetHashCode(); // bof

Les opérateurs relationnels définissant un ordonnancement peuvent aussi être surchargés. Le truc pour se simplifier l'existence, réduire l'effort d'entretien de code source et éviter d'introduire accidentellement des opérations qui ne respectent pas les règles usuelles de l'artihmétique est d'écrire un des quatre opérateurs en question et d'exprimer les trois autres en termes de celui-ci et de la négation logique.

Habituellement, on codera d'abord l'opérateur <, puis le reste suivra.

   public static bool operator <(Rationnel r0, Rationnel r1) =>
      r0.Numérateur * r1.Dénominateur < r1.Numérateur * r0.Dénominateur;
   public static bool operator >(Rationnel r0, Rationnel r1) =>
      r1 < r0;
   public static bool operator <=(Rationnel r0, Rationnel r1) =>
      !(r1 < r0);
   public static bool operator >=(Rationnel r0, Rationnel r1) =>
      !(r0 < r1);

Il est bien sûr possible d'implémenter les opérateurs arithmétiques. J'ai exprimé l'opérateur d'addition binaire, à droite; vous pourrez vous en inspirer pour compléter le portait si cela vous semble opportun.

   public static Rationnel operator +(Rationnel r0, Rationnel r1) =>
      new
      (
         r0.Numérateur * r1.Dénominateur + r1.Numérateur * r0.Dénominateur,
         r0.Dénominateur * r1.Dénominateur
      );

Lorsqu'un objet est utilisé là où une string pourrait être utilisée, C# peut en tirer profit et utiliser, si cela s'avère opportun, la méthode ToString() de l'objet en question. Cette technique est mise en application à quelques reprises dans le programme principal.

   //
   // Traiter un Rationnel comme une string
   //
   public override string ToString() =>
      $"{Numérateur}/{Dénominateur}";

Enfin, il est possible d'exprimer le code à exécuter lors d'un transtypage (un Cast) sur un Rationnel. Ce transtypage peut être implicite (mot clé implicit), donc réalisé par le compilateur lorsqu'il le juge opportun, mais cela peut jouer des tours. Il peut aussi être explicite (notre choix ici).

Détail technique : le transtypage se fait comme en C ou en Java, au sens où (T)expr demande de traiter l'expression expr comme étant de type T.

   //
   // Traiter un Rationnel comme un float ou comme un double
   //
   static public explicit operator float(Rationnel r) =>
      ((float)r.Numérateur) / r.Dénominateur;
   static public explicit operator double(Rationnel r) =>
      ((double)r.Numérateur) / r.Dénominateur;
}

Lectures complémentaires

Quelques liens pour enrichir le propos.


Valid XHTML 1.0 Transitional

CSS Valide !