CodeGym /Cours /C# SELF /Surcharge des opérateurs en C#

Surcharge des opérateurs en C#

C# SELF
Niveau 34 , Leçon 2
Disponible

1. Introduction

Tu dois sûrement être habitué à utiliser des opérateurs comme + pour additionner des nombres ou == pour comparer. Ces opérateurs marchent nickel avec les types de base. Mais si tu crées ta propre classe, genre Vector (vecteur), et que tu veux que l’addition de deux vecteurs soit naturelle, genre vector1 + vector2 ? Ou que la comparaison de deux objets Money (argent) fonctionne comme moneyAmount1 == moneyAmount2 ? C’est justement pour ça qu’il y a la surcharge des opérateurs en C#.

La surcharge des opérateurs te permet de définir comment les opérateurs standards (+, -, *, /, ==, !=, >, <, ++, -- et plein d’autres) doivent fonctionner avec les instances de tes propres classes ou structs. Ça rend ton code plus intuitif, lisible et expressif, en te permettant d’utiliser une syntaxe familière pour tes types custom.

Bases de la surcharge des opérateurs

Pour surcharger un opérateur, tu déclares une méthode statique dans ta classe ou struct, avec le mot-clé operator suivi du symbole de l’opérateur que tu veux surcharger.

  • Méthode statique : La méthode de surcharge doit toujours être public static.
  • Nom de la méthode : Le nom, c’est le mot-clé operator suivi du symbole de l’opérateur (genre operator +, operator ==).
  • Paramètres : Le nombre de paramètres dépend du type d’opérateur (unaire ou binaire).
    • Opérateurs unaires (genre +, -, !, ++, --) : prennent un paramètre du type de la classe/struct où ils sont définis.
    • Opérateurs binaires (genre +, -, *, /, ==, !=) : prennent deux paramètres, dont au moins un doit être du type de la classe/struct où ils sont définis.
  • Type de retour : Le type que l’opération retourne.

2. Surcharge des opérateurs binaires (Binary Operators)

Les opérateurs binaires prennent deux opérandes. Les plus courants, c’est les opérateurs arithmétiques (+, -, *, /) et les opérateurs de comparaison (==, !=, >, <, >=, <=).

Exemple : Surcharge de l’opérateur d’addition (+)

public struct Point
{
    public int X { get; set; }
    public int Y { get; set; }

    public Point(int x, int y) => (X, Y) = (x, y);

    // Surcharge de l’opérateur +
    public static Point operator +(Point p1, Point p2)
    {
        return new Point(p1.X + p2.X, p1.Y + p2.Y);
    }

    public override string ToString() => $"({X}, {Y})";
}

// Utilisation :
Point point1 = new Point(1, 2);
Point point2 = new Point(3, 4);
Point sumPoint = point1 + point2; // Ici on appelle l’opérateur + surchargé

Console.WriteLine($"Somme des points : {sumPoint}"); // Affiche : Somme des points : (4, 6)

Ici, on a défini que l’addition de deux objets Point doit retourner un nouvel objet Point dont les coordonnées sont la somme des coordonnées des points d’origine.

Exemple : Surcharge de l’opérateur de multiplication (*) par un scalaire (nombre)

public struct Vector
{
    public double X { get; set; }
    public double Y { get; set; }

    public Vector(double x, double y) => (X, Y) = (x, y);

    // Surcharge de l’opérateur * (vecteur * nombre)
    public static Vector operator *(Vector vec, double scalar)
    {
        return new Vector(vec.X * scalar, vec.Y * scalar);
    }

    // Surcharge de l’opérateur * (nombre * vecteur) - pour la symétrie
    public static Vector operator *(double scalar, Vector vec)
    {
        return new Vector(vec.X * scalar, vec.Y * scalar);
    }

    public override string ToString() => $"<{X}, {Y}>";
}

// Utilisation :
Vector vec1 = new Vector(2, 3);
Vector scaledVec1 = vec1 * 5;      // Appelle Vector * double
Vector scaledVec2 = 5 * vec1;      // Appelle double * Vector

Console.WriteLine($"Vecteur mis à l’échelle 1 : {scaledVec1}"); // Affiche : Vecteur mis à l’échelle 1 : <10, 15>
Console.WriteLine($"Vecteur mis à l’échelle 2 : {scaledVec2}"); // Affiche : Vecteur mis à l’échelle 2 : <10, 15>

Remarque : pour l’opérateur de multiplication, on l’a surchargé deux fois pour qu’il marche dans les deux sens : Vector * double et double * Vector.

Exemple : Surcharge des opérateurs de comparaison (== et !=)

Quand tu surcharges == et !=, il y a un contrat important : si tu en surcharges un, tu dois aussi surcharger l’autre. Aussi, il est super conseillé que le comportement de ces opérateurs colle à celui de la méthode Equals() et GetHashCode().

public class Money
{
    public decimal Amount { get; set; }
    public string Currency { get; set; }

    public Money(decimal amount, string currency)
    {
        Amount = amount;
        Currency = currency;
    }

    // On doit override Equals et GetHashCode si on surcharge == / !=
    public override bool Equals(object? obj)
    {
        if (obj is not Money other) return false;
        return Amount == other.Amount && Currency == other.Currency;
    }

    public override int GetHashCode() => HashCode.Combine(Amount, Currency);

    // Surcharge de l’opérateur ==
    public static bool operator ==(Money? m1, Money? m2)
    {
        // Vérif null pour les types référence
        if (ReferenceEquals(m1, null)) return ReferenceEquals(m2, null);
        return m1.Equals(m2); // On utilise notre Equals overridé
    }

    // Surcharge de l’opérateur != (obligatoire si on surcharge ==)
    public static bool operator !=(Money? m1, Money? m2)
    {
        return !(m1 == m2);
    }
}

// Utilisation :
Money cash1 = new Money(100, "USD");
Money cash2 = new Money(100, "USD");
Money cash3 = new Money(50, "USD");
Money cash4 = new Money(100, "EUR");

Console.WriteLine($"cash1 == cash2: {cash1 == cash2}"); // True
Console.WriteLine($"cash1 == cash3: {cash1 == cash3}"); // False
Console.WriteLine($"cash1 == cash4: {cash1 == cash4}"); // False

Pour les types valeur (structs), les opérateurs == et != font par défaut une comparaison bit à bit, ce qui est souvent ok. Mais pour les classes (class), par défaut == compare les références, donc la surcharge est super utile pour comparer les valeurs.

3. Surcharge des opérateurs unaires (Unary Operators)

Les opérateurs unaires bossent avec un seul opérande. Exemples : + (plus unaire), - (moins unaire), ! (NON logique), ~ (NON bit à bit), ++ (incrément), -- (décrément).

Exemple : Surcharge du moins unaire (-)

public struct Vector3D
{
    public double X, Y, Z;

    public Vector3D(double x, double y, double z) => (X, Y, Z) = (x, y, z);

    // Surcharge de l’opérateur unaire -
    public static Vector3D operator -(Vector3D vec)
    {
        return new Vector3D(-vec.X, -vec.Y, -vec.Z);
    }

    public override string ToString() => $"<{X}, {Y}, {Z}>";
}

// Utilisation :
Vector3D originalVec = new Vector3D(1, -2, 3);
Vector3D invertedVec = -originalVec; // Appelle l’opérateur unaire - surchargé

Console.WriteLine($"Vecteur d’origine : {originalVec}"); // Affiche : Vecteur d’origine : <1, -2, 3>
Console.WriteLine($"Vecteur inversé : {invertedVec}"); // Affiche : Vecteur inversé : <-1, 2, -3>

Exemple : Surcharge des opérateurs d’incrément (++) et de décrément (--)

Ces opérateurs modifient l’opérande et renvoient la nouvelle valeur.

public struct Counter
{
    public int Value { get; set; }

    public Counter(int value) => Value = value;

    // Surcharge de l’opérateur d’incrément ++
    public static Counter operator ++(Counter c)
    {
        // Important : on retourne un NOUVEL objet si la struct est immutable,
        // ou on modifie l’actuel et on le retourne si c’est une classe
        return new Counter(c.Value + 1);
    }

    // Surcharge de l’opérateur de décrément --
    public static Counter operator --(Counter c)
    {
        return new Counter(c.Value - 1);
    }

    public override string ToString() => $"[Compteur : {Value}]";
}

// Utilisation :
Counter myCounter = new Counter(5);
myCounter++; // Incrément postfixé
Console.WriteLine(myCounter); // Affiche : [Compteur : 6]

++myCounter; // Incrément préfixé
Console.WriteLine(myCounter); // Affiche : [Compteur : 7]

myCounter--;
Console.WriteLine(myCounter); // Affiche : [Compteur : 6]

Les opérateurs ++ et -- surchargés sont toujours préfixés (ils modifient d’abord, puis renvoient). Le compilateur gère tout seul le comportement postfixé.

4. Surcharge des opérateurs de conversion de type

Tu peux définir comment les instances de ton type peuvent être converties explicitement ou implicitement vers un autre type, et inversement.

  • implicit (conversion implicite) : À utiliser quand la conversion est toujours safe et sans perte de données (genre int vers long).
  • explicit (conversion explicite) : À utiliser quand la conversion peut causer une perte de données ou une erreur, et nécessite d’être indiquée explicitement ((Type)obj).

Exemple : Conversion implicite de Score vers int

public struct Score
{
    public int Points { get; set; }
    public Score(int points) => Points = points;

    // Conversion implicite Score vers int
    public static implicit operator int(Score s)
    {
        return s.Points;
    }
}

// Utilisation :
Score examScore = new Score(95);
int scoreValue = examScore; // Conversion implicite
Console.WriteLine($"Points : {scoreValue}"); // Affiche : Points : 95

Exemple : Conversion explicite de Celsius vers Fahrenheit

public struct Celsius
{
    public double Degrees { get; set; }
    public Celsius(double degrees) => Degrees = degrees;
}

public struct Fahrenheit
{
    public double Degrees { get; set; }
    public Fahrenheit(double degrees) => Degrees = degrees;

    // Conversion explicite Fahrenheit vers Celsius
    public static explicit operator Celsius(Fahrenheit f)
    {
        return new Celsius((f.Degrees - 32) * 5 / 9);
    }

    // Conversion explicite Celsius vers Fahrenheit
    public static explicit operator Fahrenheit(Celsius c)
    {
        return new Fahrenheit(c.Degrees * 9 / 5 + 32);
    }
}

// Utilisation :
Celsius c = new Celsius(25);
Fahrenheit f = (Fahrenheit)c; // Conversion explicite
Console.WriteLine($"25°C = {f.Degrees}°F"); // Affiche : 25°C = 77°F

Fahrenheit f2 = new Fahrenheit(212);
Celsius c2 = (Celsius)f2; // Conversion explicite
Console.WriteLine($"212°F = {c2.Degrees}°C"); // Affiche : 212°F = 100°C

5. Limitations et recommandations

Tu ne peux pas surcharger tous les opérateurs : Impossible de surcharger des opérateurs comme &&, ||, ?., new, typeof, is, as, == (pour string, car il est déjà surchargé dans la classe string et a un comportement spécial), () (appel de méthode), = (assignation) et quelques autres.

Symétrie : Si tu surcharges un opérateur binaire, c’est souvent utile de le surcharger aussi pour l’ordre inverse des opérandes, si ça a du sens (comme dans l’exemple Vector * double).

Contrats : Respecte toujours les contrats, surtout pour les opérateurs de comparaison (==, !=, >, <, >=, <=) et leur lien avec Equals() et GetHashCode(). Si tu ne respectes pas, tu risques d’avoir des bugs chelous, surtout dans les collections.

Lisibilité et intuitivité : Surcharge les opérateurs seulement si ça rend le code plus intuitif et lisible. Si le comportement de l’opérateur n’est pas évident ou peut prêter à confusion, préfère des méthodes classiques avec des noms explicites (genre Add() au lieu de +).

Objets mutables : Fais gaffe quand tu surcharges des opérateurs pour des types mutables. Par exemple, Vector v1 = v2 + v3; crée par défaut un nouvel objet, il ne modifie pas v2. Si ton opérateur modifie l’objet existant, ça peut surprendre.

Structs vs Classes : La surcharge des opérateurs est plus courante pour les structs, qui représentent souvent des valeurs (genre Point, ComplexNumber), et pour lesquels les opérations mathématiques ou les comparaisons de valeurs sont naturelles.

2
Mission
C# SELF, niveau 34, leçon 2
Bloqué
Surcharge de l'opérateur binaire d'addition
Surcharge de l'opérateur binaire d'addition
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION