Содержание
О перегрузке методов мы уже знаем. Вместе с этим, в C# можно перегружать не только методы и свойства, но и операторы, например, операторы сложения и вычитания, сравнения и так далее. Более того, мы можем сделать перегрузку операций преобразования типов. И сегодня мы рассмотрим некоторые практические примеры, когда нам может пригодиться такая возможность языка программирования C# как перегрузка операторов и операций преобразования типов.
Перегрузка операторов
Перегрузка арифметических операторов в C#
Перегрузка операторов заключается в определении в классе специального метода:
public static возвращаемый_тип operator оператор(параметры) { }
Например, рассмотрим такой класс:
public class Point { public double X { get; set; } public double Y { get; set; } public Point(double x, double y) { X = x; Y = y; } }
Этот класс определяет точку на плоскости с координатами (X, Y). Нам необходимо обеспечить векторное сложение и векторное вычитание. Компилятор C# умеет складывать, вычитать, сравнивать примитивные типы данных, однако про то, как сложить наши собственные классы и объекты он не знает. Технически мы могли бы каждый раз писать что-то наподобие такого:
Point point1 = new Point(10,10); Point point2 = new Point(7,7); Point point3 = new Point(point1.X+point2.X,point1.Y+point2.Y);
И это вполне себе работоспособный код. Однако можно сделать лучше и красивее — переопределить оператор сложения:
public class Point { public double X { get; set; } public double Y { get; set; } public Point(double x, double y) { X = x; Y = y; } //переопределенный оператор сложения public static Point operator +(Point p1, Point p2) { return new Point(p1.X + p2.X, p1.Y + p2.Y); } }
Так как перегружаемый оператор будет использоваться для всех объектов данного класса, то он имеет модификаторы доступа public static
. При сложении возвращается объект класса Point
. Теперь мы можем сделать наш код более элегантным и понятным:
Point point1 = new Point(10, 10); Point point2 = new Point(7, 7); Point point3 = point1 + point2;//используем перегруженный оператор Console.WriteLine($"X = {point3.X} Y = {point3.Y}"); //X = 17 Y = 17
Аналогичным образом можно переопределять и другие арифметические операторы, в том числе операторы сложения, вычитания, умножения, деления и так далее. Например, вот так может выглядеть оператор * для выполнения операция скалярного умножения точки на плоскости:
public static Point operator *(double s, Point point) { return new Point(s * point.X, s * point.Y); }
И теперь мы можем умножать точку на любое число (выполнять скалярное умножение):
Point point2 = new Point(7, 7); Point point3 = 2.5*point2; Console.WriteLine($"X = {point3.X} Y = {point3.Y}"); //X = 17,5 Y = 17,5
Также следует упомянуть, что операторы в C# бывают унарные и бинарные, но в любом случае один из параметров должен представлять тот тип — класс или структуру, в котором определяется оператор.
Перегрузка логических операторов в C#
Немного иначе обстоит дело с перегрузкой логических операторов в C#. Отличие заключается в том, что операторы сравнения должны переопределяться попарно. Парными являются следующие операторы:
- Операторы
==
и!=
- Операторы
<
и>
- Операторы
<=
и>=
Например, переопределим оператор >. Переопределенный оператор в классе Point
может быть таким:
public static bool operator >(Point point1, Point point2) { return (point1.X > point2.X) || ((point1.X == point2.X) && (point1.Y > point2.Y)); }
При этом, как только мы переопределим один из парных операторов, компилятор C# сообщит нам об ошибке:
Поэтому, переопределяем и парный оператор <.
public static bool operator <(Point point1, Point point2) { return (point1.X < point2.X) || ((point1.X == point2.X) && (point1.Y < point2.Y)); }
Мы можем также переопределить операторы true
и false
. Например, определим их в классе Point
:
public class Point { public double X { get; set; } public double Y { get; set; } public Point(double x, double y) { X = x; Y = y; } public static bool operator true(Point p1) { return (p1.X != 0) && (p1.Y != 0); } public static bool operator false(Point p1) { return (p1.X == 0) && (p1.Y == 0); } }
Использовать эти операторы можно следующим образом:
Point point1 = new Point(10, 10); if (point1) //--используем операторы true/false у Point Console.WriteLine("Координаты точки point1 больше нуля"); else Console.WriteLine("Координаты точки point1 равны нулю"); Point point2 = new Point(0, 0); if (point2) //--используем операторы true/false у Point Console.WriteLine("Координаты точки point2 больше нуля"); else Console.WriteLine("Координаты точки point2 равны нулю");
Консольный вывод будет следующим:
Координаты точки point2 равны нулю
Что стоит учитывать при перегрузке операторов в C#
При переопределении операторов в C# следует учитывать следующее:
- так как определение оператора представляет собой метод, то этот метод мы также можем перегрузить, то есть создать для него еще одну версию. О том, как перегружать методы в C# мы говорили здесь.
- при перегрузке не должны изменяться те объекты, которые передаются в оператор через параметры.
Второй пункт наиболее наглядно демонстрирует перегрузка унарных операторов, например, ++. Определим для класса Point
оператор инкремента:
public static Point operator ++(Point p1) { p1.X += 1; p1.Y += 1; return p1; }
Так как оператор ++ унарный, то он принимает один параметр — объект того класса, в котором данный оператор определен. Несмотря на то, что компилятор C# не предупредит нас об ошибке, это неправильное определение инкремента, так как оператор не должен менять значения своих параметров.
Более корректная перегрузка оператора инкремента будет выглядеть так:
public static Point operator ++(Point p1) { return new Point(p1.X + 1, p1.Y + 1); }
Использование унарного оператора будет таким же, как и при его использовании для простых типов данных. При этом нам не надо определять отдельно операторы для префиксного и для постфиксного инкремента (а также декремента), так как одна реализация будет работать в обоих случаях.
Point point1 = new Point(10, 10); point1++; ++point1; Console.WriteLine($"X = {point1.X} Y = {point1.Y}"); //X = 12 Y = 12
Полный список перегружаемых операторов можно найти в документации msdn
При перегрузке операторов также следует помнить, что мы не можем изменить приоритет оператора или его ассоциативность, мы не можем создать новый оператор или изменить логику операторов в типах, который есть по умолчанию в .NET.
Операции преобразования типов
С этой темой перегрузки операторов в C# тесно связана тема перегрузки операторов преобразования типов. В прошлой статье мы рассматривали восходящее и нисходящее преобразование типов. Было бы не плохо иметь возможность определять логику преобразования одних типов в другие. С помощью перегрузки операторов мы можем это делать. Для этого в классе необходимо определить метод, который имеет следующую форму:
public static implicit|explicit operator Тип_в_который_надо_преобразовать(исходный_тип param) { // логика преобразования }
public static
идет ключевое слово explicit
(если преобразование явное, то есть нужна операция приведения типов) или implicit
(если преобразование неявное). Затем идет ключевое слово operator
и далее возвращаемый тип, в который надо преобразовать объект. В скобках в качестве параметра передается объект, который надо преобразовать.Например, пусть у нас есть следующий класс Counter
, который представляет секундомер и который хранит количество секунд в свойстве Seconds
:
class Counter { public int Seconds { get; set; } public static implicit operator Counter(int x) { return new Counter { Seconds = x }; } public static explicit operator int(Counter counter) { return counter.Seconds; } }
Первый оператор преобразует число — объект типа int
к типу Counter
. Его логика проста — создается новый объект Counter
, у которого устанавливается свойство Seconds
. Второй оператор преобразует объект Counter
к типу int
, то есть получает из Counter
число.
Применение операторов преобразования типов в программе может быть следующим:
Counter counter1 = new Counter { Seconds = 23 }; int x = (int)counter1; Console.WriteLine(x); // 23 Counter counter2 = x; Console.WriteLine(counter2.Seconds); // 23
Counter
в int
определена с ключевым словом explicit
, то есть как явное преобразование, то в этом случае необходимо применить операцию приведения типов:int x = (int)counter1;
В случае с операцией преобразования от int
к Counter
операция определена с ключевым словом implicit
, то есть как неявная, поэтому в коде выше мы ничего не указывали перед переменной x
. Какие операции преобразования делать явными, а какие неявные — решает разработчик по своему усмотрению.
Отметим, что оператор преобразования типов должен преобразовывать из типа или в тип, в котором этот оператор определен. То есть оператор преобразования, определенный в типе Counter
, должен либо принимать в качестве параметра объект типа Counter
, либо возвращать объект типа Counter
. Рассмотрим также более сложные преобразования, к примеру, из одного составного типа в другой составной тип. Допустим, у нас есть еще класс Timer
:
class Timer { public int Hours { get; set; } public int Minutes { get; set; } public int Seconds { get; set; } } class Counter { public int Seconds { get; set; } public static implicit operator Counter(int x) { return new Counter { Seconds = x }; } public static explicit operator int(Counter counter) { return counter.Seconds; } //преобразования в Timer и из Timer public static explicit operator Counter(Timer timer) { int h = timer.Hours * 3600; int m = timer.Minutes * 60; return new Counter { Seconds = h + m + timer.Seconds }; } public static implicit operator Timer(Counter counter) { int h = counter.Seconds / 3600; int m = (counter.Seconds % 3600) / 60; int s = counter.Seconds % 60; return new Timer { Hours = h, Minutes = m, Seconds = s }; } }
Класс Timer
представляет условный таймер, который хранит часы, минуты и секунды. Класс Counter
представляет секундомер, который хранит только количество секунд. Исходя из этого мы можем определить логику преобразования из одного типа к другому, то есть получение из секунд в объекте Counter
часов, минут и секунд в объекте Timer
.
Применение операций преобразования:
Counter counter1 = new Counter { Seconds = 115 }; Timer timer = counter1; Console.WriteLine($"{timer.Hours}:{timer.Minutes}:{timer.Seconds}"); // 0:1:55 Counter counter2 = (Counter)timer; Console.WriteLine(counter2.Seconds); //115
Итого
Сегодня мы рассмотрели вопросы, связанные с перегрузкой операторов и операций преобразования типов в C#. Перегрузка операторов позволяет определять собственную логику при использовании операторов, например, арифметических или логических при использовании собственных классов. С помощью перегрузки операций преобразования типов мы можем определить логику преобразования одного типа данных (например, класса) в другой.