Введение в перегрузку операторов в C#

Преобразование, бинарные операторы, унарные операторы, и операторы сравнения для ваших типов.

Введение

Перегрузка операторов – это мощная и недостаточно используемая (но часто неправильно используемая) возможность, способная сделать код более простым, а использование объектов - более интуитивно-понятным. Добавление нескольких простых перегруженных операторов в класс или структуру даст вам возможность:

  1. выполнять преобразования в/ из вашего типа (и) в другие типы
  2. производить математические/логические операции над вашим типом и над ним самим или над другими типами

Операторы преобразования

Существуют несколько способов преобразования между типами, но среди них наиболее распространенный - это использование неявных/явных операторов преобразования.

Неявное преобразование

Неявное преобразование означает, что можно напрямую присваивать экземпляр одного типа другому.

Давайте рассмотрим пример.

public struct MyIntStruct
{
    int m_IntValue;
    private MyIntStruct(Int32 intValue)
    {
        m_IntValue = intValue;
    }
    public static implicit operator MyIntStruct(Int32 intValue)
    {
        return new MyIntStruct(intValue);
    }
}

Так как в данной структуре есть такой оператор, новый экземпляр данной структуры можно создать в нашем коде просто при помощи присвоения ей значения – приведение типов или новые объявления в нашем коде не требуются.

MyIntStruct myIntStructInstance = 5;

Чтобы явно преобразовать структуру обратно в int, необходимо добавить еще один оператор преобразования:

 public static implicit operator Int32(MyIntStruct instance)
{
    return instance.m_IntValue;
}

Теперь можно присваивать myIntStructInstance напрямую int (тип целых чисел).

int i = myIntStructInstance;

Также можно вызвать любую другую функцию, которая будет принимать int, и передать ей экземпляр структуры напрямую. Это можно проделать с любым нужным числом типов.

Структура может иметь и строковое поле, и в этом случае было бы полезно иметь возможность создать ее экземпляр путем непосредственного присвоения строки. Эта структура может выглядеть так:

public struct MyIntStringStruct
{
    int m_IntValue;
    string m_StringValue;
    private MyIntStringStruct(Int32 intValue)
    {
        m_IntValue = intValue;
        m_StringValue = string.Empty; // значение по умолчанию
    }
    private MyIntStringStruct(string stringValue)
    {
        m_IntValue = 0; // значение по умолчанию
        m_StringValue = stringValue;
    }
    public static implicit operator MyIntStringStruct(Int32 intValue)
    {
        return new MyIntStringStruct(intValue);
    }
    public static implicit operator MyIntStringStruct(string stringValue)
    {
        return new MyIntStringStruct(stringValue);
    }
    public static implicit operator Int32(MyIntStringStruct instance)
    {
        return instance.m_IntValue;
    }
}

... и экземпляр структуры может быть создан путем присвоения значения типа int, как и раньше, или путем присвоения string.

MyIntStringStruct myIntStringStructInstance = "Hello World";

Обратите внимание, оператор неявного преобразования из нашей структуры в string (строку) не был создан. В некоторых ситуациях это может создать неопределенность, с которой компилятор не сможет разобраться. Чтобы увидеть это, добавьте следующие строки кода:

 public static implicit operator string(MyIntStringStruct instance)
{
    return instance.m_StringValue;
}

Этот код компилируется без проблем. Теперь испытайте этот код:

 MyIntStringStruct myIntStringStructInstance = "Hello World";
Console.WriteLine(myIntStringStructInstance); // компилятор выдает ошибку здесь

Компилятор выдаст такую ошибку: "Неопределенность вызова между следующими методами или свойствами". Консоль успешно примет int или string (и многие другие типы, конечно), поэтому можно рассчитывать, что консоль будет знать, что именно использовать? Решение – использовать оператор явного преобразования.

Явное преобразование

Такое преобразование означает, что в коде должно выполняться явное приведение типов. Замените ключевое слово implicit на explicit в последнем операторе преобразования, чтобы он выглядел так.

public static explicit operator string(MyIntStringStruct instance)
{
    return instance.m_StringValue;
}

Теперь можно возвращать строковое значение, но только в случае явного преобразования типа в string.

MyIntStringStruct myIntStringStructInstance = "Hello World";
Console.WriteLine((string)myIntStringStructInstance);

Получаем ожидаемый результат.

Сравнение преобразований

implicit значительно удобней, чем explicit, так как не нужно явно преобразовывать типы в коде, но как можно было увидеть ранее, при этом могут возникать проблемы. Нужно учитывать и другие вещи: если объекты, передаваемые в любом направлении, будут так или иначе округлены или усечены, то необходимо использовать явное преобразование, чтобы использующий ваш объект человек не был уличен в том, что данные отбрасываются.

Представьте, что выполняется серия сложных вычислений с десятичными дробями, и разрешается неявное преобразование структуры в десятичную дробь для удобства, но поле, где она сохраняется, целочисленное. Пользователь справедливо стал бы предполагать, что любой объект, неявно передаваемый в десятичную дробь, сохранит точность, но наша структура потеряла бы все десятичные разряды, что может привести к получению неверного результата вычислений!

Если пользователю необходимо явно преобразовывать в/из описываемой структуры, то следует разместить предупреждение. В данной ситуации может быть лучшим вариантом создать неудобство для пользователя и не иметь ни явный, ни неявный, чтобы они были вынуждены приводить к int, чтобы использовать описываемую структуру, и тогда не будет недопонимания.

Есть еще и другие проблемы – выполните тщательный анализ перед использованием implicit. В случае сомнения используйте explicit, или вообще не реализуйте оператор преобразования для этого типа.

Бинарные операторы

Бинарные операторы принимают два аргумента. Могут быть перегружены следующие операторы: +, -, *, /, %, &, |, ^, <<, >>.

Примечание: Важно, чтобы вы не делали ничего неожиданного при использовании операторов, двуместных или других.

Обычно эти операторы довольно логичны. Для начала рассмотрим +. Он обычно вычисляет сумму двух аргументов.

int a = 1;
int b = 2;
int c = a + b; // c = 3
Но класс строк использует оператор + для конкатенции.
  
string x = "Hello";
string y = " World";
string z = x + y; // z = Hello Worl

Поэтому в зависимости от ситуации можно делать все, что логически необходимо.

Представьте структуру, хранящую два целых числа, и у вас есть два ее экземпляра, которые вы хотите сложить. Логично было бы сложить соответствующие целые числа в каждом экземпляре. Вы также можете хотеть прибавить только одно целое число к обоим значениям. Получившаяся структура могла бы выглядеть примерно так.

public struct MySize
{
    int m_Width;
    int m_Height;
    public MySize(int width, int height)
    {
        m_Width = width;
        m_Height = height;
    }
    public static MySize operator +(MySize mySizeA, MySize mySizeB)
    {
        return new MySize(
            mySizeA.m_Width + mySizeB.m_Width,
            mySizeA.m_Height + mySizeB.m_Height);
    }
    public static MySize operator +(MySize mySize, Int32 value)
    {
        return new MySize(
            mySize.m_Width + value,
            mySize.m_Height + value);
    }
}

Эту структуру можно использовать примерно так:

MySize a = new MySize(1, 2);
MySize b = new MySize(3, 4);
MySize c = a + b; // сложение экземпляров MySize
MySize d = c + 5; // Прибавление целого числа

Вы можете выполнять аналогичные действия со всеми перегружаемыми двухместными операциями.

Унарные операторы

Унарные операторы принимают только один аргумент. Могут быть перегружены следующие операторы: +, -, !, ~, ++, --, true, false. Для большинства типов не нужно реализовывать все эти операторы, поэтому реализуйте только необходимые вам!

Простой пример использования ++ для описанной выше структуры:

public static MySize operator ++(MySize mySize)
{
    mySize.m_Width++;
    mySize.m_Height++;
    return mySize;
}

Примечание:  унарные операторы ++ и -- должны (для стандартных операций) изменять целые значения вашей структуры и возвращать тот же самый экземпляр, а не новый экземпляр с новыми значениями.

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

Операторы сравнения принимают два аргумента. Могут быть перегружены следующие операторы. [==, !=], [<, >], [<=, >=]. Они сгруппированы в квадратных скобках, так как их нужно реализовывать попарно.

При использовании == and != также необходимо заменить Equals(object o) и GetHashCode().

public static bool operator ==(MySize mySizeA, MySize mySizeB)
{
    return (mySizeA.m_Width == mySizeB.m_Width &&
            mySizeA.m_Height == mySizeB.m_Height);
}
public static bool operator !=(MySize mySizeA, MySize mySizeB)
{
    return !(mySizeA == mySizeB);
}
public override bool Equals(object obj)
{
    if (obj is MySize)
        return this == (MySize)obj;
    return false;
}
public override int GetHashCode()
{
    return (m_Width * m_Height).GetHashCode();
    // unique hash for each area
}

Операторы сравнения обычно возвращают результат логического типа, хотя это не обязательно, но помните, что не стоит шокировать конечного пользователя!

Другие операторы

Остались условные операторы, &&, ||, и операторы присваивания, +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=. Они не перегружаются, а вычисляются при помощи двухместных операторов. Иными словами, обеспечьте наличие двухместных операторов, и получите их бесплатно!

Заключение

Надеемся, что данное введение в перегрузку операторов было полезным и информативным. Если вы не использовали ее раньше, опробуйте ее в работе – она может оказаться для вас полезным инструментом.

  • Не злоупотребляйте ими. Если оператор или преобразование точно никогда не понадобятся, или не очевидно, каким будет результат, не реализуйте его.
  • Не используйте их неправильно. Можно сделать так, чтобы ++ уменьшал значения, но этого определенно не нужно делать!
  • Будьте очень осторожны при неявном преобразовании ваших классов или структур в другие типы.
  • Помните, что структуры – это типы значения, а классы – это ссылочные типы. Поэтому они по-разному обрабатываются в ваших перегруженных методах.