Классы и объекты C#: перегрузка операторов и операций преобразования типов

О перегрузке методов мы уже знаем. Вместе с этим, в 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# сообщит нам об ошибке:

Ошибка CS0216 Для оператора «Point.operator >(Point, Point)» требуется, чтобы был определен соответствующий оператор «<«.

Поэтому, переопределяем и парный оператор <.

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 равны нулю");

Консольный вывод будет следующим:

Координаты точки point1 больше нуля

Координаты точки point2 равны нулю

Что стоит учитывать при перегрузке операторов в C#

При переопределении операторов в C# следует учитывать следующее:

  1. так как определение оператора представляет собой метод, то этот метод мы также можем перегрузить, то есть создать для него еще одну версию.  О том, как перегружать методы в C# мы говорили здесь.
  2. при перегрузке не должны изменяться те объекты, которые передаются в оператор через параметры.

Второй пункт наиболее наглядно демонстрирует перегрузка унарных операторов, например, ++. Определим для класса 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#. Перегрузка операторов позволяет определять собственную логику при использовании операторов, например, арифметических или логических при использовании собственных классов. С помощью перегрузки операций преобразования типов мы можем определить логику преобразования одного типа данных (например, класса) в другой.

Подписаться
Уведомить о
guest
0 Комментарий
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии