Введение в перегрузку операторов в C#
Введение
Перегрузка операторов – это мощная и недостаточно используемая (но часто неправильно используемая) возможность, способная сделать код более простым, а использование объектов - более интуитивно-понятным. Добавление нескольких простых перегруженных операторов в класс или структуру даст вам возможность:
- выполнять преобразования в/ из вашего типа (и) в другие типы
- производить математические/логические операции над вашим типом и над ним самим или над другими типами
Операторы преобразования
Существуют несколько способов преобразования между типами, но среди них наиболее распространенный - это использование неявных/явных операторов преобразования.
Неявное преобразование
Неявное преобразование означает, что можно напрямую присваивать экземпляр одного типа другому.
Давайте рассмотрим пример.
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
}
Операторы сравнения обычно возвращают результат логического типа, хотя это не обязательно, но помните, что не стоит шокировать конечного пользователя!
Другие операторы
Остались условные операторы, &&, ||, и операторы присваивания, +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=. Они не перегружаются, а вычисляются при помощи двухместных операторов. Иными словами, обеспечьте наличие двухместных операторов, и получите их бесплатно!
Заключение
Надеемся, что данное введение в перегрузку операторов было полезным и информативным. Если вы не использовали ее раньше, опробуйте ее в работе – она может оказаться для вас полезным инструментом.
- Не злоупотребляйте ими. Если оператор или преобразование точно никогда не понадобятся, или не очевидно, каким будет результат, не реализуйте его.
- Не используйте их неправильно. Можно сделать так, чтобы ++ уменьшал значения, но этого определенно не нужно делать!
- Будьте очень осторожны при неявном преобразовании ваших классов или структур в другие типы.
- Помните, что структуры – это типы значения, а классы – это ссылочные типы. Поэтому они по-разному обрабатываются в ваших перегруженных методах.