Как перегрузить оператор c

от admin

Как перегрузить оператор c

Перегрузка операторов (operator overloading) позволяет определить для объектов классов втроенные операторы, такие как +, -, * и т.д. Для определения оператора для объектов своего класса, необходимо определить функцию, название которой содержит слово operator и символ перегружаемого оператора. Функция оператора может быть определена как член класса, либо вне класса.

Перегрузить можно только те операторы, которые уже определены в C++. Создать новые операторы нельзя. Также нельзя изменить количество операндов, их ассоциативность, приоритет.

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

Формальное определение операторов в виде функций-членов класса:

Формальное определение операторов в виде функций, которые не являются членами класса:

Здесь ClassType представляет тип, для которого определяется оператор. Type — тип другого операнда, который может совпадать, а может и не совпадать с первым. ReturnType — тип возвращаемого результата, который также может совпадать с одним из типов операндов, а может и отличаться. Op — сама операция.

Рассмотрим пример с классом Counter, который хранит некоторое число:

Здесь в классе Counter определен оператор сложения, цель которого сложить два объекта Counter:

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

Результатом оператора сложения является новый объект Counter, в котором значение value равно сумме значений value обоих операндов.

После опеределения оператора можно складывать два объекта Counter:

Подобным образом можно определить функцию оператора вне класса:

Если бинарный оператор определяется в виде внешней функции, как здесь, то он принимает два параметра. Первый параметр будет представлять левый операнд операции, а второй параметр — правый операнд.

Но по сравнению с предыдущим кодом здесь сделано еще пару изменений. Во-первых, внешняя функция естественно не может обращаться к приватным полям класса, поэтому для доступа к ним придется создавать отдельные функции, которые бы возвращали значения полей. Я для простоты просто сделал переменную value публичной. Другим решением в данном случае могло быть определение дружественной функции оператора. Второй момент — внешние функции оператора не могут быть константными. Поэтому гораздо определение операторов внутри класса имеет некоторые преимущества.

Стоит отметить, что необязательно возвращать объект класса. Это может быть и любой объект в зависимости от ситуации. И также мы можем определять дополнительные перегруженные функции операторов:

Здесь определена вторая версия оператора сложения, которая складывает объект Counter с числом и возвращает также число. Поэтому левый операнд операции должен представлять тип Counter, а правый операнд — тип int.

Какие операторы где переопределять? Операторы присвоения, индексирования ([]), вызова (()), доступа к члену класса по указателю (->) следует определять в виде функций-членов класса. Операторы, которые изменяют состояние объекта или непосредственно связаны с объектом (инкремент, декремент), обычно также определяются в виде функций-членов класса. Операторы выделения и удаления памяти ( new new[] delete delete[] ) определяются только в виде функций, которые не являются членами класса. Все остальные операторы можно определять как отдельные функции, а не члены класса.

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

Результатом операторов сравнения ( == , != , < , > ), как правило, является значение типа bool . Например, перегрузим данные операторы для типа Counter:

По умолчанию будут сравниваться все поля класса, для которых определен оператор ==. Если значения всех полей будут равны, то оператор возвратить true

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

Оператор присвоения обычно возвращает ссылку на свой левый операнд:

Унарные операции

Унарные операции обычно возвращают новый объект, созданный на основе имеющегося. Например, возьмем операцию унарного минуса:

Здесь операция унарного минуса возвращает новый объект Counter, значение value в котором фактически равно значению value текущего объекта, умноженного на -1.

Операции инкремента и декремента

Особую сложность может представлять переопределение операций инкремента и декремента, поскольку нам надо определить и префиксную, и постфиксную форму для этих операторов. Определим подобные операторы для типа Counter:

Префиксные операторы должны возвращать ссылку на текущий объект, который можно получить с помощью указателя this:

В самой функции можно определить некоторую логику по инкременту значения. В данном случае значение value увеличивается на 1.

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

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

Переопределение оператора <<

оператор << принимает два аргумента: ссылку на объект потока (левый операнд) и фактическое значение для вывода (правый операнд). Затем он возвращает новую ссылку на поток, которую можно передать при следующем вызове оператора << в цепочке.

Стандартный выходной поток cout имеет тип std::ostream . Поэтому первый параметр (левый операнд) представляет объект ostream , а второй (правый операнд) — выводимый объект Counter. Поскольку мы не можем изменить стандартное определение std::ostream, поэтому определяем функцию оператора, которая не является членом класса.

В данном случае для выводим значение переменной value. Для получения значения value извне класса Counter я добавил функцию getValue() .

Возвращаемое значение всегда должно быть ссылкой на тот же объект потока, на который ссылается левый операнд оператора.

После определения функции оператора можно выводить на консоль объекты Counter:

Выражение одних операторов через другие

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

Здесь вначале реализован оператор сложения с присвоением +=:

В функции оператора сложения мы создаем копию текущего объекта и к этой копии и аргументу применяем оператор +=:

В данном случае суть сложения: к полю value прибавляем значение value другого объекта. Однако логика оператора может быть более сложной, и чтобы не повторяться, мы можем таким образом выражать одни операторы через другие.

Перегрузка операторов в C++. Способы применения

В прошлой части мы рассмотрели основные аспекты использования перегрузки операторов. В этом материалы вашему вниманию будут представлены перегружаемые операторы C++. Для каждого раздела характерна семантика, т.е. ожидаемое поведение. Кроме того, будут показаны типичные способы объявления и реализации операторов.

В примерах кода X означает пользовательский тип, для которого реализован оператор. T — это необязательный тип, пользовательский либо встроенный. Параметры бинарного оператора будут называться lhs и rhs . Если оператор будет объявлен как метод класса, у его объявления будет префикс X:: .

operator=

  • Определение справа налево: в отличие от большинства операторов, operator= правоассоциативен, т.е. a = b = c означает a = (b = c) .
Копирование
  • Семантика: присваивание a = b . Значение или состояние b передаётся a . Кроме того, возвращается ссылка на a . Это позволяет создавать цепочки вида c = a = b .
  • Типичное объявление: X& X::operator= (X const& rhs) . Возможны другие типы аргументов, но используется это нечасто.
  • Типичная реализация:
Перемещение (начиная с C++11)
  • Семантика: присваивание a = temporary() . Значение или состояние правой величины присваивается a путём перемещения содержимого. Возвращается ссылка на a .
  • Типичные объявление и реализация:
  • Сгенерированный компилятором operator= : компилятор может создать только два вида этого оператора. Если же оператор не объявлен в классе, компилятор пытается создать публичные операторы копирования и перемещения. Начиная с C++11 компилятор может создавать оператор по умолчанию:

operator+, -, *, /, %

  • Семантика: операции сложения, вычитания, умножения, деления, деления с остатком. Возвращается новый объект с результирующим значением.
  • Типичные объявление и реализация:

Обычно, если существует operator+ , имеет смысл также перегрузить и operator+= для того, чтобы использовать запись a += b вместо a = a + b . Если же operator+= не перегружен, реализация будет выглядеть примерно так:

Унарные operator+, —

  • Семантика: положительный или отрицательный знак. operator+ обычно ничего не делает и поэтому почти не используется. operator- возвращает аргумент с противоположным знаком.
  • Типичные объявление и реализация:

operator<<, >>

  • Семантика: во встроенных типах операторы используются для битового сдвига левого аргумента. Перегрузка этих операторов с именно такой семантикой встречается редко, на ум приходит лишь std::bitset . Однако, для работы с потоками была введена новая семантика, и перегрузка операторов ввода/вывода весьма распространена.
  • Типичные объявление и реализация: поскольку в стандартные классы iostream добавлять методы нельзя, операторы сдвига для определённых вами классов нужно перегружать в виде свободных функций:

Кроме того, тип левого операнда может быть любым классом, которые должен вести себя как объект ввода/вывода, то есть правый операнд может быть и встроенного типа.

Бинарные operator&, |, ^

  • Семантика: Битовые операции «и», «или», «исключающее или». Эти операторы перегружаются очень редко. Опять же, единственным примером является std::bitset .

operator+=, -=, *=, /=, %=

  • Семантика: a += b обычно означает то же, что и a = a + b . Поведение остальных операторов аналогично.
  • Типичные определение и реализация: поскольку операция изменяет левый операнд, скрытое приведение типов нежелательно. Поэтому эти операторы должны быть перегружены как методы класса.

operator&=, |=, ^=, <<=, >>=

  • Семантика: аналогична operator+= , но для логических операций. Эти операторы перегружаются так же редко, как и operator| и т.д. operator<<= и operator>>= не используются для операций ввода/вывода, поскольку operator<< и operator>> уже изменяют левый аргумент.

operator==, !=

  • Семантика: проверка на равенство/неравенство. Смысл равенства очень сильно зависит от класса. В любом случае, учитывайте следующие свойства равенств:
    1. Рефлексивность, т.е. a == a .
    2. Симметричность, т.е. если a == b , то b == a .
    3. Транзитивность, т.е. если a == b и b == c , то a == c .
  • Типичные объявление и реализация:

operator<, <=, >, >=

  • Семантика: проверка на соотношение (больше, меньше и т.д.). Обычно используется, если порядок элементов однозначно определён, то есть сложные объекты с несколькими характеристиками сравнивать бессмысленно.
  • Типичные объявление и реализация:

Реализация operator> с использованием operator< или наоборот обеспечивает однозначное определение. operator<= может быть реализован по-разному, в зависимости от ситуации. В частности, при отношении строго порядка operator== можно реализовать лишь через operator< :

operator++, —

  • Семантика: a++ (постинкремент) увеличивает значение на 1 и возвращает старое значение. ++a (преинкремент) возвращает новое значение. С декрементом operator— все аналогично.
  • Типичные объявление и реализация:

operator()

  • Семантика: исполнение объекта-функции (функтора). Обычно используется не для изменения объекта, а для использования его в качестве функции.
  • Нет ограничений на параметры: в отличие от прошлых операторов, в этом случае нет никаких ограничений на количество и тип параметров. Оператор может быть перегружен только как метод класса.
  • Пример объявления:

operator[]

  • Семантика: доступ к элементам массива или контейнера, например, в std::vector , std::map , std::array .
  • Объявление: тип параметра может быть любым. Тип возвращаемого значения обычно является ссылкой на то, что хранится в контейнере. Часто оператор перегружается в двух версиях, константной и неконстантной:

operator!

  • Семантика: отрицание в логическом смысле.
  • Типичные объявление и реализация:

explicit operator bool

  • Семантика: использования в логическом контексте. Чаще всего используется с умными указателями.
  • Реализация:

operator&&, ||

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

Унарный operator*

  • Семантика: разыменовывание указателя. Обычно перегружается для классов с умными указателями и итераторами. Возвращает ссылку на то, куда указывает объект.
  • Типичные объявление и реализация:

operator->

  • Семантика: доступ к полю по указателю. Как и предыдущий, этот оператор перегружается для использования с умными указателями и итераторами. Если в коде встречается оператор -> , компилятор перенаправляет вызовы на operator-> , если возвращается результат пользовательского типа.
  • Usual implementation:

operator->*

  • Семантика: доступ к указателю-на-поле по указателю. Оператор берёт указатель на поле и применяет его к тому, на что указывает *this , то есть objPtr->*memPtr — это то же самое, что и (*objPtr).*memPtr . Используется очень редко.
  • Возможная реализация:

Унарный operator&

  • Семантика: адресный оператор. Этот оператор перегружают очень редко.

operator,

  • Семантика: встроенный оператор «запятая», применённый к двум выражениям, выполняет их оба в порядке записи и возвращает значение второго из них. Перегружать его не рекомендуется.

operator

  • Семантика: оператор побитовой инверсии. Один из наиболее редко используемых операторов.

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

  • Семантика: позволяет скрытое или явное приведение объектов класса к другим типам.
  • Объявление:

operator new, new[], delete, delete[]

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

Заключение

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

Name already in use

csharp_for_beginners / c9.md

  • Go to file T
  • Go to line L
  • Copy path
  • Copy permalink
  • Open with Desktop
  • View raw
  • Copy raw contents Copy raw contents

Copy raw contents

Copy raw contents

ГЛАВА 9. Перегрузка операторов

В языке C# допускается определять назначение опе­ ратора по отношению к создаваемому классу. Этот процесс называется перегрузкой операторов. Благода­ ря перегрузке расширяется сфера применения оператора в классе. При этом действие оператора полностью контро­ лируется и может меняться в зависимости от конкретного класса. Например, оператор + может использоваться для ввода объекта в связный список в одном классе, где опреде­ ляется такой список, тогда как в другом классе его назначе­ ние может оказаться совершенно иным.

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

Главное преимущество перегрузки операторов заключа­ ется в том, что она позволяет плавно интегрировать класс нового типа в среду программирования. Подобного рода расширяемость типов является важной составляющей эффективности такого объектно-ориентированного языка программирования, как С#. Как только для класса опреде­ ляются операторы, появляется возможность оперировать объектами этого класса, используя обычный синтаксис вы­ ражений в С#. Перегрузка операторов является одной из самых сильных сторон языка С#.

Основы перегрузки операторов

Перегрузка операторов тесно связана с перегрузкой методов. Для перегрузки опе­ ратора служит ключевое слово operator, определяющее операторный метод, который, в свою очередь, определяет действие оператора относительно своего класса.

Существуют две формы операторных методов (operator): одна — для унарных операторов, другая — для бинарных. Ниже приведена общая форма для каждой раз­ новидности этих методов.

Здесь вместо ор подставляется перегружаемый оператор, например + или /; а воз­ вращаемый_тип обозначает конкретный тип значения, возвращаемого указанной опе­ рацией. Это значение может быть любого типа, но зачастую оно указывается такого же типа, как и у класса, для которого перегружается оператор. Такая корреляция упро­ щает применение перегружаемых операторов в выражениях. Для унарных операторов операнд обозначает передаваемый операнд, а для бинарных операторов то же самое обозначают операнд1 и операнд2. Обратите внимание на то, что операторные мето­ ды должны иметь оба типа, public и static.

Тип операнда унарных операторов должен быть таким же, как и у класса, для ко­ торого перегружается оператор. А в бинарных операторах хотя бы один из операндов должен быть такого же типа, как и у его класса. Следовательно, в C# не допускается перегрузка любых операторов для объектов, которые еще не были созданы. Например, назначение оператора + нельзя переопределить для элементов типа int или string. И еще одно замечание: в параметрах оператора нельзя использовать модификатор ref или out.

Перегрузка бинарных операторов

Для того чтобы продемонстрировать принцип действия перегрузки операторов, начнем с простого примера, в котором перегружаются два оператора — + и -. В при­ веденной ниже программе создается класс ThreeD, содержащий координаты объекта в трехмерном пространстве. Перегружаемый оператор + складывает отдельные коор­ динаты одного объекта типа ThreeD с координатами другого. А перегружаемый опе­ ратор — вычитает координаты одного объекта из координат другого.

При выполнении этой программы получается следующий результат.

Внимательно проанализируем приведенную выше программу, начиная с перегру­ жаемого оператора +. Когда оператор + оперирует двумя объектами типа ThreeD, то величины их соответствующих координат складываются, как показано в объявлении операторного метода operator+(). Следует, однако, иметь в виду, что этот оператор не видоизменяет значения своих операндов, а лишь возвращает новый объект типа ThreeD, содержащий результат операции сложения координат. Для того чтобы стало понятнее, почему операция + не меняет содержимое объектов, выступающих в роли ее операндов, обратимся к примеру обычной операции арифметического сложения: 10 + 12. Результат этой операции равен 22, но она не меняет ни число 10, ни число 12. Несмотря на то что ни одно из правил не препятствует перегруженному оператору изменить значение одного из своих операндов, все же лучше, чтобы действия этого оператора соответствовали его обычному назначению.

Обратите внимание на то, что метод operator+() возвращает объект типа ThreeD. Этот метод мог бы возвратить значение любого допустимого в C# типа, но благодаря тому что он возвращает объект типа ThreeD, оператор + можно использовать в та­ ких составных выражениях, как a+b+с. В данном случае выражение а+b дает резуль­ тат типа ThreeD, который можно затем сложить с объектом с того же типа. Если бы выражение а+b давало результат другого типа, то вычислить составное выражение a+b+с было бы просто невозможно.

Следует также подчеркнуть, что когда отдельные координаты точек складываются в операторе operator+(), то в результате такого сложения получаются целые значения, поскольку отдельные координаты х, у и z представлены целыми величинами. Но сама перегрузка оператора + для объектов типа ThreeD не оказывает никакого влияния на операцию сложения целых значений, т.е. она не меняет первоначальное назначение этого оператора.

А теперь проанализируем операторный метод operator-(). Оператор — действу­ ет так же, как и оператор +, но для него важен порядок следования операндов. Напом­ ним, что сложение носит коммутативный характер (от перестановки слагаемых сумма не меняется), чего нельзя сказать о вычитании: А — В не то же самое, что и В — А! Для всех двоичных операторов первым параметром операторного метода является левый операнд, а вторым параметром — правый операнд. Поэтому, реализуя перегружае­ мые варианты некоммутативных операторов, следует помнить, какой именно операнд должен быть указан слева и какой — справа.

Перегрузка унарных операторов

Унарные операторы перегружаются таким же образом, как и бинарные. Главное отличие заключается, конечно, в том, что у них имеется лишь один операнд. В каче­ стве примера ниже приведен метод, перегружающий оператор унарного минуса для класса ThreeD.

В данном примере создается новый объект, в полях которого сохраняются отрица­ тельные значения операнда перегружаемого унарного оператора, после чего этот объ­ ект возвращается операторным методом. Обратите внимание на то, что сам операнд не меняется. Это означает, что и в данном случае обычное назначение оператора унарного минуса сохраняется. Например, результатом выражения

является отрицательное значение операнда b, но сам операнд b не меняется. В C# перегрузка операторов ++ и — осуществляется довольно просто. Для этого достаточно возвратить инкрементированное или декрементированное значение, но не изменять вызывающий объект. А все остальное возьмет на себя компилятор С#, раз­ личая префиксные и постфиксные формы этих операторов. В качестве примера ниже приведен операторный метод operator++() для класса ThreeD.

Ниже приведен расширенный вариант предыдущего примера программы, в кото ром демонстрируется перегрузка унарных операторов — и ++.

Координаты точки а: 1, 2, 3 Координаты точки b: 10, 10, 10 Результат сложения а + b: 11, 12, 13 Результат сложения а + b + с: 22, 24, 26 Результат вычитания с — а: 21, 22, 23 Результат вычитания с — b: 11, 12, 13 Результат присваивания -а: -1, -2, -3 Если с = а++ то координаты точки с равны 1, 2, 3 а координаты точки а равны 2, 3, 4 Установка исходных координат точки а: 1, 2, 3 Если с = ++а то координаты точки с равны 2, 3, 4 а координаты точки а равны 2, 3, 4

// Перегрузить бинарный оператор + для сложения объекта // типа ThreeD и целого значения типа int. public static ThreeD operator +(ThreeD op1, int op2)

// Перегрузить бинарный оператор + дважды: // один раз — для сложения объектов класса ThreeD, // а другой раз — для сложения объекта типа ThreeD и целого значения типа int. using System;

// Класс для хранения трехмерных координат. class ThreeD < int х, у, z; // трехмерные координаты public ThreeD() < x = у = z = 0; >public ThreeD(int i, int j, int k) < x = i; у = j; z = k; >// Перегрузить бинарный оператор + для сложения объектов класса ThreeD. public static ThreeD operator + (ThreeD op1, ThreeD op2) < ThreeD result = new ThreeD(); /* Сложить координаты двух точек и возвратить результат. */ result.х = op1.x + ор2.х; result.у = op1.у + ор2.у; result.z = op1.z + op2.z; return result; >// Перегрузить бинарный оператор + для сложения // объекта типа ThreeD и целого значения типа int. public static ThreeD operator +(ThreeD opl, int op2) < ThreeD result = new ThreeD(); result.x = op1.x + op2; result.у = op1.у + op2; result.z = op1.z + op2; return result; >// Вывести координаты X, Y, Z. public void Show() < Console.WriteLine(x + ", " + у + ", " + z); >>

Координаты точки а: 1, 2, 3 Координаты точки b: 10, 10, 10 Результат сложения а + b: 11, 12, 13 Результат сложения b + 10: 20, 20, 20

// Перегрузить бинарный оператор + трижды: // один раз — для сложения объектов класса ThreeD, // второй раз — для сложения объекта типа ThreeD и целого значения типа int, // а третий раз — для сложения целого значения типа int и объекта типа ThreeD. using System;

// Класс для хранения трехмерных координат. class ThreeD < int х, у, z; // трехмерные координаты public ThreeD() < x = y = z = 0; >public ThreeD(int i, int j, int k) < x = i; у = j; z = k; >// Перегрузить бинарный оператор + для сложения объектов класса ThreeD. public static ThreeD operator +(ThreeD op1, ThreeD op2) < ThreeD result = new ThreeD(); /* Сложить координаты двух точек и возвратить результат. */ result.х = op1.x + ор2.х; result.у = op1.y + ор2.у; result.z = op1.z + op2.z; return result; >// Перегрузить бинарный оператор + для сложения // объекта типа ThreeD и целого значения типа int. public static ThreeD operator +(ThreeD op1, int op2) < ThreeD result = new ThreeD(); result.x = op1.x + op2; result.у = op1.y + op2; result.z = op1.z + op2; return result; >// Перегрузить бинарный оператор + для сложения // целого значения типа int и объекта типа ThreeD. public static ThreeD operator +(int op1, ThreeD op2) < ThreeD result = new ThreeD(); result.x = op2.x + op1; result.у = op2.y + op1; result.z = op2.z + op1; return result; >// Вывести координаты X, Y, Z. public void Show() < Console.WriteLine(x + ", " + у + ", " + z); >>

Координаты точки а: 1, 2, 3 Координаты точки b: 10, 10, 10 Результат сложения а + b: 11, 12, 13 Результат сложения b + 10: 20, 20, 20 Результат сложения 15 + b: 25, 25, 25

// Перегрузить операторы < и >. using System;

Координаты точки а: 5, 6, 7 Координаты точки b: 10, 10, 10 Координаты точки с: 1, 2, 3 Координаты точки d: 6, 7, 5 а > с истинно а < b истинно

И еще одно замечание: если перегружаются операторы == и !=, то для это­ го обычно требуется также переопределить методы Object.Equals() и Object. GetHashCode(). Эти методы и способы их переопределения подробнее рассматри­ ваются в главе 11.

Перегрузка операторов true и false

Ключевые слова true и false можно также использовать в качестве унарных опе­ раторов для целей перегрузки. Перегружаемые варианты этих операторов позволяют определить назначение ключевых слов true и false специально для создаваемых клас­ сов. После перегрузки этих ключевых слов в качестве унарных операторов для конкрет­ ного класса появляется возможность использовать объекты этого класса для управления операторами if, while, for и do-while или же в условном выражении ?.

Операторы true и false должны перегружаться попарно, а не раздельно. Ниже приведена общая форма перегрузки этих унарных операторов.

Обратите внимание на то, что и в том и в другом случае возвращается результат типа bool.

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

Выполнение этой программы приводит к следующему результату.

Обратите внимание на то, как объекты класса ThreeD используются для управле­ ния условным оператором if и оператором цикла do-while. Так, в операторах if объект типа ThreeD проверяется с помощью оператора true. Если результат этой проверки оказывается истинным, то оператор if выполняется. А в операторе цикла do-while объект b декрементируется на каждом шаге цикла. Следовательно, цикл повторяется до тех пор, пока проверка объекта b дает истинный результат, т.е. этот объект содержит хотя бы одну ненулевую координату. Если же окажется, что объект b содержит все нулевые координаты, его проверка с помощью оператора true даст ложный результат и цикл завершится.

Перегрузка логических операторов

Как вам должно быть уже известно, в C# предусмотрены следующие логические операторы: &, |, !, && и ||. Из них перегрузке, безусловно, подлежат только опера­ торы &, | и !. Тем не менее, соблюдая определенные правила, можно извлечь также пользу из укороченных логических операторов && и ||. Все эти возможности рассма­ триваются ниже.

Простой способ перегрузки логических операторов

Рассмотрим сначала простейший случай. Если не пользоваться укороченными ло­ гическими операторами, то перегрузку операторов & и | можно выполнять совершен­ но естественным путем, получая в каждом случае результат типа bool. Аналогичный результат, как правило, дает и перегружаемый оператор !.

Ниже приведен пример программы, в которой демонстрируется перегрузка логи­ ческих операторов !, & и | для объектов типа ThreeD. Как и в предыдущем примере, объект типа ThreeD считается истинным, если хотя бы одна из его координат не равна нулю. Если же все три координаты объекта равны нулю, то он считается ложным.

При выполнении этой программы получается следующий результат.

При таком способе перегрузки логических операторов &, | и ! методы каждого из них возвращают результат типа bool. Это необходимо для того, чтобы использовать рассматриваемые операторы обычным образом, т.е. в тех выражениях, где предпола­ гается результат типа bool. Напомним, что для всех встроенных в C# типов данных результатом логической операции должно быть значение типа bool. Поэтому впол­ не разумно предусмотреть возврат значения типа bool и в перегружаемых вариан­ тах этих логических операторов. Но, к сожалению, такой способ перегрузки пригоден лишь в том случае, если не требуются укороченные логические операторы.

Как сделать укороченные логические операторы доступными для применения

Для того чтобы применение укороченных логических операторов && и || стало возможным, необходимо соблюсти следующие четыре правила. Во-первых, в классе должна быть произведена перегрузка логических операторов & и |. Во-вторых, пере­ гружаемые методы операторов & и | должны возвращать значение того же типа, что и у класса, для которого эти операторы перегружаются. В-третьих, каждый параметр должен содержать ссылку на объект того класса, для которого перегружается логиче­ ский оператор. И в-четвертых, для класса должны быть перегружены операторы true и false. Если все эти условия выполняются, то укороченные логические операторы автоматически становятся пригодными для применения.

В приведенном ниже примере программы показано, как правильно реализовать логические операторы & и | в классе ThreeD, чтобы сделать доступными для примене­ ния укороченные логические операторы && и ||.

Выполнение этой программы приводит к следующему результату.

Рассмотрим более подробно, каким образом реализуются логические операторы & и |. Они представлены в следующем фрагменте кода.

Прежде всего обратите внимание на то, что методы обоих перегружаемых логиче­ ских операторов теперь возвращают объект типа ThreeD. И особенно обратите вни­ мание на то, как формируется этот объект. Если логическая операция дает истинный результат, то создается и возвращается истинный объект типа ThreeD, у которого хотя бы одна координата не равна нулю. Если же логическая операция дает ложный резуль­ тат, то соответственно создается и возвращается ложный объект. Таким образом, ре­ зультатом вычисления логического выражения а & b в следующем фрагменте кода:

является объект типа ThreeD, который в данном случае оказывается истинным. А по­ скольку операторы true и false уже определены, то созданный объект типа ThreeD подвергается действию оператора true и в конечном итоге возвращается результат типа bool. В данном случае он равен true, а следовательно, условный оператор if успешно выполняется.

Благодаря тому что все необходимые правила соблюдены, укороченные операторы становятся доступными для применения к объектам ThreeD. Они действуют следующим образом. Первый операнд проверяется с помощью операторного метода operator true (для оператора ||) или же с помощью операторного метода operator false (для опе­ ратора &&). Если удается определить результат данной операции, то соответствующий перегружаемый оператор (& или |) далее не выполняется. В противном случае перегру­ жаемый оператор (& или | соответственно) используется для определения конечного ре­ зультата. Следовательно, когда применяется укороченный логический оператор && или ||, то соответствующий логический оператор & или | вызывается лишь в том случае, если по первому операнду невозможно определить результат вычисления выражения. В ка­ честве примера рассмотрим следующую строку кода из приведенной выше программы.

В этой строке кода сначала применяется оператор true к объекту а. В данном слу­ чае объект а истинен, и поэтому использовать далее операторный метод | нет необхо­ димости. Но если переписать данную строку кода следующим образом:

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

Описанный выше способ применения укороченных логических операторов может показаться, на первый взгляд, несколько запутанным, но если подумать, то в таком применении обнаруживается известный практический смысл. Ведь благодаря пере­ грузке операторов true и false для класса компилятор получает разрешение на применение укороченных логических операторов, не прибегая к явной их перегрузке. Это дает также возможность использовать объекты в условных выражениях. И вообще, логические операторы & и | лучше всего реализовывать полностью, если, конечно, не требуется очень узко направленная их реализация.

Иногда объект определенного класса требуется использовать в выражении, вклю­ чающем в себя данные других типов. В одних случаях для этой цели оказывается пригодной перегрузка одного иди более операторов, а в других случаях — обыкно­ венное преобразование типа класса в целевой тип. Для подобных ситуаций в C# пред­ усмотрена специальная разновидность операторного метода, называемая оператором преобразования. Такой оператор преобразует объект исходного класса в другой тип. Операторы преобразования помогают полностью интегрировать типы классов в среду программирования на С#, разрешая свободно пользоваться классами вместе с другими типами данных, при условии, что определен порядок преобразования в эти типы.

Существуют две формы операторов преобразования: явная и неявная. Ниже они представлены в общем виде:

где целевой_тип обозначает тот тип, в который выполняется преобразование; ис­ ходный_тип — тот тип, который преобразуется; значение — конкретное значение, приобретаемое классом после преобразования. Операторы преобразования возвра­ щают данные, имеющие целевой_тип, причем указывать другие возвращаемые типы данных не разрешается.

Если оператор преобразования указан в неявной форме (implicit), то преобразо­ вание вызывается автоматически, например, в том случае, когда объект используется в выражении вместе со значением целевого типа. Если же оператор преобразования указан в явной форме (explicit), то преобразование вызывается в том случае, когда выполняется приведение типов. Для одних и тех же исходных и целевых типов данных нельзя указывать оператор преобразования одновременно в явной и неявной форме.

Создадим оператор преобразования специально для класса ThreeD, чтобы проде­ монстрировать его применение. Допустим, что требуется преобразовать объект типа ThreeD в целое значение, чтобы затем использовать его в целочисленном выражении. Такое преобразование требуется, в частности, для получения произведения всех трех координат объекта. С этой целью мы воспользуемся следующей неявной формой опе­ ратора преобразования.

Ниже приведен пример программы, демонстрирующей применение этого опера­ тора преобразования.

Вот к какому результату приводит выполнение этой программы.

Как следует из приведенного выше примера программы, когда объект типа ThreeD используется в таком целочисленном выражении, как i = а, происходит его преоб­ разование. В этом конкретном случае преобразование приводит к возврату целого зна­ чения 6, которое является произведением координат точки а, хранящихся в объекте того же названия. Но если для вычисления выражения преобразование в тип int не требуется, то оператор преобразования не вызывается. Именно поэтому операторный метод operator int() не вызывается при вычислении выражения с = а + b.

Но для различных целей можно создать разные операторы преобразования. Так, для преобразования объекта типа ThreeD в тип double можно было бы определить второй оператор преобразования. При этом каждый вид преобразования выполнялся бы автоматически и независимо от другого.

Оператор неявного преобразования применяется автоматически в следующих слу­ чаях: когда в выражении требуется преобразование типов; методу передается объект; осуществляется присваивание и производится явное приведение к целевому типу. С другой стороны, можно создать оператор явного преобразования, вызываемый толь­ ко тогда, когда производится явное приведение типов. В таком случае оператор явного преобразования не вызывается автоматически. В качестве примера ниже приведен ва­ риант предыдущей программы, переделанный для демонстрации явного преобразо­ вания в тип int.

Оператор преобразования теперь указан в явной форме, и поэтому преобразова­ ние должно быть явно приведено к типу int. Например, следующая строка кода не будет скомпилирована, если исключить приведение типов.

На операторы преобразования накладывается ряд следующих ограничений.

  • Исходный или целевой тип преобразования должен относиться к классу, для которого объявлено данное преобразование. В частности, нельзя переопределить преобразование в тип int, если оно первоначально указано как преобразование в тип double.
  • Нельзя указывать преобразование в класс object или же из этого класса.
  • Для одних и тех же исходных и целевых типов данных нельзя указывать одновременно явное и неявное преобразование.
  • Нельзя указывать преобразование базового класса в производный класс. (Подробнее о базовых и производных классах речь пойдет в главе 11.)
  • Нельзя указывать преобразование в интерфейс или же из него. (Подробнее об интерфейсах — в главе 12.)

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

Рекомендации и ограничения по перегрузке операторов

Действие перегружаемого оператора распространяется на класс, для которого он определяется, и никак не связано с его первоначальным применением к данным встро­ енных в C# типов. Но ради сохранения ясности структуры и удобочитаемости исходно­ го кода перегружаемый оператор должен, по возможности, отражать основную суть своего первоначального назначения. Например, назначение оператора + для класса ThreeD по сути не должно заметно отличаться от его назначения для целочисленных типов данных. Если бы, например, определить оператор + относительно некоторого класса таким образом, чтобы по своему действию он стал больше похожим на опера­ тор /, то вряд ли от этого было бы много проку. Главный принцип перегрузки опера­ торов заключается в следующем: несмотря на то, что перегружаемый оператор может получить любое назначение, ради ясности новое его назначение должно быть так или иначе связано с его первоначальным назначением.

На перегрузку операторов накладывается ряд ограничений. В частности, нельзя из­ менять приоритет любого оператора или количество операндов, которое требуется для оператора, хотя в операторном методе можно и проигнорировать операнд. Кроме того, имеется ряд операторов, которые нельзя перегружать. А самое главное, что пере­ грузке не подлежит ни один из операторов присваивания, в том числе и составные, как, например, оператор +=. Ниже перечислены операторы, которые нельзя перегру­ жать. Среди них имеются и такие операторы, которые будут рассматриваться далее в этой книге.

&& () . ?
?? [] || =
=> -> as checked
default is new sizeof
typeof unchecked

Несмотря на то что оператор приведения () нельзя перегружать явным образом, имеется все же возможность создать упоминавшиеся ранее операторы преобразова­ ния, выполняющие ту же самую функцию.

Ограничение, связанное с тем, что некоторые операторы, например +=, нельзя перегружать, на самом деле не является таким уж непреодолимым. Вообще говоря, если оператор определен как перегружаемый и используется в составном операторе присваивания, то обычно вызывается метод этого перегружаемого оператора. Следо­ вательно, при обращении к оператору += в программе автоматически вызывается за­ ранее объявленный вариант метода operator+(). Например, в приведенном ниже фрагменте кода метод operator+() автоматически вызывается для класса ThreeD, а в итоге объект b будет содержать координаты 11, 12, 13.

И последнее замечание: несмотря на то, что оператор индексации массива [] нель­ зя перегружать с помощью операторного метода, имеется возможность создать индек­ саторы, о которых речь пойдет в следующей главе.

Еще один пример перегрузки операторов

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

В данном примере разрабатывается 4-разрядный целочисленный тип данных и для него определяется ряд операций. Вам, вероятно, известно, что на ранней стадии развития вычислительной техники широко применялся тип данных для обозначения 4-разрядных двоичных величин, называвшихся полубайтами, поскольку они составля­ ли половину байта, содержали одну шестнадцатеричную цифру и были удобны для ввода кода полубайтами с пульта ЭВМ, что в те времена считалось привычным заня­ тием для программистов! В наше время этот тип данных применяется редко, но он по-прежнему является любопытным дополнением целочисленных типов данных в С#. По традиции полубайт обозначает целое значение без знака.

В приведенном ниже примере программы тип полубайтовых данных реализуется с помощью класса Nybble. В качестве базового для него используется тип int, но с огра­ ничением на хранение данных от 0 до 15. В классе Nybble определяются следующие операторы.

  • Сложение двух объектов типа Nybble.
  • Сложение значения типа int с объектом типа Nybble.
  • Сложение объекта типа Nybble со значением типа int.
  • Операции сравнения: больше (>) и меньше (<).
  • Операция инкремента.
  • Преобразование значения типа int в объект типа Nybble.
  • Преобразование объекта типа Nybble в значение типа int.

Перечисленных выше операций достаточно, чтобы показать, каким образом тип класса Nybble интегрируется в систему типов С#. Но для полноценной реализации этого типа данных придется определить все остальные доступные для него операции. Попробуйте сделать это сами в качестве упражнения.

Ниже полностью приводится класс Nybble, а также класс NybbleDemo, демонстри­ рующий его применение.

При выполнении этой программы получается следующий результат.

Большая часть функций класса Nybble не требует особых пояснений. Тем не менее необходимо подчеркнуть ту особую роль, которую операторы преобразования игра­ ют в интегрировании класса типа Nybble в систему типов С#. В частности, объект типа Nybble можно свободно комбинировать с данными других типов в арифметических вы­ ражениях, поскольку определены преобразования объекта этого типа в тип int и обрат­ но. Рассмотрим для примера следующую строку кода из приведенной выше программы.

В этом выражении переменная t и значения 2 и 3 относятся к типу int, но в ней присутствует также объект типа Nybble. Оба типа оказываются совместимыми благо­ даря неявному преобразованию типа Nybble в тип int. В данном случае остальная часть выражения относится к типу int, поэтому объект а преобразуется в тип int с помощью своего метода преобразования.

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

сначала выполняется оператор преобразования типа int в тип Nybble. Затем созда­ ется новый объект типа Nybble, в котором сохраняются 4 младших разряда целого значения 19, а по существу, число 3, поскольку значение 19 превышает диапазон пред­ ставления чисел для типа Nybble. Далее этот объект присваивается переменной эк­ земпляра а. Без операторов преобразования подобные выражения были бы просто недопустимы.

Кроме того, преобразование типа Nybble в тип Nybble используется в цикле for. Без такого преобразования организовать столь простой цикл for было бы просто не­ возможно.

ПРИМЕЧАНИЕ В качестве упражнения попробуйте создать вариант полубайтового типа Nybble, предотвращающий переполнение, если присваиваемое значение оказывается за пределами допустимого диапазона чисел. Для этой цели лучше всего сгенерировать исключение. (Подробнее об исключениях — в главе 13.)

Основы перегрузки операторов

В C#, подобно любому языку программирования, имеется готовый набор лексем, используемых для выполнения базовых операций над встроенными типами. Например, известно, что операция + может применяться к двум целым, чтобы дать их сумму:

Здесь нет ничего нового, но задумывались ли вы когда-нибудь о том, что одна и та же операция + может применяться к большинству встроенных типов данных C#? Например, рассмотрим такой код:

По сути, функциональность операции + уникальным образом базируются на представленных типах данных (строках или целых в данном случае). Когда операция + применяется к числовым типам, мы получаем арифметическую сумму операндов. Однако когда та же операция применяется к строковым типам, получается конкатенация строк.

Язык C# предоставляет возможность строить специальные классы и структуры, которые также уникально реагируют на один и тот же набор базовых лексем (вроде операции +). Имейте в виду, что абсолютно каждую встроенную операцию C# перегружать нельзя. В следующей таблице описаны возможности перегрузки основных операций:

Операция C# Возможность перегрузки
+, -, !, ++, —, true, false Этот набор унарных операций может быть перегружен
+, -, *, /, %, &, |, ^, > Эти бинарные операции могут быть перегружены
==, !=, <, &gt, <=, >= Эти операции сравнения могут быть перегружены. C# требует совместной перегрузки «подобных» операций (т.е. < и >, <= и >=, == и !=)
[] Операция [] не может быть перегружена. Oднако, аналогичную функциональность предлагают индексаторы
() Операция () не может быть перегружена. Однако ту же функциональность предоставляют специальные методы преобразования
+=, -=, *=, /=, %=, &=, |=, ^=, >= Сокращенные операции присваивания не могут перегружаться; однако вы получаете их автоматически, перегружая соответствующую бинарную операцию

Перегрузка операторов тесно связана с перегрузкой методов. Для перегрузки оператора служит ключевое слово operator, определяющее операторный метод, который, в свою очередь, определяет действие оператора относительно своего класса. Существуют две формы операторных методов (operator): одна — для унарных операторов, другая — для бинарных. Ниже приведена общая форма для каждой разновидности этих методов:

Здесь вместо op подставляется перегружаемый оператор, например + или /, а возвращаемый_тип обозначает конкретный тип значения, возвращаемого указанной операцией. Это значение может быть любого типа, но зачастую оно указывается такого же типа, как и у класса, для которого перегружается оператор. Такая корреляция упрощает применение перегружаемых операторов в выражениях. Для унарных операторов операнд обозначает передаваемый операнд, а для бинарных операторов то же самое обозначают операнд1 и операнд2. Обратите внимание на то, что операторные методы должны иметь оба спецификатора типа — public и static.

Перегрузка бинарных операторов

Давайте рассмотрим применение перегрузки бинарных операторов на простейшем примере:

Читать:
Как удалить exchange 2019

Related Posts