CodeGym /Corsi /C# SELF /Overloading degli operatori in C#

Overloading degli operatori in C#

C# SELF
Livello 34 , Lezione 2
Disponibile

1. Introduzione

Sicuramente sei abituato a usare operatori come + per sommare numeri o == per confrontare valori. Questi operatori funzionano alla grande con i tipi di dato built-in. Ma che succede se crei una tua classe, tipo Vector (vettore), e vuoi che la somma di due vettori sia naturale, tipo vector1 + vector2? O che il confronto tra due oggetti Money (soldi) funzioni come moneyAmount1 == moneyAmount2? Proprio per questi casi in C# esiste l'overloading degli operatori.

L'overloading degli operatori ti permette di definire come gli operatori standard (tipo +, -, *, /, ==, !=, >, <, ++, -- e tanti altri) devono funzionare con le istanze delle tue classi o struct. Così il tuo codice diventa più intuitivo, leggibile ed espressivo, permettendoti di usare la sintassi familiare anche per i tuoi tipi custom.

Basi dell'overloading degli operatori

Per fare l'overload di un operatore, dichiari un metodo statico nella tua classe o struct, usando la parola chiave operator seguita dal simbolo dell'operatore che vuoi sovrascrivere.

  • Metodo statico: Il metodo per l'overload deve essere sempre public static.
  • Nome del metodo: Il nome è la parola chiave operator seguita dal simbolo dell'operatore (tipo operator +, operator ==).
  • Parametri: Il numero di parametri dipende dal tipo di operatore (unario o binario).
    • Operatori unari (tipo +, -, !, ++, --): accettano un solo parametro del tipo della classe/struct dove sono definiti.
    • Operatori binari (tipo +, -, *, /, ==, !=): accettano due parametri, almeno uno dei quali deve essere del tipo della classe/struct dove sono definiti.
  • Tipo di ritorno: Il tipo che restituisce l'operazione.

2. Overloading degli operatori binari (Binary Operators)

Gli operatori binari prendono due operandi. I più comuni sono quelli aritmetici (+, -, *, /) e quelli di confronto (==, !=, >, <, >=, <=).

Esempio: Overloading dell'operatore somma (+)

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

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

    // Overloading dell'operatore +
    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})";
}

// Uso:
Point point1 = new Point(1, 2);
Point point2 = new Point(3, 4);
Point sumPoint = point1 + point2; // Qui viene chiamato l'operatore overloadato +

Console.WriteLine($"Somma dei punti: {sumPoint}"); // Output: Somma dei punti: (4, 6)

Qui abbiamo definito che la somma di due oggetti Point deve restituire un nuovo oggetto Point le cui coordinate sono la somma delle coordinate dei punti originali.

Esempio: Overloading dell'operatore moltiplicazione (*) per uno scalare (numero)

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

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

    // Overloading dell'operatore * (vettore per numero)
    public static Vector operator *(Vector vec, double scalar)
    {
        return new Vector(vec.X * scalar, vec.Y * scalar);
    }

    // Overloading dell'operatore * (numero per vettore) - per simmetria
    public static Vector operator *(double scalar, Vector vec)
    {
        return new Vector(vec.X * scalar, vec.Y * scalar);
    }

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

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

Console.WriteLine($"Vettore scalato 1: {scaledVec1}"); // Output: Vettore scalato 1: <10, 15>
Console.WriteLine($"Vettore scalato 2: {scaledVec2}"); // Output: Vettore scalato 2: <10, 15>

Nota che per l'operatore di moltiplicazione lo abbiamo overloadato due volte, così funziona in modo simmetrico: Vector * double e double * Vector.

Esempio: Overloading degli operatori di confronto (== e !=)

Quando fai l'overload degli operatori == e != devi rispettare un contratto importante: se ne overloadi uno, devi overloadare anche l'altro. Inoltre, è super consigliato che il comportamento di questi operatori sia coerente con i metodi Equals() e GetHashCode().

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

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

    // Override obbligatorio di Equals e GetHashCode se overloadi == / !=
    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);

    // Overloading dell'operatore ==
    public static bool operator ==(Money? m1, Money? m2)
    {
        // Controllo null per tipi reference
        if (ReferenceEquals(m1, null)) return ReferenceEquals(m2, null);
        return m1.Equals(m2); // Usiamo il nostro Equals overrideato
    }

    // Overloading dell'operatore != (obbligatorio se overloadi ==)
    public static bool operator !=(Money? m1, Money? m2)
    {
        return !(m1 == m2);
    }
}

// Uso:
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

Per i tipi valore (struct) gli operatori == e != di default fanno un confronto bit a bit, che di solito va bene. Invece, per le classi (class) di default == confronta i riferimenti, quindi l'overloading è molto utile per confrontare i valori.

3. Overloading degli operatori unari (Unary Operators)

Gli operatori unari lavorano su un solo operando. Esempi: + (più unario), - (meno unario), ! (NOT logico), ~ (NOT bit a bit), ++ (incremento), -- (decremento).

Esempio: Overloading del meno unario (-)

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

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

    // Overloading del meno unario
    public static Vector3D operator -(Vector3D vec)
    {
        return new Vector3D(-vec.X, -vec.Y, -vec.Z);
    }

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

// Uso:
Vector3D originalVec = new Vector3D(1, -2, 3);
Vector3D invertedVec = -originalVec; // Chiama l'operatore unario overloadato -

Console.WriteLine($"Vettore originale: {originalVec}"); // Output: Vettore originale: <1, -2, 3>
Console.WriteLine($"Vettore invertito: {invertedVec}"); // Output: Vettore invertito: <-1, 2, -3>

Esempio: Overloading degli operatori incremento (++) e decremento (--)

Questi operatori modificano l'operando e restituiscono il valore modificato.

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

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

    // Overloading dell'operatore incremento ++
    public static Counter operator ++(Counter c)
    {
        // Importante: restituiamo un NUOVO oggetto se la struct è immutabile,
        // oppure modifichiamo l'attuale e lo restituiamo se è una classe
        return new Counter(c.Value + 1);
    }

    // Overloading dell'operatore decremento --
    public static Counter operator --(Counter c)
    {
        return new Counter(c.Value - 1);
    }

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

// Uso:
Counter myCounter = new Counter(5);
myCounter++; // Incremento postfisso
Console.WriteLine(myCounter); // Output: [Contatore: 6]

++myCounter; // Incremento prefisso
Console.WriteLine(myCounter); // Output: [Contatore: 7]

myCounter--;
Console.WriteLine(myCounter); // Output: [Contatore: 6]

Gli operatori overloadati ++ e -- sono sempre prefissi (prima modificano, poi restituiscono). Il compilatore genera automaticamente il comportamento giusto per la forma postfissa.

4. Overloading degli operatori di conversione di tipo

Puoi definire come le istanze del tuo tipo possono essere convertite esplicitamente o implicitamente in un altro tipo, e viceversa.

  • implicit (conversione implicita): Si usa quando la conversione è sempre sicura e non causa perdita di dati (tipo int in long).
  • explicit (conversione esplicita): Si usa quando la conversione può causare perdita di dati o errori, e richiede la specifica esplicita ((Type)obj).

Esempio: Conversione implicita da Score a int

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

    // Conversione implicita da Score a int
    public static implicit operator int(Score s)
    {
        return s.Points;
    }
}

// Uso:
Score examScore = new Score(95);
int scoreValue = examScore; // Conversione implicita
Console.WriteLine($"Punti: {scoreValue}"); // Output: Punti: 95

Esempio: Conversione esplicita da Celsius a 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;

    // Conversione esplicita da Fahrenheit a Celsius
    public static explicit operator Celsius(Fahrenheit f)
    {
        return new Celsius((f.Degrees - 32) * 5 / 9);
    }

    // Conversione esplicita da Celsius a Fahrenheit
    public static explicit operator Fahrenheit(Celsius c)
    {
        return new Fahrenheit(c.Degrees * 9 / 5 + 32);
    }
}

// Uso:
Celsius c = new Celsius(25);
Fahrenheit f = (Fahrenheit)c; // Conversione esplicita
Console.WriteLine($"25°C = {f.Degrees}°F"); // Output: 25°C = 77°F

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

5. Limiti e consigli

Non tutti gli operatori possono essere overloadati: Non puoi fare l'overload di operatori come &&, ||, ?., new, typeof, is, as, == (per string, perché è già overloadato nella classe string e ha un comportamento speciale), () (chiamata di metodo), = (assegnazione) e altri.

Simmetria: Se fai l'overload di un operatore binario, spesso conviene overloadarlo anche per l'ordine inverso degli operandi, se ha senso (come nell'esempio con Vector * double).

Contratti: Rispetta sempre i contratti, soprattutto per gli operatori di confronto (==, !=, >, <, >=, <=) e la loro relazione con Equals() e GetHashCode(). Se non rispetti i contratti puoi avere comportamenti strani e bug, soprattutto nelle collection.

Leggibilità e intuizione: Fai l'overload degli operatori solo se rende il codice più intuitivo e leggibile. Se il comportamento dell'operatore non è ovvio o può confondere, meglio usare metodi normali con nomi chiari (tipo Add() invece di +).

Oggetti mutabili: Occhio quando fai l'overload per tipi mutabili. Per esempio, Vector v1 = v2 + v3; di solito crea un nuovo oggetto, non modifica v2. Se il tuo operatore modifica l'oggetto esistente, può essere poco chiaro.

Struct vs Classi: L'overloading degli operatori si usa più spesso con le struct, che di solito rappresentano valori (tipo Point, ComplexNumber), e per cui operazioni matematiche o confronti di valore sono naturali.

2
Compito
C# SELF, livello 34, lezione 2
Bloccato
Overload dell'operatore binario di addizione
Overload dell'operatore binario di addizione
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION