Прочитав эту главу, вы научитесь:
• реализовывать для своих собственных типов бинарные операторы;
• реализовывать для своих собственных типов унарные операторы;
• создавать для своих собственных типов операторы инкремента и декремента;
• осознавать необходимость реализации некоторых операторов в виде пар;
• реализовывать для своих собственных типов неявную конверсию операторов;
• реализовывать для своих собственных типов явную конверсию операторов.
В примерах книги для выполнения стандартных операций (например, сложения и вычитания) широко использовались стандартные символы операторов (например, + и –). Многие встроенные типы поставляются для каждого оператора со своим предопределенным поведением. Можно также определить, как операторы должны себя вести с вашими собственными структурами и классами. Именно эта тема и будет рассмотрена в данной главе.
Перед тем как углубиться в подробности работы операторов и способы их перегрузки, стоит вспомнить об основных аспектах, кратко перечисленных в следующем списке.
• Операторы используются для объединения операндов в выражениях. У каждого оператора в зависимости от типа, с которым он работает, имеется собственная семантика. Например, когда используются числовые типы, оператор + означает «сложить», а когда строки — «объединить».
• У каждого оператора есть приоритет. Например, приоритет оператора * выше приоритета оператора +. Это означает, что выражение a + b * c является эквивалентом выражения a + (b * c).
• Для каждого оператора определена ассоциативность, задающая направление связанного с ним вычисления: слева направо или справа налево. Например, оператор = имеет правую ассоциативность (выражение с ним вычисляется справа налево), следовательно, выражение a = b = c эквивалентно выражению a = (b = c).
• Оператор, имеющий только один операнд, называется унарным. Например, к унарным относится оператор инкремента (++).
• Оператор, имеющий два операнда, называется бинарным. Например, к бинарным относится оператор умножения (*).
В этой книге представлено множество примеров того, как в C# при определении собственных типов можно перегружать методы. В C# для своих собственных типов можно также перегружать многие из существующих символов операторов, хотя синтаксис при этом может немного различаться. При такой перегрузке реализуемые вами операторы автоматически попадают в четко определенную среду со следующими правилами.
• Изменить приоритет или ассоциативность оператора невозможно, поскольку они определяются символом оператора (например, +), а не типом, в отношении которого используется этот символ оператора (например, int). Следовательно, выражение a + b * c всегда будет эквивалентно выражению a + (b * c) независимо от типов a, b и c.
• Нельзя изменять количество операндов оператора. Например, * (символ умножения) является бинарным оператором. Если объявлять оператор * для собственного типа, он должен быть бинарным оператором.
• Нельзя изобретать новые символы операторов. Например, для возведения числа в степень другого числа нельзя создать такой оператор, как **. Для этого нужно определить метод.
• Нельзя изменять значение операторов, когда они применяются ко встроенным типам. Например, выражение 1 + 2 имеет предопределенное значение, переопределять которое не разрешается. Если бы такое было возможно, то все бы чрезвычайно усложнилось.
• Есть ряд символов операторов, не подлежащих перегрузке. Например, нельзя перегрузить оператор точки (.), который служит признаком доступа к компоненту класса. Если бы и это было возможно, то возникли бы ненужные осложнения.
СОВЕТ Для имитации в качестве оператора квадратных скобок [ ] можно использовать индексаторы. По аналогии с этим для имитации в качестве оператора знака равенства (=) можно использовать свойства, а для имитации в качестве оператора вызова функции — воспользоваться делегатами.
Чтобы определить выбранный оператор, задав ему нужное вам поведение, нужно его перегрузить. При этом используется синтаксис, похожий на синтаксис метода, с возвращаемым значением и параметрами, но в качестве имени метода используются ключевое слово operator и символ объявляемого оператора. Например, следующий код показывает определяемую пользователем структуру по имени Hour, в которой определяется бинарный оператор + для сложения двух экземпляров Hour:
struct Hour
{
public Hour(int initialValue)
{
this.value = initialValue;
}
public static Hour operator +(Hour lhs, Hour rhs)
{
return new Hour(lhs.value + rhs.value);
}
...
private int value;
}
Обратите внимание на следующие обстоятельства.
• Оператор является открытым (public). Все операторы должны быть открытыми.
• Оператор является статическим (static). Все операторы должны быть статическими. К операторам никогда не применяется полиморфизм, и в отношении них не могут использоваться модификаторы virtual, abstract, override или sealed.
• У бинарного оператора (такого, как +, показанный в данном примере) имеются два явно указанных аргумента, а у унарного оператора имеется один явно указанный аргумент. (Программисты, работавшие на C++, должны заметить, что у операторов параметр this никогда не скрывается.)
СОВЕТ При объявлении сильно стилизованных функциональных средств, таких как операторы, полезно внедрить для параметров соглашение о присваивании имен. Например, для бинарных операторов разработчики часто используют имена lhs и rhs (акронимы для операнда, находящегося слева от оператора (left-hand side), и операнда, находящегося справа от него (right-hand side), соответственно).
При использовании оператора + в отношении двух выражений типа Hour компилятор C# автоматически превращает ваш код в вызов метода вашего оператора +. Компилятор C# превращает код
Hour Example(Hour a, Hour b)
{
return a + b;
}
в такой:
Hour Example(Hour a, Hour b)
{
return Hour.operator +(a,b); // Псевдокод
}
Но следует заметить, что этот синтаксис является псевдокодом, недопустимым в C#. Бинарные операторы можно использовать только с их стандартной инфиксной системой записи (с символом между операндами).
Существует еще одно, итоговое правило, которого нужно придерживаться при объявлении оператора: как минимум один параметр должен содержать тип. В предыдущем примере operator + для класса Hour один из параметров, a или b, должен быть Hour-объектом. В данном примере оба параметра относятся к типу Hour. Но случается, что нужно определить дополнительные реализации operator + для сложения, к примеру, целого числа (количество часов) с объектом Hour — у первого параметра должен быть тип Hour, а второй параметр может быть целым числом. Если выполняется это правило, компилятору проще узнать, на что нужно ориентироваться при попытке разрешения вызова оператора, а также гарантировать невозможность изменения значения встроенных операторов.
В предыдущем разделе было показано, как объявляется бинарный оператор + для сложения двух экземпляров типа Hour. У структуры Hour имеется также конструктор, создающий Hour из int. Это означает возможность сложения Hour и int, просто нужно сначала, как показано в следующем примере, воспользоваться Hour-конструктором для преобразования int в Hour:
Hour a = ...;
int b = ...;
Hour sum = a + new Hour(b);
Этот код заведомо допустим, но он менее понятен или выразителен, чем непосредственное сложение Hour и int:
Hour a = ...;
int b = ...;
Hour sum = a + b;
Чтобы легализовать выражение a + b, нужно указать, что оно означает сложение Hour (левого операнда a) с int (правым операндом b). Иными словами, нужно объявить бинарный оператор +, чьим первым параметром будет тип Hour, а вторым — тип int. Рекомендуемый подход показан в следующем примере кода:
struct Hour
{
public Hour(int initialValue)
{
this.value = initialValue;
}
...
public static Hour operator +(Hour lhs, Hour rhs)
{
return new Hour(lhs.value + rhs.value);
}
public static Hour operator +(Hour lhs, int rhs)
{
return lhs + new Hour(rhs);
}
...
private int value;
}
Заметьте, что во второй версии оператора всего лишь создается значение типа Hour из аргумента, имеющего тип int, с последующим вызовом первой версии. Таким образом, реальная логика оператора содержится в одном месте. Главное здесь то, что дополнительный оператор + просто упрощает использование существующих функциональных возможностей. Также следует заметить, что вам не нужно предоставлять множество различных версий этого оператора, у каждой из которых будет другой второй параметр, — вместо этого подстройтесь только под наиболее распространенные и значимые случаи применения и позвольте пользователю класса предпринять любые дополнительные шаги в том случае, когда ему придется иметь дело с необычными вариантами.
Оператор + объявляет, как сложить значение типа Hour, фигурирующее в качестве левого операнда, со значением типа int, фигурирующим в качестве правого операнда. В нем не объявляется, как сложить значение типа int, фигурирующее в качестве левого операнда, и значение типа Hour, фигурирующее в качестве правого операнда:
int a = ...;
Hour b = ...;
Hour sum = a + b; // Ошибка в ходе компиляции
Это противоречит здравому смыслу. Если можно написать выражение a + b, то вполне ожидаемо, что можно написать и выражение b + a. Поэтому вы должны предоставить еще одну перегрузку оператора +:
struct Hour
{
public Hour(int initialValue)
{
this.value = initialValue;
}
...
public static Hour operator +(Hour lhs, int rhs)
{
return lhs + new Hour(rhs);
}
public static Hour operator +(int lhs, Hour rhs)
{
return new Hour(lhs) + rhs;
}
...
private int value;
}
ПРИМЕЧАНИЕ Программистам, работавшим с языком C++, следует заметить, что перегрузку вы должны предоставлять самостоятельно. Компилятор не станет создавать код за вас или молча менять порядок следования двух операндов, чтобы найти подходящий оператор.
Операторы и языковая совместимость
Перегрузка операторов поддерживается или понимается не всеми языками, выполнение программ на которых осуществляется в общеязыковой среде выполнения (common language runtime (CLR)). Чтобы мог использоваться класс из языков, не поддерживающих перегрузку операторов, при их перегрузке нужно предоставлять альтернативный механизм, реализующий такую же функциональную возможность. Предположим, к примеру, что вы реализуете оператор + для структуры Hour следующим образом:
public static Hour operator +(Hour lhs, int rhs)
{
...
}
Если нужно предоставить возможность использования вашего класса из приложения, написанного на языке Microsoft Visual Basic, вы также должны предоставить метод Add, выполняющий ту же самую задачу:
public static Hour Add(Hour lhs, int rhs)
{
...
}
Оператор составного присваивания (например, +=) всегда вычисляется в понятиях его связанности с простым оператором (например, +). Иными словами, инструкция
a += b;
автоматически вычисляется в
a = a + b;
В общем виде выражение a @= b, где символом @ представлен любой допустимый оператор, всегда вычисляется в a = a @ b. Если вы перегрузили соответствующий простой оператор, то при использовании соответствующего ему составного оператора присваивания автоматически вызывается перегруженная версия:
Hour a = ...;
int b = ...;
a += a; // То же самое, что и a = a + a
a += b; // То же самое, что и a = a + b
Первое выражение составного присваивания a += a вполне допустимо, поскольку a относится к типу Hour, а в типе Hour объявляется бинарный оператор +, оба параметра которого относятся к типу Hour. Аналогично этому вполне допустимо и второе выражение составного присваивания a += b, поскольку a относится к типу Hour, а b — к типу int. В типе Hour также объявляется бинарный оператор +, чьим первым параметром является Hour-переменная, а вторым — int-переменная. Но следует иметь в виду, что вы не можете написать выражение b += a, поскольку это то же самое, что написать b = b + a. Хотя сложение допустимо, но присваивание недопустимо, поскольку способов присваивания значения типа Hour переменной, относящейся к встроенному типу int, не существует.
В C# допустимо объявление своих собственных версий операторов инкремента (++) и декремента (– –). При объявлении этих операторов нужно применять обычные правила: они должны быть открытыми, статическими и унарными — получающими только один параметр. Оператор инкремента для структуры Hour объявляется следующим образом:
struct Hour
{
...
public static Hour operator ++(Hour arg)
{
arg.value++;
return arg;
}
...
private int value;
}
Операторы инкремента и декремента уникальны тем, что их можно использовать в префиксной и постфиксной форме. Языку C# присуще разумное использование одного и того же оператора как для префиксной, так и для постфиксной версии. Результатом постфиксного выражения является определение значения операнда перед тем, как будет вычислено само выражение. Иными словами, компилятор фактически преобразует код
Hour now = new Hour(9);
Hour postfix = now++;
в код
Hour now = new Hour(9);
Hour postfix = now;
now = Hour.operator ++(now); // Псевдокод, недопустимый в C#
Результатом префиксного выражения будет значение, возвращаемое оператором, следовательно, компилятор C# фактически преобразует код
Hour now = new Hour(9);
Hour prefix = ++now;
в код
Hour now = new Hour(9);
now = Hour.operator ++(now); // Псевдокод, недопустимый в C#
Hour prefix = now;
Эта эквивалентность означает, что тип возвращаемого значения операторов инкремента и декремента должен совпадать с типом параметра.
Следует иметь в виду, что реализация оператора инкремента в структуре Hour работает только потому, что Hour является структурой. Если сделать Hour классом, но оставить реализацию его оператора инкремента без изменений, то обнаружится, что постфиксная трансляция не дает правильного ответа. Вы, наверное, помните, что класс является ссылочным типом, и если еще раз взглянуть на рассмотренную ранее трансляцию, выполняемую компилятором, то в следующем примере вы сможете понять, почему операторы для класса Hour больше не работают так, как от них ожидалось:
Hour now = new Hour(9);
Hour postfix = now;
now = Hour.operator ++(now); // Псевдокод, недопустимый в C#
Если Hour является классом, то инструкция присваивания postfix = теперь заставляет переменную postfix ссылаться на тот же объект, на который ссылается переменная now. Обновление now автоматически приведет к обновлению postfix! Если Hour является структурой, то инструкция присваивания делает копию now в postfix и любые изменения now не приводят к изменению postfix, то есть вы получаете желаемый результат.
Корректная реализация оператора инкремента для класса Hour будет иметь следующий вид:
class Hour
{
public Hour(int initialValue)
{
this.value = initialValue;
}
...
public static Hour operator ++(Hour arg)
{
return new Hour(arg.value + 1);
}
...
private int value;
}
Обратите внимание на то, что теперь оператор ++ создает на основе данных оригинала новый объект. Данные в новом объекте инкрементируются, но данные в оригинале не изменяются. Это вполне работоспособный вариант, но компилятор при трансляции оператора инкремента всякий раз создает новый объект, что может дорого обойтись из-за расходования памяти и издержек на сборку мусора. Поэтому рекомендуется при определении типов не увлекаться перегрузкой операторов. Эта рекомендация касается всех операторов, а не только оператора инкремента.
Особенностью некоторых операций является применение пар операторов. Например, если есть возможность сравнивать два значения типа Hour путем использования оператора !=, то вполне логично ожидать и сравнения двух Hour-значений с помощью оператора ==. Компилятор C# подкрепляет эти весьма резонные ожидания, настаивая на том, что при определении либо оператора ==, либо оператора != вы должны определить оба оператора. Правило «все или ничего» применимо и к операторам < и >, а также <= и >=. Сам компилятор ни одного из этих партнерских операторов для вас не создает. При всей своей очевидности все партнерские операторы должны быть написаны вами самостоятельно и выражены явным образом. Определения операторов == и != для структуры Hour имеют следующий вид:
struct Hour
{
public Hour(int initialValue)
{
this.value = initialValue;
}
...
public static bool operator ==(Hour lhs, Hour rhs)
{
return lhs.value == rhs.value;
}
public static bool operator !=(Hour lhs, Hour rhs)
{
return lhs.value != rhs.value;
}
...
private int value;
}
Тип возвращаемого этими операторами значения может и не быть булевым. Но у вас должны быть весьма веские причины для использования какого-либо другого типа, в противном случае данные операторы могут стать источником невообразимой путаницы.
Перегрузка операторов равенства
Если в классе определяются операторы == и !=, нужно также перегружать методы Equals и GetHashCode, унаследованные от System.Object (или от System.ValueType, если создается структура). Метод Equals должен демонстрировать точно такое же поведение, что и оператор ==. (Вы должны определить один из них посредством другого.) Метод GetHashCode используется другими классами в среде Microsoft .NET Framework. (Когда, к примеру, объект используется в качестве ключа хэш-таблицы, то, чтобы помочь вычислить хэш-значение, в отношении этого объекта вызывается метод GetHashCode. Более подробно этот вопрос изложен в справочной документации по .NET Framework, предоставляемой средой Visual Studio 2015.) Этот метод должен всего лишь возвращать отличающееся целочисленное значение. Не возвращайте из метода GetHashCode то же самое целочисленное значение, поскольку это сведет на нет эффективность алгоритмов хэширования.
В следующем упражнении вы разработаете класс, имитирующий комплексные числа.
У комплексного числа имеются два элемента: вещественная и мнимая части. Обычно комплексное число представляется в форме (x + yi), где x — это вещественная часть, а yi — мнимая. Значения x и y являются обычными целыми числами, а i представляет собой квадратный корень из –1 (именно поэтому yi называется мнимой частью). Несмотря на то что комплексные числа кажутся слишком сложными для понимания и больше привязанными к теории, чем к практике, они широко применяются в электронике, прикладной математике и физике, а также во многих аспектах проектирования. При желании получить более подробные сведения о пользе комплексных чисел и о том, в чем она выражается, обратитесь к Википедии, где имеется весьма полезная и информативная статья.
ПРИМЕЧАНИЕ В Microsoft .NET Framework версии 4.0 и более поздних имеется тип под названием Complex, который находится в пространстве имен System.Numerics и реализует комплексные числа, поэтому нет никакого смысла в определении собственной версии этого типа. Тем не менее весьма поучительно посмотреть, как для этого типа реализуются некоторые наиболее широко используемые операторы.
Комплексные числа будут реализованы вами в виде пар целочисленных значений, представляющих коэффициенты x и y для вещественной и мнимой частей. Вы также реализуете операторы, необходимые для выполнения простых арифметических операций с использованием комплексных чисел. В табл. 22.1 дается сводка способов выполнения четырех арифметических операций над парами комплексных чисел, (a + bi) и (c + di).
Таблица 22.1
Операция | Вычисление |
(a + bi) + (c + di) | ((a + c) + (b + d)i) |
(a + bi) – (c + di) | ((a – c) + (b – d)i) |
(a + bi)(c + di) | ((ac – bd) + (bc + ad)i) |
(a + bi)/(c + di) | (((ac + bd)/(cc + dd)) + ((bc – ad)/(cc + dd))i) |
Откройте в среде Visual Studio 2015 проект ComplexNumbers, который находится в папке \Microsoft Press\VCSBS\Chapter 22\ComplexNumbers вашей папки документов. Для сборки и тестирования кода будет использоваться консольное приложение. В файле Program.cs содержится уже знакомый вам метод doWork.
В обозревателе решений щелкните на проекте ComplexNumbers. В меню Проект щелкните на пункте Добавить класс. В поле Имя диалогового окна Добавить новый элемент — ComplexNumbers наберите строку Complex.cs, а затем щелкните на кнопке Добавить. Среда Visual Studio создаст класс Complex и откроет файл Complex.cs в окне редактора. Добавьте к классу Complex автоматически создаваемые целочисленные свойства Real и Imaginary, показанные в следующем примере кода жирным шрифтом.
class Complex
{
public int Real { get; set; }
public int Imaginary { get; set; }
}
В этих свойствах будут храниться вещественная и мнимая части комплексного числа.
Добавьте к классу Complex конструктор, показанный далее жирным шрифтом:
class Complex
{
...
public Complex (int real, int imaginary)
{
this.Real = real;
this.Imaginary = imaginary;
}
}
Этот конструктор получает два int-параметра и использует их для заполнения свойств Real и Imaginary. Переопределите метод ToString, применив код, выделенный далее жирным шрифтом:
class Complex
{
...
public override string ToString()
{
return $"({this.Real} + {this.Imaginary}i)";
}
}
Этот метод возвращает строку, представляющую комплексное число в формате (x + yi). Добавьте к классу Complex перегружаемый оператор +, выделенный далее жирным шрифтом:
class Complex
{
...
public static Complex operator +(Complex lhs, Complex rhs)
{
return new Complex(lhs.Real + rhs.Real, lhs.Imaginary + rhs.Imaginary);
}
}
Это бинарный оператор сложения. Он получает два Complex-объекта и складывает их, выполняя вычисление, показанное в табл. 22.1. Оператор возвращает новый Complex-объект, содержащий результаты этого вычисления.
Добавьте к классу Complex перегружаемый оператор -:
class Complex
{
...
public static Complex operator -(Complex lhs, Complex rhs)
{
return new Complex(lhs.Real - rhs.Real, lhs.Imaginary - rhs.Imaginary);
}
}
Этот оператор придерживается такого же формата, что и перегружаемый оператор +.
Реализуйте операторы * и /:
class Complex
{
...
public static Complex operator *(Complex lhs, Complex rhs)
{
return new Complex(lhs.Real * rhs.Real - lhs.Imaginary * rhs.Imaginary,
lhs.Imaginary * rhs.Real + lhs.Real * rhs.Imaginary);
}
public static Complex operator /(Complex lhs, Complex rhs)
{
int realElement = (lhs.Real * rhs.Real + lhs.Imaginary * rhs.Imaginary) /
(rhs.Real * rhs.Real + rhs.Imaginary * rhs.Imaginary);
int imaginaryElement = (lhs.Imaginary * rhs.Real - lhs.Real *
rhs.Imaginary) / (rhs.Real * rhs.Real + rhs.Imaginary * rhs.Imaginary);
return new Complex(realElement, imaginaryElement);
}
}
Эти операторы придерживаются того же формата, что и предыдущие два оператора, хотя выполнить вычисление немного сложнее. (Вычисление для оператора / было разбито на две части, чтобы строки кода не получались слишком длинными.)
Выведите в окно редактора файл Program.cs. Добавьте к методу doWork следующие инструкции, выделенные далее жирным шрифтом, и удалите комментарий // TODO::
static void doWork()
{
Complex first = new Complex(10, 4);
Complex second = new Complex(5, 2);
Console.WriteLine($"first is {first}");
Console.WriteLine($"second is {second}");
Complex temp = first + second;
Console.WriteLine($"Add: result is {temp}");
temp = first - second;
Console.WriteLine($"Subtract: result is {temp}");
temp = first * second;
Console.WriteLine($"Multiply: result is {temp}");
temp = first / second;
Console.WriteLine($"Divide: result is {temp}");
}
Этот код создает два Complex-объекта, которые представляют комплексные значения (10 + 4i) и (5 + 2i). Код выводит их на экран, а затем тестирует все только что определенные вами операторы, выводя результаты для каждого случая.
В меню Отладка щелкните на пункте Запуск без отладки. Убедитесь в том, что приложение выводит на экран результаты, показанные на рис. 22.1.
Рис. 22.1
Закройте приложение и вернитесь в среду Visual Studio 2015.
Вы только что создали тип, моделирующий комплексные числа и поддерживающий основные арифметические операции. В следующем упражнении вы расширите класс Complex и предоставите для него операторы равенства == и !=.
Выведите в окно редактора среды Visual Studio 2015 файл Complex.cs. Добавьте к классу Complex операторы == и !=, показанные далее жирным шрифтом:
class Complex
{
...
public static bool operator ==(Complex lhs, Complex rhs)
{
return lhs.Equals(rhs);
}
public static bool operator !=(Complex lhs, Complex rhs)
{
return !(lhs.Equals(rhs));
}
}
Обратите внимание на то, что оба эти оператора используют метод Equals, который сравнивает экземпляр класса с другим экземпляром класса, указанным в качестве аргумента. Он возвращает true, если их значения равны, и false, если они не равны.
Щелкните в меню Сборка на пункте Пересобрать решение.
В окне Список ошибок появятся следующие предупреждающие сообщения:
'ComplexNumbers.Complex' defines operator == or operator != but does not override
Object.Equals(object o)
'ComplexNumbers.Complex' defines operator == or operator != but does not override
Object.GetHashCode()
Если вы определили операторы != и ==, то нужно также переопределить методы Equals и GetHashCode, унаследованные от System.Object.
ПРИМЕЧАНИЕ Если список ошибок не показан, щелкните в меню Вид на пункте Список ошибок.
Переопределите метод Equals в классе Complex, как показано далее жирным шрифтом:
class Complex
{
...
public override bool Equals(Object obj)
{
if (obj is Complex)
{
Complex compare = (Complex)obj;
return (this.Real == compare.Real) &&
(this.Imaginary == compare.Imaginary);
}
else
{
return false;
}
}
}
В качестве параметра метод Equals получает значение типа Object. Этот код проверяет, что тип параметра фактически является Complex-объектом. Если так оно и есть, код сравнивает значения в свойствах Real и Imaginary в текущем экземпляре и в переданном ему параметре. Если они одинаковы, метод возвращает true, а если нет — false. Если переданный параметр не является Complex-объектом, метод возвращает false.
ВНИМАНИЕ У вас может возникнуть соблазн написать метод Equals следующим образом:
public override bool Equals(Object obj)
{
Complex compare = obj as Complex;
if (compare != null)
{
return (this.Real == compare.Real) &&
(this.Imaginary == compare.Imaginary);
}
else
{
return false;
}
}
Но выражение compare != null вызывает оператор != класса Complex, который опять вызывает метод Equals, что приводит к рекурсивному циклу.
Переопределите метод GetHashCode. Эта реализация просто вызывает метод, унаследованный от класса Object, но если хотите, то для создания хэш-кода для объекта можете предоставить собственный механизм:
Class Complex
{
...
public override int GetHashCode()
{
return base.GetHashCode();
}
}
Щелкните в меню Сборка на пункте Пересобрать решение. Убедитесь в том, что теперь решение проходит сборку без каких-либо предупреждений.
Выведите в окно редактора файл Program.cs и добавьте к концу метода doWork следующий код, выделенный жирным шрифтом:
static void doWork()
{
...
if (temp == first)
{
Console.WriteLine("Comparison: temp == first");
}
else
{
Console.WriteLine("Comparison: temp != first");
}
if (temp == temp)
{
Console.WriteLine("Comparison: temp == temp");
}
else
{
Console.WriteLine("Comparison: temp != temp");
}
}
ПРИМЕЧАНИЕ Выражение temp == temp вызывает вывод предупреждающего сообщения «Comparison made to same variable; did you mean to compare to something else?» («Сравнение проводится с той же самой переменной; возможно, вы предполагали провести сравнение с чем-нибудь другим?») В данном случае можете проигнорировать предупреждение, поскольку такое сравнение выполнено намеренно, чтобы проверить, что оператор == работает так, как ожидалось.
В меню Отладка щелкните на пункте Запуск без отладки. Убедитесь в том, что последние два сообщения, выведенные на экран, выглядят следующим образом:
Comparison: temp != first
Comparison: temp == temp
Закройте приложение и вернитесь в среду Visual Studio 2015.
Иногда нужно преобразовать выражение одного типа в выражение другого типа. Например, следующий метод объявлен с единственным параметром типа double:
class Example
{
public static void MyDoubleMethod(double parameter)
{
...
}
}
Вполне логично ожидать, что когда ваш код вызывает метод MyDoubleMethod, в качестве аргумента могут использоваться только значения типа double, но это не так. Компилятор C# также позволяет методу MyDoubleMethod быть вызванным с аргументом какого-либо другого типа, но только если значение аргумента может быть преобразовано в double. Например, если предоставить аргумент типа int, компилятор при вызове метода создаст код, преобразующий значение аргумента в double.
Во встроенных типах имеются некоторые встроенные преобразования. Например, как уже упоминалось, int-значение может быть неявным образом преобразовано в значение типа double. Неявное преобразование не требует специального синтаксиса и никогда не выдает исключений:
Example.MyDoubleMethod(42); // Неявное преобразование int в double
Иногда неявное преобразование называют расширяющим преобразованием, поскольку результат оказывается шире исходного значения — в нем содержится больше информации, чем в исходном значении, и при этом ничего не теряется. В случае с типами int и double диапазон double шире диапазона int и все int-значения имеют эквивалентные им double-значения. Но обратное утверждение будет неверным, и double-значение не может быть неявным образом преобразовано в int-значение:
class Example
{
public static void MyIntMethod(int parameter)
{
...
}
}
...
Example.MyIntMethod(42.0); // Ошибка в ходе компиляции
При преобразовании double в int есть риск потери информации, поэтому преобразование не выполняется в автоматическом режиме. (Подумайте, что получилось бы, если бы аргументом MyIntMethod было число 42.5. Как бы оно могло быть преобразовано?) Значение типа double может быть преобразовано в значение типа int, но преобразование требует явной записи (приведения типа):
Example.MyIntMethod((int)42.0);
Явное преобразование иногда называют сужающим преобразованием, поскольку результат получается менее широким, чем исходное значение (то есть в нем может содержаться меньше информации), и может выдать исключение OverflowException, если результат значения выходит за допустимый диапазон целевого типа. В C# можно создать операторы преобразования для самостоятельно определяемых вами типов, чтобы контролировать целесообразность преобразования значений в другие типы. Также можно указать, будет ли это преобразование неявным или явным.
Синтаксис для объявления операторов преобразования, определяемых пользователем, отчасти похож на объявление перегружаемого оператора, но у него также имеется несколько важных отличий. Вот как выглядит оператор преобразования, позволяющий Hour-объекту быть неявным образом преобразованным в int-значение:
struct Hour
{
...
public static implicit operator int (Hour from)
{
return from.value;
}
private int value;
}
Оператор преобразования должен быть открытым (public), а также статическим (static). Тип, из которого осуществляется преобразование, объявляется в качестве параметра (в данном случае это Hour), а тип, в который осуществляется преобразование, объявляется в качестве названия типа после ключевого слова operator (в данном случае это int). Возвращаемого значения, указываемого перед ключевым словом operator, здесь нет.
При объявлении собственных операторов преобразования нужно указать, какими именно они являются — неявными или явными. Это делается путем использования ключевых слов implicit и explicit. Оператор преобразования Hour в int, показанный в предыдущем примере, является неявным, а это означает, что компилятор C# может использовать его, не требуя при этом приведения типа:
class Example
{
public static void MyOtherMethod(int parameter) { ... }
public static void Main()
{
Hour lunch = new Hour(12);
Example.MyOtherMethod(lunch); // Неявное преобразование Hour в int
}
}
Если оператор преобразования был объявлен явным, то есть с ключевым словом explicit, предыдущий пример не будет скомпилирован, поскольку оператор явного преобразования требует приведения типа.
Example.MyOtherMethod((int)lunch); // Явное преобразование Hour в int
Когда нужно объявлять оператор преобразования явным, а когда неявным? Если преобразование всегда проводится безопасно, без риска потери информации, и не может выдать исключение, оно может быть определено как неявное. В противном случае оно должно быть объявлено как явное. Преобразование из Hour в int всегда безопасно — для каждого значения Hour имеется соответствующее значение int, следовательно, есть смысл сделать его неявным. Оператор, преобразующий строковое значение string в значение Hour, должен осуществлять явное преобразование, поскольку не все строки являются допустимым представлением значений для часа — Hours. (Строка «7» подходит, а как можно преобразовать в Hour строку «Hello, World»?)
Операторы преобразования предоставляют вам альтернативный способ решения проблемы симметричных операторов. Например, вместо предоставления показанных ранее трех версий оператора + (Hour + Hour, Hour + int и int + Hour) для структуры Hour вы можете предоставить единственную версию оператора + (которая принимает два Hour-параметра) и неявное преобразование int в Hour:
struct Hour
{
public Hour(int initialValue)
{
this.value = initialValue;
}
public static Hour operator +(Hour lhs, Hour rhs)
{
return new Hour(lhs.value + rhs.value);
}
public static implicit operator Hour (int from)
{
return new Hour (from);
}
...
private int value;
}
Если сложить Hour и int (в любом порядке), компилятор C# автоматически преобразует int в Hour, а затем вызовет оператор + с двумя аргументами типа Hour:
void Example(Hour a, int b)
{
Hour eg1 = a + b; // b преобразуется в Hour
Hour eg2 = b + a; // b преобразуется в Hour
}
В заключительном упражнении этой главы вы добавите операторы преобразования к классу Complex. Сначала вы напишете пару операторов преобразования, позволяющих осуществлять преобразование между типами int и Complex. Преобразование int в объект типа Complex всегда выполняется безопасно и никогда не приводит к потере информации, поскольку int фактически является простым комплексным числом без мнимой части. Оно будет реализовано в виде оператора неявного преобразования. Но обратное утверждение будет неверным — чтобы преобразовать Complex-объект в int, нужно отбросить мнимую часть. Следовательно, реализация будет выполнена в виде создания оператора явного преобразования.
Вернитесь в среду Visual Studio 2015 и выведите в окно редактора файл Complex.cs. Добавьте к коду, который следует за классом Complex, сразу после существующего конструктора и перед методом ToString еще один конструктор, показанный далее жирным шрифтом. Этот новый конструктор получит один int-параметр, который используется им для инициализации свойства Real. Для свойства Imaginary устанавливается значение 0:
class Complex
{
...
public Complex(int real)
{
this.Real = real;
this.Imaginary = 0;
}
...
}
Добавьте к классу Complex следующий оператор неявного преобразования:
class Complex
{
...
public static implicit operator Complex(int from)
{
return new Complex(from);
}
...
}
Этот оператор осуществляет преобразование из int в Complex-объект, возвращая новый экземпляр класса Complex с помощью только что созданного вами конструктора.
Добавьте к классу Complex следующий оператор явного преобразования, показанный далее жирным шрифтом:
class Complex
{
...
public static explicit operator int(Complex from)
{
return from.Real;
}
...
}
Этот оператор получает Complex-объект и возвращает значение свойства Real. При этом преобразовании отбрасывается мнимая часть комплексного числа.
Выведите в окно редактора файл Program.cs и добавьте к концу метода doWork следующий код, показанный далее жирным шрифтом:
static void doWork()
{
...
Console.WriteLine($"Current value of temp is {temp}");
if (temp == 2)
{
Console.WriteLine("Comparison after conversion: temp == 2");
}
else
{
Console.WriteLine("Comparison after conversion: temp != 2");
}
temp += 2;
Console.WriteLine($"Value after adding 2: temp = {temp}");
}
Эти инструкции выполняют тестирование оператора неявного преобразования int в Complex-объект. Инструкция if сравнивает Complex-объект с int. Компилятор создает код, который сначала преобразует int в Complex-объект, а затем вызывает оператор == класса Complex. Инструкция, которая прибавляет 2 к значению переменной temp, преобразует int-значение 2 в Complex-объект, а затем использует оператор + класса Complex.
Добавьте к концу метода doWork следующие инструкции:
static void doWork()
{
...
int tempInt = temp;
Console.WriteLine($"Int value after conversion: tempInt == {tempInt}");
}
Первая инструкция пытается присвоить Complex-объект int-переменной.
Щелкните в меню Сборка на пункте Пересобрать решение. Сборка пройдет неудачно, и компилятор сообщит в окне Список ошибок о следующей ошибке:
Cannot implicitly convert type 'ComplexNumbers.Complex' to 'int'. An explicit
conversion exists (are you missing a cast?)
Оператор, осуществляющий преобразование из Complex-объекта в int-значение, является оператором явного преобразования, поэтому нужно указать приведение типа.
Измените инструкцию, которая пытается сохранить Complex-значение в int-переменной, так, чтобы в ней использовалось приведение типа:
int tempInt = (int)temp;
В меню Отладка щелкните на пункте Запуск без отладки. Убедитесь в том, что теперь решение проходит сборку и выведенные на экран последние четыре сообщения имеют следующий вид:
Current value of temp is (2 + 0i)
Comparison after conversion: temp == 2
Value after adding 2: temp = (4 + 0i)
Int value after conversion: tempInt == 4
Закройте приложение и вернитесь в среду Visual Studio 2015.
В этой главе вы научились перегружать операторы и предоставлять соответствующие функциональные средства применительно к классу или структуре. Вами были реализованы несколько арифметических операторов, а также созданы операторы, с помощью которых можно сравнивать экземпляры класса. И наконец, вы научились создавать операторы неявного и явного преобразования.
Если хотите продолжить работу и изучить следующую главу, оставьте открытой среду Visual Studio 2015 и переходите к главе 23 «Повышение производительности путем использования задач».
Если сейчас вы хотите выйти из среды Visual Studio 2015, то в меню Файл щелкните на пункте Выход. Увидев диалоговое окно с предложением сохранить изменения, щелкните на кнопке Да и сохраните проект.
Чтобы | Сделайте следующее |
Реализовать оператор | Напишите ключевые слова public и static, укажите после них возвращаемое значение, затем ключевое слово operator, а за ним — символ объявляемого оператора, после которого укажите в круглых скобках соответствующие параметры. Реализуйте в теле метода логику для оператора, например: class Complex { ... public static bool operator ==(Complex lhs, Complex rhs) { ... // Реализация логики для оператора == } ... } |
Определить оператор преобразования | Напишите ключевые слова public и static, укажите после них ключевое слово implicit или explicit, а за ним — ключевое слово operator и тип, в который осуществляется преобразование, после чего укажите преобразуемый тип в виде единственного параметра, заключенного в круглые скобки, например: class Complex { ... public static implicit operator Complex(int from) { ... // Код для преобразования из int } ... } |