JavaRush /Курси /C# SELF /Перевантаження операторів у C#

Перевантаження операторів у C#

C# SELF
Рівень 34 , Лекція 2
Відкрита

1. Вступ

Ймовірно, ви звикли використовувати оператори, такі як + для додавання чисел або == для порівняння. Ці оператори чудово працюють із вбудованими типами даних. Але що, якщо ви створюєте власний клас, скажімо, Vector (вектор), і хочете, щоб додавання двох векторів виглядало природно, наприклад vector1 + vector2? Або щоб порівняння двох об’єктів Money (гроші) працювало як moneyAmount1 == moneyAmount2? Саме для таких сценаріїв у C# існує перевантаження операторів.

Перевантаження операторів дає змогу вам визначити, як стандартні оператори (такі як +, -, *, /, ==, !=, >, <, ++, -- і багато інших) мають працювати з екземплярами ваших класів або структур. Це робить ваш код інтуїтивнішим, читабельнішим і виразнішим, дозволяючи використовувати звичний синтаксис для ваших власних типів.

Основи перевантаження операторів

Щоб перевантажити оператор, ви оголошуєте статичний метод у своєму класі або структурі, використовуючи ключове слово operator, за яким іде символ оператора, який ви хочете перевантажити.

  • Статичний метод: Метод перевантаження оператора завжди має бути public static.
  • Імʼя методу: Імʼя методу — це ключове слово operator, за яким іде сам символ оператора (наприклад, operator +, operator ==).
  • Параметри: Кількість параметрів залежить від типу оператора (унарний чи бінарний).
    • Унарні оператори (наприклад, +, -, !, ++, --): приймають один параметр типу класу/структури, у якому вони визначені.
    • Бінарні оператори (наприклад, +, -, *, /, ==, !=): приймають два параметри, принаймні один з яких має бути типу класу/структури, у якому вони визначені.
  • Тип, що повертається: Тип, який повертає оператор.

2. Перевантаження бінарних операторів (Binary Operators)

Бінарні оператори приймають два операнди. Найпоширеніші — це арифметичні оператори (+, -, *, /) і оператори порівняння (==, !=, >, <, >=, <=).

Приклад: перевантаження оператора додавання (+)

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

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

    // Перевантаження оператора +
    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})";
}

// Використання:
Point point1 = new Point(1, 2);
Point point2 = new Point(3, 4);
Point sumPoint = point1 + point2; // Тут викликається перевантажений оператор +

Console.WriteLine($"Сума точок: {sumPoint}"); // Виведення: Сума точок: (4, 6)

Тут ми визначили, що додавання двох об’єктів Point має повертати новий об’єкт Point, координати якого є сумами відповідних координат початкових точок.

Приклад: перевантаження оператора множення (*) на скаляр (число)

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

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

    // Перевантаження оператора * (вектор на число)
    public static Vector operator *(Vector vec, double scalar)
    {
        return new Vector(vec.X * scalar, vec.Y * scalar);
    }

    // Перевантаження оператора * (число на вектор) - для симетрії
    public static Vector operator *(double scalar, Vector vec)
    {
        return new Vector(vec.X * scalar, vec.Y * scalar);
    }

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

// Використання:
Vector vec1 = new Vector(2, 3);
Vector scaledVec1 = vec1 * 5;      // Викликається Vector * double
Vector scaledVec2 = 5 * vec1;      // Викликається double * Vector

Console.WriteLine($"Масштабований вектор 1: {scaledVec1}"); // Виведення: Масштабований вектор 1: <10, 15>
Console.WriteLine($"Масштабований вектор 2: {scaledVec2}"); // Виведення: Масштабований вектор 2: <10, 15>

Зверніть увагу: для оператора множення ми перевантажили його двічі, щоб він працював симетрично: Vector * double і double * Vector.

Приклад: перевантаження операторів порівняння (== і !=)

Під час перевантаження операторів == і != слід дотримуватися важливого контракту: якщо ви перевантажуєте один із них, обов’язково перевантажте й другий. Також бажано, щоб поведінка цих операторів відповідала поведінці методів Equals() і GetHashCode().

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

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

    // Обов’язково перевизначаємо Equals і GetHashCode, якщо перевантажуємо == / !=
    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);

    // Перевантаження оператора ==
    public static bool operator ==(Money? m1, Money? m2)
    {
        // Перевірка на null для посилальних типів
        if (ReferenceEquals(m1, null)) return ReferenceEquals(m2, null);
        return m1.Equals(m2); // Використовуємо наш перевизначений Equals
    }

    // Перевантаження оператора != (обов’язково при перевантаженні ==)
    public static bool operator !=(Money? m1, Money? m2)
    {
        return !(m1 == m2);
    }
}

// Використання:
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

Для типів-значень (структур) оператори == і != за замовчуванням виконують побітове порівняння, що зазвичай прийнятно. А для класів (class) за замовчуванням == порівнює посилання, тому його перевантаження дуже корисне для порівняння за значенням.

3. Перевантаження унарних операторів (Unary Operators)

Унарні оператори працюють з одним операндом. Приклади: + (унарний плюс), - (унарний мінус), ! (логічне НЕ), ~ (побітове НЕ), ++ (інкремент), -- (декремент).

Приклад: перевантаження унарного мінуса (-)

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

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

    // Перевантаження унарного оператора -
    public static Vector3D operator -(Vector3D vec)
    {
        return new Vector3D(-vec.X, -vec.Y, -vec.Z);
    }

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

// Використання:
Vector3D originalVec = new Vector3D(1, -2, 3);
Vector3D invertedVec = -originalVec; // Викликається перевантажений унарний оператор -

Console.WriteLine($"Початковий вектор: {originalVec}"); // Виведення: Початковий вектор: <1, -2, 3>
Console.WriteLine($"Інвертований вектор: {invertedVec}"); // Виведення: Інвертований вектор: <-1, 2, -3>

Приклад: перевантаження операторів інкремента (++) і декремента (--)

Ці оператори змінюють операнд і повертають змінене значення.

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

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

    // Перевантаження оператора інкремента ++
    public static Counter operator ++(Counter c)
    {
        // Важливо: повертаємо НОВИЙ об’єкт, якщо структура незмінна,
        // або змінюємо поточний і повертаємо його, якщо це клас
        return new Counter(c.Value + 1);
    }

    // Перевантаження оператора декремента --
    public static Counter operator --(Counter c)
    {
        return new Counter(c.Value - 1);
    }

    public override string ToString() => $"[Лічильник: {Value}]";
}

// Використання:
Counter myCounter = new Counter(5);
myCounter++; // Постфіксний інкремент
Console.WriteLine(myCounter); // Виведення: [Лічильник: 6]

++myCounter; // Префіксний інкремент
Console.WriteLine(myCounter); // Виведення: [Лічильник: 7]

myCounter--;
Console.WriteLine(myCounter); // Виведення: [Лічильник: 6]

Перевантажені оператори ++ і -- мають єдине визначення, що відповідає префіксній формі. Компілятор автоматично забезпечує коректну поведінку для постфіксної форми.

4. Перевантаження операторів перетворення типів

Ви можете визначити, як екземпляри вашого типу можуть бути явно або неявно перетворені в інший тип, і навпаки.

  • implicit (неявне перетворення): використовується, коли перетворення завжди безпечне і не призводить до втрати даних (наприклад, int у long).
  • explicit (явне перетворення): використовується, коли перетворення може спричинити втрату даних або помилку, і потребує явного зазначення ((Type)obj).

Приклад: неявне перетворення Score у int

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

    // Неявне перетворення Score у int
    public static implicit operator int(Score s)
    {
        return s.Points;
    }
}

// Використання:
Score examScore = new Score(95);
int scoreValue = examScore; // Неявне перетворення
Console.WriteLine($"Бали: {scoreValue}"); // Виведення: Бали: 95

Приклад: явне перетворення Celsius у 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;

    // Явне перетворення Fahrenheit у Celsius
    public static explicit operator Celsius(Fahrenheit f)
    {
        return new Celsius((f.Degrees - 32) * 5 / 9);
    }

    // Явне перетворення Celsius у Fahrenheit
    public static explicit operator Fahrenheit(Celsius c)
    {
        return new Fahrenheit(c.Degrees * 9 / 5 + 32);
    }
}

// Використання:
Celsius c = new Celsius(25);
Fahrenheit f = (Fahrenheit)c; // Явне перетворення
Console.WriteLine($"25°C = {f.Degrees}°F"); // Виведення: 25°C = 77°F

Fahrenheit f2 = new Fahrenheit(212);
Celsius c2 = (Celsius)f2; // Явне перетворення
Console.WriteLine($"212°F = {c2.Degrees}°C"); // Виведення: 212°F = 100°C

5. Обмеження та рекомендації

Не всі оператори можна перевантажити: Ви не можете перевантажувати такі оператори, як &&, ||, ?., new, typeof, is, as, == (для string, бо він уже перевантажений у класі string і має особливу поведінку), () (виклик методу), = (присвоєння) та ще деякі інші.

Симетрія: Якщо ви перевантажуєте бінарний оператор, часто корисно перевантажити його і для зворотного порядку операндів, якщо це має сенс (як у прикладі з Vector * double).

Контракти: Завжди дотримуйтеся контрактів, особливо для операторів порівняння (==, !=, >, <, >=, <=) і їхнього зв’язку з Equals() і GetHashCode(). Порушення контрактів може призвести до неочікуваної та некоректної поведінки, особливо в колекціях.

Читабельність та інтуїтивність: Перевантажуйте оператори лише тоді, коли це робить код інтуїтивнішим і читабельнішим. Якщо поведінка оператора неочевидна або може заплутати, краще використовувати звичайні методи з виразними іменами (наприклад, Add() замість +).

Змінювані (mutable) об’єкти: Будьте обережні під час перевантаження операторів для змінюваних типів. Наприклад, Vector v1 = v2 + v3; за замовчуванням створює новий об’єкт, а не змінює v2. Якщо ваш оператор змінює наявний об’єкт, це може бути неочевидно.

Структури та класи: Перевантаження операторів частіше застосовують для структур, які за своєю суттю часто є значеннями (наприклад, Point, ComplexNumber), і для яких математичні операції або порівняння значень є природними.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ