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. Если ваш оператор изменяет существующий объект, это может быть неочевидно.

Структуры vs Классы: Перегрузка операторов чаще используется для структур, которые по своей природе часто представляют собой значения (например, Point, ComplexNumber), и для которых математические операции или сравнения значений естественны.

2
Задача
C# SELF, 34 уровень, 2 лекция
Недоступна
Перегрузка бинарного оператора сложения
Перегрузка бинарного оператора сложения
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ