Skip to content

Latest commit

 

History

History
1611 lines (1490 loc) · 96.8 KB

c9.md

File metadata and controls

1611 lines (1490 loc) · 96.8 KB

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

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

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

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

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

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

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

// Общая форма перегрузки унарного оператора.
public static возвращаемый_тип operator op(тип_параметра операнд)
{
    // операции
}
// Общая форма перегрузки бинарного оператора.
public static возвращаемый_тип operator op(тип_параметра1 операнд1,
тип_параметра1 операнд2)
{
    // операции
}

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

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

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

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

// Пример перегрузки бинарных операторов.
using System;

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

class ThreeDDemo {
    static void Main() {
        ThreeD a = new ThreeD(1, 2, 3);
        ThreeD b = new ThreeD(10, 10, 10);
        ThreeD c;
        Console.Write("Координаты точки a: ");
        a.Show();
        Console.WriteLine();

        Console.Write("Координаты точки b: ");
        b.Show();
        Console.WriteLine();

        с = а + b; // сложить координаты точек а и b
        Console.Write("Результат сложения а + b: ");
        с.Show();
        Console.WriteLine();

        с = а + b + с; // сложить координаты точек а, b и с
        Console.Write("Результат сложения а + b + с: ");
        с.Show();
        Console.WriteLine();

        с = с - а; // вычесть координаты точки а
        Console.Write("Результат вычитания с - а: ");
        с.Show();
        Console.WriteLine();

        с = с - b; // вычесть координаты точки b
        Console.Write("Результат вычитания с - b: ");
        с.Show();
        Console.WriteLine();
    }
}

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

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

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

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

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

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

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

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

// Перегрузить оператор унарного минуса.
public static ThreeD operator - (ThreeD op)
{
    ThreeD result = new ThreeD ();
    result.x = -op.x;
    result.у = -op.у;
    result.z = -op.z;
    return result;
}

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

а = -b

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

// Перегрузить унарный оператор ++.
public static ThreeD operator ++(ThreeD op)
{
    ThreeD result = new ThreeD();
    // Возвратить результат инкрементирования.
    result.x = op.x + 1;
    result.у = op.у + 1;
    result.z = op.z + 1;
    return result;
}

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

// Пример перегрузки бинарных и унарных операторов.
using System;

// Класс для хранения трехмерных координат.
class ThreeD {
    int х, у, z; // трехмерные координаты
    public ThreeD() { х = у = z = 0; }
    public ThreeD(int i, int j, int k) { x = i; у = j; z = k; }
    // Перегрузить бинарный оператор +.
    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;
    }
    // Перегрузить бинарный оператор -.
    public static ThreeD operator -(ThreeD op1, ThreeD op2)
    {
        ThreeD result = new ThreeD();
        /* Обратить внимание на порядок следования операндов:
        op1 — левый операнд, ор2 — правый операнд. */
        result.х = op1.x - ор2.х;
        result.у = op1.y - ор2.у;
        result.z = op1.z - op2.z;
        return result;
    }
    // Перегрузить унарный оператор -.
    public static ThreeD operator -(ThreeD op)
    {
        ThreeD result = new ThreeD();
        result.x = -op.x;
        result.у = -op.y;
        result.z = -op.z;
        return result;
    }
    // Перегрузить унарный оператор ++.
    public static ThreeD operator ++(ThreeD op)
    {
        ThreeD result = new ThreeD();
        // Возвратить результат инкрементирования.
        result.x = op.x + 1;
        result.у = op.y + 1;
        result.z = op.z + 1;
        return result;
    }
    // Вывести координаты X, Y, Z.
    public void Show()
    {
        Console.WriteLine(x + ", " + у + ", " + z);
    }
}

class ThreeDDemo {
    static void Main() {
        ThreeD a = new ThreeD(1, 2, 3);
        ThreeD b = new ThreeD(10, 10, 10);
        ThreeD с = new ThreeD();
        Console.Write("Координаты точки a: ");
        a.Show();
        Console.WriteLine();

        Console.Write("Координаты точки b: ");
        b.Show();
        Console.WriteLine();

        с = a + b; // сложить координаты точек а и b
        Console.Write("Результат сложения a + b: ");
        c.Show();
        Console.WriteLine();

        c = a + b + c; // сложить координаты точек a, b и с
        Console.Write("Результат сложения a + b + с: ");
        с.Show();
        Console.WriteLine();

        с = с - а; // вычесть координаты точки а
        Console.Write("Результат вычитания с - а: ");
        с.Show();
        Console.WriteLine();

        с = с - b; // вычесть координаты точки b
        Console.Write("Результат вычитания с - b: ");
        с.Show();
        Console.WriteLine();

        с = -a; // присвоить точке с отрицательные координаты точки а
        Console.Write("Результат присваивания -а: ");
        с.Show();
        Console.WriteLine();

        с = а++; // присвоить точке с координаты точки а,
        // а затем инкрементировать их
        Console.WriteLine("Если с = а++");
        Console.Write("то координаты точки с равны ");
        с.Show();
        Console.Write("а координаты точки а равны ");
        а.Show();
        // Установить исходные координаты (1,2,3) точки а
        а = new ThreeD(1, 2, 3);
        Console.Write("\nУстановка исходных координат точки а: ");
        а.Show();
        с = ++а; // инкрементировать координаты точки а,
        // а затем присвоить их точке с
        Console.WriteLine("\nЕсли с = ++а");
        Console.Write("то координаты точки с равны ");
        с.Show();
        Console.Write("а координаты точки а равны ");
        а.Show();
    }
}
Вот к какому результату приводит выполнение данной программы.

Координаты точки а: 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


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

// Перегрузить бинарный оператор + для сложения объекта // типа 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. Такая операция вполне допустима, потому что, как пояснялось выше, при
перегрузке бинарного оператора один из его операндов должен быть того же типа,
что и класс, для которого этот оператор перегружается. Но у второго операнда этого
оператора может быть любой другой тип.

Ниже приведен вариант класса ThreeD с двумя перегружаемыми методами опе­
ратора +.

// Перегрузить бинарный оператор + дважды: // один раз - для сложения объектов класса 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); } }

class ThreeDDemo { static void Main() { ThreeD a = new ThreeD(1, 2, 3); ThreeD b = new ThreeD(10, 10, 10); ThreeD с = new ThreeD(); Console.Write("Координаты точки a; "); a.Show(); Console.WriteLine();

    Console.Write("Координаты точки b: ");
    b.Show();
    Console.WriteLine();

    с = a + b; // сложить объекты класса ThreeD
    Console.Write("Результат сложения a + b: ");
    c.Show();
    Console.WriteLine();

    c = b + 10; // сложить объект типа ThreeD и целое значение типа int
    Console.Write("Результат сложения b + 10: ");
    с.Show();
}

}

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

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

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

Продемонстрированная выше перегрузка оператора +, безусловно, расширяет по­
лезные функции класса ThreeD, тем не менее, она делает это не до конца. И вот по­
чему. Метод operator+(ThreeD, int) позволяет выполнять операции, подобные
следующей.

ob1 = оb2 + 10;

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

ob1 = 10 + оb2;

Дело в том, что второй целочисленный аргумент данного метода обозначает правый
операнд бинарного оператора +, но в приведенной выше строке кода целочисленный
аргумент указывается слева. Для того чтобы разрешить выполнение такой операции
сложения, придется перегрузить оператор + еще раз. В этом случае первый параметр
операторного метода должен иметь тип int, а второй параметр — тип ThreeD. Таким
образом, в одном варианте метода operator+() выполняется сложение объекта типа
ThreeD и целого значения, а во втором — сложение целого значения и объекта типа
ThreeD. Благодаря такой перегрузке оператора + (или любого другого бинарного опе­
ратора) допускается появление встроенного типа данных как с левой, так и с правой
стороны данного оператора. Ниже приведен еще один вариант класса ThreeD, в кото­
ром бинарный оператор + перегружается описанным выше образом.

// Перегрузить бинарный оператор + трижды: // один раз — для сложения объектов класса 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); } }

class ThreeDDemo { static void Main() { ThreeD a = new ThreeD(l, 2, 3); ThreeD b = new ThreeD(10, 10, 10); ThreeD с = new ThreeD(); Console.Write("Координаты точки a: "); a.Show(); Console.WriteLine();

    Console.Write("Координаты точки b: ");
    b.Show();
    Console.WriteLine();

    с = a + b; // сложить объекты класса ThreeD
    Console.Write("Результат сложения a + b: ");
    c.Show();
    Console.WriteLine();

    c = b + 10; // сложить объект типа ThreeD и целое значение типа int
    Console.Write("Результат сложения b + 10: ");
    с.Show();
    Console.WriteLine();

    с = 15 + b; // сложить целое значение типа int и объект типа ThreeD
    Console.Write("Результат сложения 15 + b: ");
    с.Show();
}

}

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

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


### Перегрузка операторов отношения
Операторы отношения, например == и <, могут также перегружаться, причем
очень просто. Как правило, перегруженный оператор отношения возвращает ло­
гическое значение true и false. Это вполне соответствует правилам обычного
применения подобных операторов и дает возможность использовать их перегру­
жаемые разновидности в условных выражениях. Если же возвращается результат
другого типа, то тем самым сильно ограничивается применимость операторов от­
ношения.

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

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

// Класс для хранения трехмерных координат. class ThreeD { int х, у, z; // трехмерные координаты public ThreeD() { х = у = z = 0; } public ThreeD(int i, int j, int k) { x = i; у = j; z = k; } // Перегрузить оператор <. public static bool operator <(ThreeD op1, ThreeD op2) { if(Math.Sqrt(op1.x * op1.x + op1.y * op1.y + op1.z * op1.z) < Math.Sqrt(op2.x * op2.x + op2.у * op2.y + op2.z * op2.z)) return true; else return false; } // Перегрузить оператор >. public static bool operator >(ThreeD op1, ThreeD op2) { if(Math.Sqrt(op1.x * op1.x + op1.y * op1.y + op1.z * op1.z) > Math.Sqrt(op2.x * op2.x + op2.у * op2.у + op2.z * op2.z)) return true; else return false; } // Вывести координаты X, Y, Z. public void Show() { Console.WriteLine(x + ", " + у + ", " + z); } }

class ThreeDDemo { static void Main() { ThreeD a = new ThreeD(5, 6, 7); ThreeD b = new ThreeD(10, 10, 10); ThreeD с = new ThreeD(1, 2, 3); ThreeD d = new ThreeD(6, 7, 5); Console.Write("Координаты точки a: "); a.Show(); Console.Write("Координаты точки b: "); b.Show(); Console.Write("Координаты точки с: "); c.Show(); Console.Write("Координаты точки d: "); d.Show(); Console.WriteLine();

    if(а > с) Console.WriteLine("а > с истинно");
    if(а < с) Console.WriteLine("а < с истинно");
    if(а > b) Console.WriteLine("а > b истинно");
    if(а < b) Console.WriteLine("а < b истинно");
    if(а > d) Console.WriteLine("а > d истинно");
    else if(а < d) Console.WriteLine("a < d истинно");
    else Console.WriteLine("Точки a и d находятся на одном расстоянии " +
    "от начала отсчета");
}

}

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

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

Точки a и d находятся на одном расстоянии от начала отсчета
На перегрузку операторов отношения накладывается следующее важное ограни­
чение: они должны перегружаться попарно. Так, если перегружается оператор <, то
следует перегрузить и оператор >, и наоборот. Ниже приведены составленные в пары
перегружаемые операторы отношения.
== !=
< >
<= >=

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

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

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

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

public static bool operator true(тип_параметра операнд)
{
    // Возврат логического значения true или false.
}
public static bool operator false(тип_параметра операнд)
{
    // Возврат логического значения true или false.
}

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

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

// Перегрузить операторы true и false для класса ThreeD.
using System;

// Класс для хранения трехмерных координат.
class ThreeD {
    int х, у, z; // трехмерные координаты
    public ThreeD() { х = у = z = 0; }
    public ThreeD(int i, int j, int k) { x = i; у = j; z = k; }
    // Перегрузить оператор true.
    public static bool operator true(ThreeD op) {
        if((op.x != 0) || (op.у != 0) || (op.z != 0))
            return true; // хотя бы одна координата не равна нулю
        else
            return false;
    }
    // Перегрузить оператор false.
    public static bool operator false(ThreeD op) {
        if((op.x == 0) && (op.у == 0) && (op.z == 0))
            return true; // все координаты равны нулю
        else
            return false;
    }
    // Перегрузить унарный оператор --.
    public static ThreeD operator --(ThreeD op)
    {
        ThreeD result = new ThreeD();
        // Возвратить результат декрементирования.
        result.x = op.x - 1;
        result.у = op.у - 1;
        result.z = op.z - 1;
        return result;
    }
    // Вывести координаты X, Y, Z.
    public void Show()
    {
        Console.WriteLine(x + ", " + у + ", " + z);
    }
}

class TrueFalseDemo {
    static void Main() {
        ThreeD a = new ThreeD(5, 6, 7);
        ThreeD b = new ThreeD(10, 10, 10);
        ThreeD с = new ThreeD (0, 0, 0);
        Console.Write("Координаты точки a: ");
        a.Show();
        Console.Write("Координаты точки b: ");
        b.Show();
        Console.Write("Координаты точки с: ");
        c.Show();
        Console.WriteLine();

        if(a) Console.WriteLine("Точка а истинна.");
        else Console.WriteLine("Точка а ложна.");
        if(b) Console.WriteLine("Точка b истинна.");
        else Console.WriteLine("Точка b ложна.");
        if(с) Console.WriteLine("Точка с истинна.");
        else Console.WriteLine("Точка с ложна.");
        Console.WriteLine();

        Console.WriteLine("Управление циклом с помощью объекта класса
        ThreeD.");
        do {
            b.Show();
            b--;
        } while(b);
    }
}

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

Координаты точки а: 5, 6, 7
Координаты точки b: 10, 10, 10
Координаты точки с: 0, 0, 0

Точка а истинна
Точка b истинна
Точка с ложна

Управление циклом с помощью объекта класса ThreeD.
10, 10, 10
9, 9, 9
8, 8, 8
7, 7, 7
6, 6, 6
5, 5, 5
4, 4, 4
3, 3, 3
2, 2, 2
1, 1, 1

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

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

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

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

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

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

// Простой способ перегрузки логических операторов
// !, | и & для объектов класса ThreeD.
using System;

// Класс для хранения трехмерных координат.
class ThreeD {
    int х, у, z; // трехмерные координаты
    public ThreeD() { х = у = z = 0; }
    public ThreeD(int i, int j, int k) { x = i; у = j; z = k; }
    // Перегрузить логический оператор |.
    public static bool operator |(ThreeD op1, ThreeD op2)
    {
        if( ((op1.x != 0) || (op1.у != 0) || (op1.z != 0)) |
        ((op2.x != 0) || (op2.у != 0) || (op2.z != 0)) )
            return true;
        else
            return false;
    }
    // Перегрузить логический оператор &.
    public static bool operator &(ThreeD op1, ThreeD op2)
    {
        if( ((op1.x != 0) && (op1.у != 0) && (op1.z != 0)) &
        ((op2.x != 0) && (op2.y != 0) && (op2.z != 0)) )
            return true;
        else
            return false;
    }
    // Перегрузить логический оператор !.
    public static bool operator !(ThreeD op)
    {
        if((op.x != 0) || (op.y != 0) || (op.z != 0))
        return false;
        else return true;
    }
    // Вывести координаты X, Y, Z.
    public void Show()
    {
        Console.WriteLine(x + ", " + у + ", " + z);
    }
}

class TrueFalseDemo {
    static void Main() {
        ThreeD a = new ThreeD(5, 6, 7);
        ThreeD b = new ThreeD(10, 10, 10);
        ThreeD с = new ThreeD(0, 0, 0);
        Console.Write("Координаты точки a: ");
        a.Show();
        Console.Write("Координаты точки b: ");
        b.Show();
        Console.Write("Координаты точки с: ");
        c.Show();
        Console.WriteLine();

        if(!a) Console.WriteLine("Точка а ложна.");
        if(!b) Console.WriteLine("Точка b ложна.");
        if(!c) Console.WriteLine("Точка с ложна.");
        Console.WriteLine();

        if(a & b) Console.WriteLine("a & b истинно.");
        else Console.WriteLine("a & b ложно.");
        if(a & c) Console.WriteLine("a & с истинно.");
        else Console.WriteLine("a & с ложно.");
        if(a | b) Console.WriteLine("a | b истинно.");
        else Console.WriteLine("a | b ложно.");
        if(a | c) Console.WriteLine("a | с истинно.");
        else Console.WriteLine("a | с ложно.");
    }
}

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

Координаты точки а: 5, 6, 7
Координаты точки b: 10, 10, 10
Координаты точки с: 0, 0, 0

Точка с ложна.

а & b истинно.
a & с ложно.
а | b истинно.
а | с истинно.

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

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

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

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

/* Более совершенный способ перегрузки логических
операторов !, | и & для объектов класса ThreeD.
В этом варианте укороченные логические операторы && и ||
становятся доступными для применения автоматически. */
using System;

// Класс для хранения трехмерных координат.
class ThreeD {
    int х, у, z; // трехмерные координаты
    public ThreeD() { х = у = z = 0; }
    public ThreeD(int i, int j, int k) { x = i; у = j; z = k; }
    // Перегрузить логический оператор | для укороченного вычисления.
    public static ThreeD operator |(ThreeD op1, ThreeD op2)
    {
        if( ((op1.x != 0) || (op1.y != 0) || (op1.z != 0)) |
        ((op2.x != 0) || (op2.у != 0) || (op2.z != 0)) )
            return new ThreeD(1, 1, 1);
        else
            return new ThreeD(0, 0, 0);
    }
    // Перегрузить логический оператор & для укороченного вычисления.
    public static ThreeD operator &(ThreeD op1, ThreeD op2)
    {
        if( ((op1.x != 0) && (op1.y != 0) && (op1.z != 0)) &
        ((op2.x != 0) && (op2.y != 0) && (op2.z != 0)) )
            return new ThreeD(1, 1, 1);
        else
            return new ThreeD(0, 0, 0);
    }
    // Перегрузить логический оператор !.
    public static bool operator !(ThreeD op)
    {
        if(op) return false;
        else return true;
    }
    // Перегрузить оператор true.
    public static bool operator true(ThreeD op) {
        if((op.x != 0) || (op.y != 0) || (op.z != 0))
            return true; // хотя бы одна координата не равна нулю
        else
            return false;
    }
    // Перегрузить оператор false.
    public static bool operator false(ThreeD op) {
        if((op.x == 0) && (op.y == 0) && (op.z == 0))
            return true; // все координаты равны нулю
        else
            return false;
    }
    // Ввести координаты X, Y, Z.
    public void Show()
    {
        Console.WriteLine(x + ", " + у + ", " + z);
    }
}

class TrueFalseDemo {
    static void Main() {
        ThreeD a = new ThreeD(5, 6, 7);
        ThreeD b = new ThreeD(10, 10, 10);
        ThreeD с = new ThreeD(0, 0, 0);
        Console.Write("Координаты точки a: ");
        a.Show();
        Console.Write("Координаты точки b: ");
        b.Show();
        Console.Write("Координаты точки с: ");
        c.Show();
        Console.WriteLine();

        if(a) Console.WriteLine("Точка а истинна.");
        if(b) Console.WriteLine("Точка b истинна.");
        if(с) Console.WriteLine("Точка с истинна.");
        if(!a) Console.WriteLine("Точка а ложна.");
        if(!b) Console.WriteLine("Точка b ложна.");
        if(!с) Console.WriteLine("Точка с ложна.");
        Console.WriteLine();

        Console.WriteLine("Применение логических операторов & и |");
        if(a & b) Console.WriteLine("а & b истинно.");
        else Console.WriteLine("а & b ложно.");
        if(а & с) Console.WriteLine("а & с истинно.");
        else Console.WriteLine("а & с ложно.");
        if(а | b) Console.WriteLine("a | b истинно.");
        else Console.WriteLine("а | b ложно.");
        if(а | с) Console.WriteLine("а | с истинно.");
        else Console.WriteLine("а | с ложно.");
        Console.WriteLine();

        // А теперь применить укороченные логические операторы.
        Console.WriteLine("Применение укороченных" +
                        "логических операторов && и ||");
        if(a && b) Console.WriteLine("a && b истинно.");
        else Console.WriteLine("а && b ложно.");
        if(а && с) Console.WriteLine("а && с истинно.");
        else Console.WriteLine("a && с ложно.");
        if(а || b) Console.WriteLine("a || b истинно.");
        else Console.WriteLine("a || b ложно.");
        if(a || c) Console.WriteLine("a || с истинно.");
        else Console.WriteLine("a || с ложно.");
    }
}

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

Координаты точки а: 5, 6, 7
Координаты точки b: 10, 10, 10
Координаты точки с: 0, 0, 0

Точка а истинна
Точка b истинна
Точка с ложна.

Применение логических операторов & и |
а & b истинно.
a & с ложно.
а | b истинно.
а | с истинно.

Применение укороченных логических операторов && и ||
a && b истинно.
a && с ложно.
а || b истинно.
а || с истинно.

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

// Перегрузить логический оператор | для укороченного вычисления.
public static ThreeD operator |(ThreeD op1, ThreeD op2)
{
    if( ((op1.x != 0) || (op1.у != 0) || (op1.z != 0)) |
    ((op2.x != 0) || (op2.у != 0) || (op2.z != 0)) )
        return new ThreeD(1, 1, 1);
    else
        return new ThreeD(0, 0, 0);
}
// Перегрузить логический оператор S для укороченного вычисления.
public static ThreeD operator &(ThreeD op1, ThreeD op2)
{
    if( ((op1.x != 0) && (op1.у != 0) && (op1.z != 0)) &
    ((op2.x != 0) && (op2.y != 0) && (op2.z != 0)) )
        return new ThreeD(1, 1, 1);
    else
        return new ThreeD(0, 0, 0);
}

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

if (а & b) Console.WriteLine("а & b истинно.");
else Console.WriteLine("а & b ложно.");

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

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

if(а || с) Console.WriteLine("а || с истинно.");

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

if(с || a) Console.WriteLine("с || а истинно.");

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

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

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

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

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

public static explicit operator целевой_тип(исходный_тип v) {return значение;}
public static implicit operator целевой_тип(исходный_тип v) {return значение;}

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

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

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

public static implicit operator int(ThreeD op1)
{
    return op1.x * op1.y * op1.z;
}

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

// Пример применения оператора неявного преобразования.
using System;

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

class ThreeDDemo {
    static void Main() {
        ThreeD a = new ThreeD(1, 2, 3);
        ThreeD b = new ThreeD(10, 10, 10);
        ThreeD с = new ThreeD();
        int i;
        Console.Write("Координаты точки a: ");
        a.Show();
        Console.WriteLine();

        Console.Write("Координаты точки b: ");
        b.Show();
        Console.WriteLine();

        с = a + b; // сложить координаты точек а и b
        Console.Write("Результат сложения a + b: ");
        c.Show();
        Console.WriteLine();

        i = a; // преобразовать в тип int
        Console.WriteLine("Результат присваивания i = a: " + i);
        Console.WriteLine();

        i = a * 2 - b; // преобразовать в тип int
        Console.WriteLine("Результат вычисления выражения a * 2 - b: " + i
    }
}

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

Координаты точки а: 1, 2, 3

Координаты точки b: 10, 10, 10

Результат сложения а + b: 11, 12, 13

Результат присваивания i = а: 6

Результат вычисления выражения а * 2 - b: -988

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

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

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

// Применить явное преобразование.
using System;

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

class ThreeDDemo {
    static void Main() {
        ThreeD a = new ThreeD(1, 2, 3);
        ThreeD b = new ThreeD(10, 10, 10);
        ThreeD с = new ThreeD();
        int i;
        Console.Write("Координаты точки a: ");
        a.Show();
        Console.WriteLine();

        Console.Write("Координаты точки b: ");
        b.Show();
        Console.WriteLine();

        с = a + b; // сложить координаты точек а и b
        Console.Write("Результат сложения a + b: ");
        c.Show();
        Console.WriteLine();

        i = (int) a; // преобразовать в тип int явно,
        // поскольку указано приведение типов
        Console.WriteLine("Результат присваивания i = а: " + i);
        Console.WriteLine();

        i = (int)a * 2 - (int)b; // явно требуется приведение типов
        Console.WriteLine("Результат вычисления выражения а * 2 - b: " + i);
    }
}

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

i = (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 а = new ThreeD(l, 2, 3);
ThreeD b = new ThreeD(10, 10, 10);
b += a; // сложить координаты точек а и b

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

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

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

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

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

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

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

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

// Создать полубайтовый тип 4-разрядных данных под названием Nybble.
using System;

// тип4-разрядных данных.
class Nybble {
    int val; // базовый тип для хранения данных
    public Nybble() { val = 0; }
    public Nybble(int i) {
        val = i;
        val = val & 0xF; // сохранить 4 младших разряда
    }
    // Перегрузить бинарный оператор + для сложения двух объектов типа Nybble.
    public static Nybble operator +(Nybble op1, Nybble op2)
    {
        Nybble result = new Nybble();
        result.val = op1.val + op2.val;
        result.val = result.val & 0xF; // сохранить 4 младших разряда
        return result;
    }
    // Перегрузить бинарный оператор + для сложения
    // объекта типа Nybble и значения типа int.
    public static Nybble operator + (Nybble op1, int op2)
    {
        Nybble result = new Nybble();
        result.val = op1.val + op2;
        result.val = result.val & 0xF; // сохранить 4 младших разряда
        return result;
    }
    // Перегрузить бинарный оператор + для сложения
    // значения типа int и объекта типа Nybble.
    public static Nybble operator +(int op1, Nybble op2)
    {
        Nybble result = new Nybble();
        result.val = op1 + op2.val;
        result.val = result.val & 0xF; // сохранить 4 младших разряда
        return result;
    }
    // Перегрузить оператор ++.
    public static Nybble operator ++(Nybble op)
    {
        Nybble result = new Nybble();
        result.val = op.val + 1;
        result.val = result.val & 0xF; // сохранить 4 младших разряда
        return result;
    }
    // Перегрузить оператор >.
    public static bool operator >(Nybble op1, Nybble op2)
    {
        if(op1.val > op2.val) return true;
        else return false;
    }
    // Перегрузить оператор <.
    public static bool operator <(Nybble op1, Nybble op2)
    {
        if(op1.val < op2.val) return true;
        else return false;
    }
    // Преобразовать тип Nybble в тип int.
    public static implicit operator int (Nybble op)
    {
        return op.val;
    }
    // Преобразовать тип int в тип Nybble.
    public static implicit operator Nybble (int op)
    {
        return new Nybble(op);
    }
}

class NybbleDemo {
    static void Main() {
        Nybble a = new Nybble(1);
        Nybble b = new Nybble(10);
        Nybble с = new Nybble();
        int t;
        Console.WriteLine("a: " + (int) a);
        Console.WriteLine("b: " + (int) b);
        // Использовать тип Nybble в условном операторе if.
        if(а < b) Console.WriteLine("а меньше b\n");
        // Сложить два объекта типа Nybble.
        с = а + b;
        Console.WriteLine("с после операции с = а + b: " + (int) с);
        // Сложить значение типа int с объектом типа Nybble.
        а += 5;
        Console.WriteLine("а после операции а += 5: " + (int) а);
        Console.WriteLine();

        // Использовать тип Nybble в выражении типа int.
        t = а * 2 + 3;
        Console.WriteLine("Результат вычисления выражения а * 2 + 3: " + t);
        Console.WriteLine();

        // Продемонстрировать присваивание значения типа int и переполнение.
        а = 19;
        Console.WriteLine("Результат присваивания а = 19: " + (int) а);
        Console.WriteLine();

        // Использовать тип Nybble для управления циклом.
        Console.WriteLine("Управление циклом for " +
        "с помощью объекта типа Nybble.");
        for(а = 0; а < 10; а++)
        Console.Write((int) а + " ");
        Console.WriteLine();
    }
}

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

а: 1
b: 10
а меньше b

с после операции с = а + b: 11
а после операции а += 5: 6

Результат вычисления выражения а * 2 + 3: 15

Результат присваивания а = 19: 3

Управление циклом for с помощью объекта типа Nybble.
0 1 2 3 4 5 6 7 8 9

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

t = а * 2 + 3;

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

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

а = 19;

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

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

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