Рекомендации по перегрузке операторов
Сохраняйте интуитивность перегруженных операторов.
«Рекомендации по перегрузке операторов» — бесплатный урок C# Academy на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения C# Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс C# Academy содержит 4 уроков всего.
Рекомендации по перегрузке операторов
Перегрузка операторов — мощный инструмент, требующий осторожности. При правильном использовании она делает типы-значения интуитивными, а при неправильном — превращает код в загадку. В этом уроке рассматриваются принципы, благодаря которым перегруженные операторы остаются предсказуемыми.
Перегружайте только при очевидном смысле
Перегружайте оператор только тогда, когда его смысл для вашего типа однозначен. + для вектора явно означает покомпонентное сложение, а + для Order лишь запутает читателей.
using System;
public struct Vector2
{
public int X, Y;
public Vector2(int x, int y) { X = x; Y = y; }
// + means adding components: obvious and expected
public static Vector2 operator +(Vector2 a, Vector2 b)
=> new Vector2(a.X + b.X, a.Y + b.Y);
public override string ToString() => "(" + X + ", " + Y + ")";
}
public class Program
{
public static void Main()
{
Console.WriteLine(new Vector2(1, 2) + new Vector2(3, 4));
}
}Сохраняйте математическую интуицию
Операторы должны соответствовать интуиции: a + b должно давать тот же результат, что и b + a, когда сложение коммутативно, а + не должен незаметно выполнять вычитание. Такие сюрпризы приводят к трудноуловимым ошибкам.
using System;
public struct Money
{
public decimal Amount;
public Money(decimal a) { Amount = a; }
public static Money operator +(Money a, Money b) => new Money(a.Amount + b.Amount);
public override string ToString() => "$" + Amount;
}
public class Program
{
public static void Main()
{
var x = new Money(3);
var y = new Money(5);
Console.WriteLine((x + y).Amount == (y + x).Amount); // commutative
}
}Сохраняйте симметрию и парность
Язык объединяет некоторые операторы в пары, но вам также следует сохранять логическую симметрию: если вы добавляете +, рассмотрите и -; если добавляете <, также предоставьте > и варианты с равенством. Незавершённые наборы операторов раздражают пользователей.
using System;
public struct Length
{
public int Mm;
public Length(int mm) { Mm = mm; }
public static Length operator +(Length a, Length b) => new Length(a.Mm + b.Mm);
public static Length operator -(Length a, Length b) => new Length(a.Mm - b.Mm);
public override string ToString() => Mm + "mm";
}
public class Program
{
public static void Main()
{
Console.WriteLine(new Length(50) + new Length(20));
Console.WriteLine(new Length(50) - new Length(20));
}
}Не изменяйте операнды
Операторы должны быть чистыми: вычисляйте и возвращайте новое значение, не изменяя операнды. Изменение операнда внутри + нарушает все ожидания читателя.
using System;
public struct Vector2
{
public int X, Y;
public Vector2(int x, int y) { X = x; Y = y; }
public static Vector2 operator +(Vector2 a, Vector2 b)
=> new Vector2(a.X + b.X, a.Y + b.Y); // returns new, mutates nothing
public override string ToString() => "(" + X + ", " + Y + ")";
}
public class Program
{
public static void Main()
{
var a = new Vector2(1, 1);
var unused = a + new Vector2(9, 9);
Console.WriteLine("a is still " + a);
}
}Согласуйте == с Equals
Важная рекомендация: при перегрузке == переопределяйте Equals и GetHashCode, чтобы все способы проверки равенства давали согласованный результат. Несогласованность здесь — одна из самых распространённых ошибок при работе с операторами.
using System;
public struct Point
{
public int X, Y;
public Point(int x, int y) { X = x; Y = y; }
public override bool Equals(object obj) => obj is Point p && p.X == X && p.Y == Y;
public override int GetHashCode() => HashCode.Combine(X, Y);
public static bool operator ==(Point a, Point b) => a.Equals(b);
public static bool operator !=(Point a, Point b) => !a.Equals(b);
}
public class Program
{
public static void Main()
{
Console.WriteLine(new Point(1, 1) == new Point(1, 1));
}
}Предоставляйте именованные альтернативы
Некоторые языки и инструменты анализа не умеют использовать операторы. Именованный метод, например Add наряду с +, улучшает совместимость и упрощает обнаружение нужной возможности.
using System;
public struct Vector2
{
public int X, Y;
public Vector2(int x, int y) { X = x; Y = y; }
public Vector2 Add(Vector2 other) => new Vector2(X + other.X, Y + other.Y);
public static Vector2 operator +(Vector2 a, Vector2 b) => a.Add(b);
public override string ToString() => "(" + X + ", " + Y + ")";
}
public class Program
{
public static void Main()
{
var v = new Vector2(1, 2);
Console.WriteLine(v.Add(new Vector2(3, 4)));
Console.WriteLine(v + new Vector2(3, 4));
}
}Предпочитайте операторы типам, подобным значениям
Операторы естественно выглядят у неизменяемых типов, подобных значениям (структур или классов, подобных записям). Для изменяемых типов, где важна идентичность, именованные методы обычно лучше передают намерение.
using System;
public readonly struct Rational
{
public readonly int Num, Den;
public Rational(int n, int d) { Num = n; Den = d; }
public static Rational operator *(Rational a, Rational b)
=> new Rational(a.Num * b.Num, a.Den * b.Den);
public override string ToString() => Num + "/" + Den;
}
public class Program
{
public static void Main()
{
Console.WriteLine(new Rational(1, 2) * new Rational(2, 3));
}
}Документируйте особые случаи
Явно указывайте, как операторы обрабатывают особые значения: деление на ноль, операнды со значением null у классов или переполнение. Предсказуемая обработка не позволяет застать вызывающий код врасплох.
using System;
public struct SafeDiv
{
public int Value;
public SafeDiv(int v) { Value = v; }
// Documented behavior: dividing by zero yields zero
public static SafeDiv operator /(SafeDiv a, SafeDiv b)
=> new SafeDiv(b.Value == 0 ? 0 : a.Value / b.Value);
public override string ToString() => Value.ToString();
}
public class Program
{
public static void Main()
{
Console.WriteLine(new SafeDiv(10) / new SafeDiv(0));
Console.WriteLine(new SafeDiv(10) / new SafeDiv(2));
}
}Продуманный набор операторов
Отточенный числовой тип сочетает несколько интуитивных операторов, согласованное равенство и неизменяемость. В результате он ведёт себя как встроенное число и приносит удовольствие в использовании.
using System;
public readonly struct Vec
{
public readonly double X, Y;
public Vec(double x, double y) { X = x; Y = y; }
public static Vec operator +(Vec a, Vec b) => new Vec(a.X + b.X, a.Y + b.Y);
public static Vec operator -(Vec a, Vec b) => new Vec(a.X - b.X, a.Y - b.Y);
public static Vec operator *(Vec a, double k) => new Vec(a.X * k, a.Y * k);
public override string ToString() => "(" + X + ", " + Y + ")";
}
public class Program
{
public static void Main()
{
Console.WriteLine((new Vec(1, 2) + new Vec(3, 4)) * 2);
}
}Попробуйте сами
Объедините рекомендации на практике: создайте неизменяемый тип-значение, интуитивные операторы, именованную альтернативу и согласованное равенство.
using System;
public readonly struct Meters : IEquatable<Meters>
{
public readonly double Value;
public Meters(double v) { Value = v; }
public Meters Add(Meters other) => new Meters(Value + other.Value);
public static Meters operator +(Meters a, Meters b) => a.Add(b);
public bool Equals(Meters other) => Value == other.Value;
public override bool Equals(object obj) => obj is Meters m && Equals(m);
public override int GetHashCode() => Value.GetHashCode();
public static bool operator ==(Meters a, Meters b) => a.Equals(b);
public static bool operator !=(Meters a, Meters b) => !a.Equals(b);
public override string ToString() => Value + "m";
}
public class Program
{
public static void Main()
{
Console.WriteLine(new Meters(3) + new Meters(4));
Console.WriteLine(new Meters(7) == new Meters(7));
}
}Быстрая проверка
Примените рекомендации по перегрузке операторов.
Итоги
Хорошая перегрузка операторов отличается продуманностью и предсказуемостью.
- Перегружайте операторы только при очевидном смысле.
- Сохраняйте математическую интуицию и симметрию.
- Оставайтесь чистыми: возвращайте новые значения и никогда не изменяйте операнды.
- Согласуйте
==сEquals/GetHashCode. - Предлагайте именованные альтернативы и предпочитайте типы, подобные значениям.
Часто задаваемые вопросы
Урок «Рекомендации по перегрузке операторов» бесплатный?
Да — полный текст урока «Рекомендации по перегрузке операторов» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс C# Academy, подпишись на CoddyKit PRO. Курс C# Academy содержит 4 уроков всего.
Чему я научусь в уроке «Рекомендации по перегрузке операторов»?
Сохраняйте интуитивность перегруженных операторов. Ты практикуешь C# Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать C# Academy?
Предыдущий опыт не требуется. C# Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 4 из 4.
Сколько времени занимает урок «Рекомендации по перегрузке операторов»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке C# Academy?
Да. Каждый урок C# Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Перегрузка арифметических операторов
- Перегрузка операторов сравнения
- Пользовательские преобразования
- Рекомендации по перегрузке операторов