Console.Write("Используем инструкцию goto. ");
// Индексная переменная и переменная
// для вычисления суммы:
int i=1,s=0;
// Метка:
start:
// Изменение переменной для вычисления суммы:
s+=i;
// Изменение индексной переменной:
i++;
// Условный оператор для перехода к метке:
if(i<=n) goto start;
// Результат метода:
return s;
}
продолжение
120
Глава 3. Основы синтаксиса языка C#
Листинг 3.1 (продолжение)
// Метод для отображения результата
// вычислений выбранным методом:
public void show(char choice){
// Отображение символьного аргумента метода:
Console.Write(choice+") ");
// Переменная для вычисления суммы:
int res;
// Оператор выбора:
switch(choice){
case 'A':
res=useWhile();
break;
case 'B':
res=useDoWhile();
break;
case 'C':
res=useFor1();
break;
case 'D':
res=useFor2();
break;
case 'E':
res=useFor3();
break;
case 'F':
res=useFor4();
break;
default:
res=useGoto();
break;
}
// Отображаем результат:
Console.WriteLine("Результат: "+res);
}
}
// Класс с главным методом программы:
class SummatorDemo{
// Главный метод программы:
public static void Main(){
// Объектная переменная:
Summator obj;
// Оператор цикла с целочисленной
// индексной переменной:
for(int i=-25;i<160;i+=50){
// Создание нового объекта:
Основные управляющие инструкции 121
obj=new Summator(i);
// Оператор цикла с символьной
// индексной переменной:
for(char s='A';s<'H';s++){
// Отображение результата
// вычислений выбранным методом:
obj.show(s);
}
// Переход к новой строке:
Console.WriteLine();
}
// Ожидание нажатия клавиши Enter:
Console.ReadLine();
}
}
Основу нашей программы составляет класс Summator, предназначенный для
решения исключительно банальной задачи — вычисления суммы натураль-
ных чисел. У класса есть закрытое целочисленное поле n, значение которо-
го определяет количество слагаемых в сумме (то есть мы вычисляем сумму
чисел от 1 до n включительно). Значение полю можно присвоить только
при создании объекта, передав присваиваемое полю значение аргументом
конструктору. При этом необходимо, чтобы аргумент попадал в диапазон
значений от 1 до 100 включительно. Если конструктору передан аргумент, меньший 1, полю n присваивается единичное значение. Если аргумент кон-
структора больше 100, полю n присваивается значение сто. Для проверки со-
ответствующих условий использованы вложенные условные операторы: сна-
чала проверяется условие, что аргумент конструктора больше 100. Если это
так, выполняются команды Console.WriteLine("Слишком большое число! Из
менено на 100.") и this.n=100. Если условие не выполнено, запускается
еще один условный оператор, в котором проверяется условие, что аргумент
меньше 1. Если он действительно меньше 1, выполняются команды Console.
WriteLine("Слишком маленькое число! Изменено на 1.") и this.n=1. Если же
и это второе условие ложно (то есть аргумент не больше 100 и не меньше 1), то
выполняться будут команды this.n=n и Console.WriteLine("Значение "+this.
n+" принято."). После завершения работы условных операторов командой
Console.WriteLine("Вычисление суммы от 1 до "+this.n+".") выводится со-
общение о вычислении суммы натуральных чисел.
Таким образом, при создании объекта поле n получает значение в диапазо-
не от 1 до 100 и в консольное окно выводится сообщение соответствующего
содержания.
Помимо конструктора, у класса Summator множество методов, назначе-
ние которых — вычислить сумму натуральных чисел. Для этого в разных
122
Глава 3. Основы синтаксиса языка C#
методах используются разные подходы (в основном базирующиеся на
использовании разных операторов цикла или различных способах их ис-
пользования).
ПРИМЕЧАНИЕ Все эти методы возвращают целочисленный результат — значение
суммы натуральных чисел. Кроме того, в начале выполнения каждого
метода выводится сообщение о том, какой метод используется для
вычисления результата.
У методов достаточно красноречивые названия. Так, в методе useWhile() сумма вычисляется с помощью оператора while(). В теле метода объявля-
ются две целочисленные переменные: индексная i для подсчета циклов
и переменная s для подсчета суммы (в эту переменную записывается те-
кущее значение вычисляемой суммы). Переменные инициализируются
с нулевыми начальными значениями, после этого запускается оператор
цикла while(). Проверяемым условием является i полняется до тех пор, пока индексная переменная меньше значения поля n объекта. В теле оператора цикла командой i++ на единицу увеличивает- ся значение индексной переменной, после чего командой s+=i значение переменной, предназначенной для подсчета суммы, увеличивается на текущее значение индексной переменной. После завершения оператора цикла в переменную s записано нужное значение — сумма чисел от 1 до n. Поэтому командой return s значение этой переменной возвращается как результат метода. В методе useDoWhile() для вычисления суммы используется оператор dowhile(). Здесь, собственно, интриги особой нет — практически все так же, как и в предыдущем случае. В четырех методах сумма вычисляется с помощью оператора цикла for(), но только реализуется все это по-разному. В методе useFor1() в операторе цикла в доке инициализации индексная переменная получает единичное значение (команда i=1). Во втором блоке проверяется условие i<=n. Это означает, что на последнем цикле команда тела цикла s+=i будет выполне- на при значении n для индексной переменной i. Эта индексная переменная каждый раз увеличивает свое значение на 1 благодаря команде i++ в тре- тьем блоке после for-инструкции оператора цикла. В методе useFor2() оператор цикла for() используется несколько ина- че. В for-инструкции теперь первый и третий блоки пустые. Команда инициализации индексной переменной перенесена из первого блока в то место, где эта индексная переменная объявляется. Команда увеличения индексной переменной на 1 вынесена из третьего блока в тело оператора цикла. Основные управляющие инструкции 123 ПРИМЕЧАНИЕ Поскольку индексная переменная инициализирована с нулевым на- чальным значением, команда увеличения индексной переменной находится перед командой изменения переменной со значением суммы чисел. Также учитывая, что в вычислениях в теле цикла зна- чение индексной переменной увеличено на 1, по сравнению с тем значением, для которого проверялось условие, нестрогое неравенство в условии заменено на строгое. В методе useFor3() оператор цикла for() имеет пустое тело (отсутствуют команды после for-инструкции). В первом блоке for-инструкции — две команды (разделенные запятой), которыми инициализируются перемен- ные i и s. Команды изменения переменных s и i объединены в одну и на- ходятся в третьем блоке for-инструкции. ПРИМЕЧАНИЕ Поскольку в команде s+=i++ использована постфиксная форма опе- ратора инкремента, то сначала значение переменной s увеличивается на текущее (старое) значение переменной i, и после этого на 1 увели- чивается значение переменной i. Команда s+=i++ эквивалентна двум последовательно выполняемым командам, s=s+i и i=i+1. Наконец, в методе useFor4() встречаем прямо противоположную ситуа- цию: for-инструкция совершенно не содержит команд, и все три блока пу- стые. Поскольку здесь мы встречаем пустой второй блок, оператор цикла является формально (и неформально тоже) бесконечным. Поэтому в теле оператора цикла предусмотрена возможность его завершения. Для этого в условном операторе проверяется условие i>n, и если это условие истин- но, выполняется инструкция break. На фоне всех этих методов и операторов метод useGoto(), в котором сум- ма натуральных чисел вычисляется с использованием условного операто- ра и инструкции безусловного перехода goto, стоит некоторым особняком, хотя на самом деле ничего особенного в этом методе нет. Традиционно ини- циализируются две переменные — индексная i и переменная s для записи в эту переменную подсчитываемой суммы. Блок команд, которые мы ранее помещали в тело цикла, помечен скромной инструкцией start, которая яв- ляется не чем иным, как меткой. После изменения значений переменных s и i выполняется условный оператор. Если условие i<=n истинно, командой goto start управление передается тому месту, которое отмечено меткой start. В результате получается такой импровизированный оператор цикла. Все перечисленные методы — закрытые. Их мы будем вызывать в откры- том методе show(). У этого метода один символьный аргумент. Буква, пере- данная аргументом методу, определяет, каким методом будет вычисляться 124 Глава 3. Основы синтаксиса языка C# сумма натуральных чисел. Соответствие устанавливается с помощью опера- тора выбора switch(). Варианты перебираются с помощью прописных букв латиницы, начиная с 'A' (затем 'B', 'C' и т. д., до буквы 'F' включительно). В зависимости от переданной аргументом методу show() буквы вызывается тот или иной метод. По умолчанию (если аргумент не есть буква в диапазо- не от 'A' до 'F') используется метод, базирующийся на инструкции goto. Помимо вычисления непосредственно суммы, методом show() в консоли отображается вычисленное значение (а также буква, которая передавалась аргументом методу). В главном методе программы инструкцией Summator obj объявляется объ- ектная переменная. После этого запускается оператор цикла, в котором индексная переменная принимает значения -25, 25, 75 и 125. Каждое из этих значений используется как аргумент конструктора при создании объ- екта класса Summator. После этого запускается еще один оператор цикла. Его особенность в том, что индексная переменная имеет тип char. Коман- да инкремента индексной переменной в этом случае фактически сводится к смене символьного значения переменной на следующую букву в кодовой таблице символов. Для отображения результатов вычислений вызывает- ся метод show() с соответствующим символьным аргументом. На рис. 3.8 Рис. 3.8. Результат выполнения программы с управляющими инструкциями Массивы большие и маленькие 125 представлен результат работы программы: в консольном окне отобража- ется результат вычисления суммы натуральных чисел разными методами для разного количества слагаемых. Как и следовало ожидать, вне зависимости от использованного метода, ре- зультат неизменен. Рассмотренные способы вычисления суммы далеко не единственно возможные, Например, есть рекурсия, которая в данном конкретном случае совершенно неуместна. Массивы большие и маленькие — Какая гадость. — Это не гадость. Это последние достижения современной науки. Из к/ф «31 июня» Представим себе такую ситуацию: нам нужно в программе создать не- сколько целочисленных переменных. Уже в этом месте становится груст- но — ведь чего только стоит подобрать каждой переменной имя. Но мы применим алгоритмический подход. Другими словами, попробуем авто- матизировать не только процесс вычислений в программе, но даже саму процедуру объявления переменных. Кульминацией этой простой, а где-то даже банальной мысли стали массивы — коллекции однотипных перемен- ных, которые объединены не только общей целью существования, но и об- щим именем. Переменные, которые входят в массив (составляют массив) называются элементами массива. Массивы, особенно в C#, могут быть самыми разными. Мы начнем с наи- более простых вариантов. Итак, сначала рассмотрим одномерные массивы. В этом случае важны следующие обстоятельства: тип элементов массива — нужно знать, сколько памяти отводить под каждый из элементов массива; количество элементов в массиве (размер массива); название массива — можно создать массив и без названия, но это скорее экзотика. Во всяком случае, для того уровня программирования, на ко- тором мы временно находимся. 126 Глава 3. Основы синтаксиса языка C# ПРИМЕЧАНИЕ Кстати, с названием массива дела обстоят не так просто, как может показаться на первый взгляд. Дело в том, что в C# массивы реализу- ются по тому же принципу, что и объекты. То, что мы будем называть именем массива, на самом деле будет переменной массива — пере- менной, в которой записана ссылка на реальный массив. Идея такая же, как и в случае с объектными ссылками. Хотя такой подход может показаться вычурным, во многих отношениях он себя оправдывает. Мы начнем с последнего пункта, касающегося имени массива. Имя масси- ва в C# — это имя переменной, которая содержит ссылку на массив. Что- бы создать массив, мало его просто создать — необходимо еще объявить переменную, в которую будет записана ссылка на массив (адрес массива). Поэтому создание массива состоит из двух этапов: объявление переменной массива и непосредственно создание массива. Как объявить переменную массива? Достаточно указать тип элементов массива и имя переменной массива (это имя мы будем отождествлять с именем массива). Чтобы отли- чить переменную массива от обычной переменной, при объявлении пере- менной массива после идентификатора типа элементов массива указывают пустые квадратные скобки. Например, следующей инструкцией объявля- ется переменная nums для целочисленного массива: int[] nums; Эта переменная может ссылаться на любой целочисленный массив. При объявлении переменной массива не имеет значения, сколько элементов в этом массиве — важен только тип этих элементов. Как соотносятся между собой переменная массива и сам массив, иллюстрирует схема на рис. 3.9. Рис. 3.9. Переменная массива и непосредственно массив В отличие, например, от языка программирования Java, в C# пустые квадратные скобки нельзя указывать после имени переменной — только после идентификатора типа, что в принципе вполне логично. Этим как бы подчеркивается, что речь идет о переменной специ- ального типа. Массивы большие и маленькие 127 Для создания самого массива используется оператор new — тот же оператор, что и при создании объектов. После оператора new указывают тип базовых элементов массива и, в квадратных скобках, количество элементов в массиве. Например, вследствие выполнения команды new int[100] создается цело- численный массив из 100 элементов. Но это еще не все. В качестве результа- та командой создания массива возвращается ссылка на этот массив. Ссылку можно записать в переменную массива. Ниже приведены некоторые коман- ды, которыми создаются два массива (целочисленный и символьный): // Переменная для целочисленного массива: int[] nums; // Создание целочисленного массива: nums=new int[100]; // Объявление переменной символьного массива и создание массива: char[] syms=new char[20]; Как и в случае с объектами, при создании массива объявление переменной массива и непосредственное создание массива можно объединять в одну команду. Обычно так и поступают. В принципе массивы бывают статическими и динамическими. Прак- тическое различие между этими типами массивов сводится к тому, что размер статических массивов должен быть известен на момент компиляции программы. Поэтому в качестве размера статического массива можно указывать только константу (или числовой литерал — то есть число). Размер динамического массива может быть определен уже после запуска программы на выполнение. Другими словами, динамический массив создается в процессе выполнения программы. В C# все массивы динамические. Для обращения к элементам массива после имени массива в квадратных скобках указывается индекс элемента в массиве. Индексация массивов всегда начинается с нуля! Это означает, что первый элемент в массиве име- ет индекс 0. Индекс последнего элемента в массиве на единицу меньше его размера — например, для массива из 100 элементов последний, 100-й, эле- мент будет иметь индекс 99. Так же просто создаются и многомерные массивы — во всяком случае, ба- зовый принцип остается неизменным. Создается переменная массива, по- сле чего с помощью оператора new создается сам массив, а ссылка на этот массив записывается в переменную массива. Принципиальное отличие от одномерного массива состоит в том, что при объявлении переменной многомерного массива после имени типа элементов массива в квадратных скобках указываются запятые (коли- чество запятых — размерность массива минус один); 128 Глава 3. Основы синтаксиса языка C# при создании многомерного массива в квадратных скобках указывается размер (количество элементов) для каждой размерности (в качестве разделителей используются запятые). ПРИМЕЧАНИЕ Размерность массива определяется количеством индексов, которые необходимо указать для однозначной идентификации элемента в массиве. В языке C# все индексы выделяются одной парой ква- дратных скобок, с использованием запятой в качестве разделите- ля — в отличие от таких языков программирования, как C++ и Java, в которых для каждого индекса используется своя пара квадратных скобок. Из многомерных массивов обычно популярностью пользуются двумерные массивы. Ниже приведены примеры создания двумерного и трехмерного массивов: // Переменная для двумерного целочисленного массива: int[,] nums; // Создание двумерного целочисленного массива: nums=new int[10,20]; // Объявление переменной трехмерного символьного массива // и создание массива: char[,,] syms=new char[5,10,15]; В листинге 3.2 приведен пример простенького программного кода, в ко- тором создаются два массива: один — числовой одномерный массив, ко- торый заполняется числами Фибоначчи, а второй — двумерный массив, заполняется случайным образом буквами. Оба массива являются полями класса. ПРИМЕЧАНИЕ В последовательности Фибоначчи первые два числа равны единице, а каждое следующее равно сумме двух предыдущих. Листинг 3.2. Знакомство с массивами using System; // Класс с полями для массивов: class MyArray{ // Поле — переменная одномерного // числового массива: int[] fibonacci; // Поле — переменная двумерного // символьного массива: Массивы большие и маленькие 129 char[,] symbols; // Конструктор класса: public MyArray(int n){ int i,j; // Создание объекта для генерирования // случайных чисел: Random rnd=new Random(); // Создание одномерного целочисленного // массива: fibonacci=new int[n]; // Создание символьного двумерного массива: symbols=new char[n-2,n+2]; // Начальные числа в последовательности // Фибоначчи: fibonacci[0]=1; fibonacci[1]=1; // Заполнение целочисленного массива // числами Фибоначчи: for(i=2;i fibonacci[i]=fibonacci[i-1]+fibonacci[i-2]; } // Заполнение двумерного массива // случайными буквами: for(i=0;i for(j=0;j // Команда с явным преобразованием типа: symbols[i,j]=(char)('A'+rnd.Next(n)); } } } // Метод для отображения числового массива: void showNums(){ Console.WriteLine("Числа Фибоначчи:"); for(int i=0;i Console.Write(fibonacci[i]+" "); } Console.WriteLine(); } // Метод для отображения символьного массива: void showSyms(){ Console.WriteLine("Случайные буквы:"); for(int i=0;i for(int j=0;j Console.Write(symbols[i,j]+" "); } продолжение 130 Глава 3. Основы синтаксиса языка C# Листинг 3.2 (продолжение) Console.WriteLine(); } } // Открытый метод для отображения массивов // (числового и символьного): public void show(){ showNums(); Console.WriteLine(); showSyms(); } } class ArrayDemo{ public static void Main(){ // Создание объекта: MyArray obj=new MyArray(10); // Отображение массивов — полей объекта: obj.show(); Console.ReadLine(); } } В принципе код достаточно простой, но все же есть несколько моментов, на которые стоит обратить внимание. В первую очередь это, конечно, спо- соб создания массивов-полей класса. Здесь интрига небольшая — соответ- ствующие поля являются переменными массива (соответствующего типа). Например, поле для хранения целочисленного одномерного массива с чис- лами Фибоначчи объявляется как int[] fibonacci — классическая пере- менная одномерного массива. Поле char[,] symbols представляет собой переменную двумерного символьного массива. Следует понимать, что такое объявление полей-массивов на самом деле не означает создания массивов. Это всего лишь переменные массивов. По умолчанию значениями этих переменных являются пустые ссылки (или null-ссылки). Массивы нужно как-то создать. Мы будем создавать масси- вы в конструкторе. Конструктор класса имеет один целочисленный аргумент. Массивы соз- даются командами fibonacci=new int[n] и symbols=new char[n-2,n+2] (здесь n — аргумент конструктора). После этого созданные массивы за- полняются значениями. С числовым массивом все достаточно просто: пер- вые два элемента получают единичные значения (команды fibonacci[0]=1 и fibonacci[1]=1). Для заполнения прочих элементов массива вызывается оператор цикла, в котором каждое новое значение вычисляется на основе двух предыдущих (команда fibonacci[i]=fibonacci[i-1]+fibonacci[i-2] в теле оператора цикла). Массивы большие и маленькие 131 В C# для определения количестве элементов массива используют свойство Length. Свойство вызывается из переменной массива. Для одномерного массива значение этого свойства совпадает с разме- ром массива. Например, инструкцией fibonacci.Length в качестве значения возвращается размер массива fibonacci. Для многомер- ного массива это общее количество элементов. Чтобы определить размер массива по определенной размерности, используют функ- цию GetLength(индекс). Здесь в качестве аргумента указывается индекс размерности массива (индексация начинается с нуля). Так, инструкция symbols.GetLength(0) дает количество элементов массива symbols по первой размерности (размер массива по первому индек- су), а инструкцией symbols.GetLength(1) возвращается количество элементов массива symbols по второй размерности (размер массива по второму индексу). Поскольку мы планируем заполнять символьный массив случайными бук- вами, нам нужно создать нечто случайное. Мы, ничтоже сумняшеся, ко- мандой Random rnd=new Random() создаем объект rnd библиотечного класса Random, предназначенного для работы со случайными числами. Как след- ствие, у объекта rnd имеется, кроме прочего, метод Next(), который позво- ляет генерировать случайные числа. Инструкцией rnd.Next(n) генериру- ется случайное целое число в диапазоне от 0 до n1 (n — аргумент метода Next()). Эта инструкция составляет основу команды symbols[i,j]=(char) ('A'+rnd.Next(n)), которой генерируется случайная буква и присваивается в качестве значения элементу символьного массива. Формально инструк- ция 'A'+rnd.Next(n) означает, что к символьной переменной 'A' добавля- ется некоторое целое число. Сама по себе такая команда является оши- бочной. Но если перед командой добавить инструкцию (char), все будет нормально — получим букву. Дело в том, что инструкция вида (тип)(выра жение) является командой явного приведения типа. В результате ее выпол- нения значение выражения приводится к указанному типу. В нашем случае выражение (char)('A'+rnd.Next(n)) вычисляется так: к коду символа 'A' добавляется значение rnd.Next(n), и полученный числовой результат при- водится к символьному типу — полученное число является кодом символа в кодовой таблице. В результате получаем буквы английского алфавита в диапазоне от 'A' до 'J'. Ранее для символьной переменной мы использовали команду инкре- мента. При этом ошибки не возникало. Причина в том, что команда вида x++ для переменной типа char фактически эквивалентна команде вида x=(char)(x+1). 132 Глава 3. Основы синтаксиса языка C# В классе есть закрытый метод showNums() для отображения содержимого числового массива, а также закрытый метод showSyms() для отображения элементов символьного массива. Оба этих метода последовательно вы- зываются в открытом методе show(). Именно этот метод мы используем для отображения содержимого массивов-полей объекта obj класса MyArray в главном методе в классе ArrayDemo. Результат (возможный) выполнения программы представлен на рис. 3.10. Рис. 3.10. Результат выполнения программы с классом, у которого есть поля-массивы От запуска к запуску результат может меняться, поскольку буквы в дву- мерном массиве случайные. Выше мы заполняли массивы с помощью операторов цикла. Но это воз- можно только в том случае, если значения элементов подчиняются неко- торой логике. А логика в нашем деле не всегда гарантирована. Поэтому ак- туальна задача быстрой и простой инициализации массива. Такая метода есть. Базируется она на том, что при объявлении массива для него указы- вается список (в фигурных скобках) значений элементов. Примеры при- ведены ниже: // Массив из пяти элементов: int[] nums={1,2,3,4,5}; // Массив из пяти элементов (размер явно не указан): int[] nums=new int[]{1,2,3,4,5}; // Массив из пяти элементов (явно указан размер): int[] nums=new int[5]{1,2,3,4,5}; // Двумерный символьный массив (размерами 2 на 3): char[,] syms={{'A','B','C'},{'D','E','F'}}; // Двумерный символьный массив // (размерами 2 на 3 — размер явно не указан): char[,] syms=new char[,]{{'A','B','C'},{'D','E','F'}}; // Двумерный символьный массив // (размерами 2 на 3 — явно указан размер): char[,] syms=new char[2,3]{{'A','B','C'},{'D','E','F'}}; Массивы большие и маленькие 133 Если при инициализации массива размер явно не указан, он определяется автоматически по количеству элементов и способу их группировки (для многомерных массивов). Как уже отмечалось, при работе с массивами нередко используется опера- тор цикла foreach(). Хотя он имеет ограниченную область применимости, в некоторых случаях оператор бывает достаточно полезным. Работу это- го оператора мы рассмотрим на очень простом примере, представленном в листинге 3.3. Листинг 3.3. Оператор цикла foreach() using System; class ForeachDemo{ // Главный метод программы: public static void Main(){ // Двумерный символьный массив размерами // 2 (строки) на 3 (столбца): char[,] symbs={{'A','B','C'},{'D','E','F'}}; // Оператор цикла foreach() — перебираются // все элементы массива: foreach(char s in symbs){ // Выводится значение элемента массива: Console.Write(s+" "); } // Переход к новой строке: Console.WriteLine(); // Ожидание нажатия клавиши Enter: Console.ReadLine(); } } В результате выполнения этой программы получаем в консольном окне со- общение из последовательности букв — значений массива, распечатанных в одну строку, как показано на рис. 3.11. Рис. 3.11. Результат выполнения программы с оператором цикла foreach() Инструкция foreach(char s in symbs) означает, что в процессе выполне- ния оператора цикла локальная переменная s типа char последовательно перебирает элементы массива symbs. Другими словами, на каждом цикле 134 Глава 3. Основы синтаксиса языка C# значение переменной s соответствует очередному элементу symbs. И хотя такой способ обработки массива может показаться удобным, на практике он не всегда приемлем. Массивы экзотические и не очень — Это безрассудство! Тебя могли увидеть! — Ничего страшного — сочтут за обыкновенное привидение. Из к/ф «Тот самый Мюнхгаузен» До этого мы ограничивались рассмотрением массивов, элементами кото- рых являются значения базовых типов (или, на худой конец, текст). Од- нако элементом массива может быть практически все, что угодно. В этом разделе нам будет угодно, чтобы роль элементов на себя примерили объ- ектные переменные, а также переменные массива. Другими словами, мы познакомимся с тем, как создавать массивы из объектов, а также массивы из массивов. Хотя, если принять к сведению, что в C# массивы реализу- ются по тому же принципу, что и объекты, несложно догадаться, что эти две задачи на самом деле являются одной задачей — не очень сложной, но где-то очень экзотической. Начнем с объектов. Обратимся к листингу 3.4, в котором представлен простенький пример того, как объекты можно орга- низовать в виде одномерного массива. Листинг 3.4. Массив объектов using System; // Класс для реализации комплексных чисел: class CNum{ // Действительная часть комплексного числа: public double Re; // Мнимая часть комплексного числа: public double Im; // Конструктор класса с двумя аргументами: public CNum(double x,double y){ Re=x; Im=y; } // Метод для отображения параметров числа: public void show(){ Console.WriteLine("Re="+Re+" и Im="+Im); } } Массивы экзотические и не очень 135 class CNumDemo{ // Главный метод программы: public static void Main(){ // Размер массива: int n=9; // Модуль комплексного числа: double r=10; // Локальные переменные: double x,y; // Создание массива из объектных переменных: CNum[] nums=new CNum[n]; // Заполнение массива: for(int i=0;i x=r*Math.Cos(2*Math.PI*i/n); // Действительная часть y=r*Math.Sin(2*Math.PI*i/n); // Мнимая часть nums[i]=new CNum(x,y); // Создание нового объекта Console.Write(i+1+"-е число: "); // Отображение текста nums[i].show(); // Отображение параметров числа } // Ожидание нажатия клавиши Enter: Console.ReadLine(); } } В программе описывается класс CNum, который имеет некоторую аналогию с классом для описания комплексных чисел. У класса CNum есть два откры- тых поля, Re и Im, типа double, которые предназначены для записи, соответ- ственно, действительной и мнимой частей комплексного числа. У класса есть конструктор с двумя аргументами и метод show() для отображения в консоли параметров объекта класса (значений полей Re и Im). ПРИМЕЧАНИЕ Комплексное число вида x + iy , где мнимая единица 2 i = -1 по определению, полностью определяется двумя числами: действитель- ной частью x и мнимой частью y . Как действительная, так и мни- мая части комплексного числа по определению являются числами действительными. Здесь нет ничего интересного. Все интересное происходит в главном ме- тоде программы в классе CNumDemo. Помимо обычных команд по объявле- нию и инициализации локальных переменных в главном методе командой CNum[] nums=new CNum[n] создается массив из объектных переменных клас- са CNum. Как мы уже знаем, подобного рода команда является объединением двух команд: инструкцией CNum[] nums объявляется переменная массива nums. О чем свидетельствует тип CNum для элементов массива? Он свиде- тельствует о том, что значениями массива nums могут быть переменные 136 Глава 3. Основы синтаксиса языка C# типа CNums. А переменные типа CNum являются объектными переменными. Другими словами, значениями элементов массива CNum могут быть ссылки на объекты класса CNum. После этого небольшого уточнения дальнейшая логика создания массива объектов проста и очевидна. Так, инструкцией new CNum[n] создается массив из n элементов. Ссылка на массив хранит- ся в переменной nums. В операторе цикла командой nums[i]=new CNum(x,y) в i-й элемент массива записывается ссылка на объект, который создается командой new CNum(x,y). После этого элемент массива nums[i] ссылается на объект класса CNum. Из этого объекта можно вызвать метод show(), что мы и делаем, когда используем в операторе цикла команду nums[i].show(). Результат выполнения программы показан на рис. 3.12. Рис. 3.12. Результат выполнения программы с массивом из объектов В программном коде мы использовали некоторые математические функции (синус и косинус), а также константу для числа π. Методы Cos() и Sin() (равно как и константа PI) являются статическими и вы- зываются из библиотечного класса Math. Практически точно так же создается массив из массивов, лишь с поправ- кой на тип элементов — теперь это не объектные переменные, а перемен- ные массива. Для конкретики рассмотрим создание массива, элементами которого являются целочисленные массивы (точнее, переменные целочис- ленных массивов). Переменная целочисленного массива — это перемен- ная, объявленная с типом int[]. Чтобы создать массив из таких перемен- ных, необходимо как минимум объявить переменную для этого массива. Ее тип — это тип int[] плюс пустые квадратные скобки []. Получается int[][]. Дальше рассмотрим программный код в листинге 3.5. Листинг 3.5. Массив из массивов using System; class BinomDemo{ // Статический метод для отображения элементов целочисленного //массива: static void show(int[] m){ // Массив- аргумент метода Массивы экзотические и не очень 137 foreach(int s in m){ Console.Write (s+" "); // Элементы отображаются в ряд } Console.WriteLine(); // Переход к новой строке } // Главный метод программы: public static void Main(){ int n=15; // Размер массива // Создание массива из массивов: int[][] binom=new int[n][]; // Заполнение массива: for(int i=0;i binom[i]=new int[i+1]; // Создаем массив-элемент binom[i][0]=1; // Первый элемент массива-элемента // Последний элемент массива-элемента: binom[i][binom[i].Length-1]=1; // Заполнение внутренних элементов // массива-элемента: for(int k=1;k // Вычисляем биномиальные коэффициенты: binom[i][k]=binom[i-1][k-1]+binom[i-1][k]; binom[i][binom[i].Length-k-1]=binom[i][k]; } // Отображаем массив-элемент: show(binom[i]); } // Ожидание нажатия клавиши Enter: Console.ReadLine(); } } ПРИМЕЧАНИЕ С помощью представленной программы мы вычисляем «треугольник Паскаля» — специальным образом упорядоченный набор биномиаль- ных коэффициентов. По определению биномиальный коэффициент k n ! Cn = , где целочисленный индекс k может принимать k !( n - k)! значения от 0 до n включительно. Эти коэффициенты обладают не- которой симметрией, которой мы и воспользуемся при их вычислении. Так, легко убедиться, что k n k Cn C - = n . Кроме того, в вычислениях нам понадобится соотношение k k 1 - k Cn = Cn 1 + C - n 1 - . Для некоторых биномиальных коэффициентов можно записать явные выражения. Например, 0 C = 1 n , а 1 Cn = n. Что касается треугольника Паскаля, то его можно представить как ряды биномиальных коэффициентов — каждый ряд соответствует фиксиро- 138 Глава 3. Основы синтаксиса языка C# ванному индексу n, начиная с нуля. Каждый такой ряд с биномиальны- ми коэффициентами мы реализуем в виде числового массива. А сами эти массивы будут элементами еще одного массива. Таким образом, получаем массив, элементами которого являются числовые массивы, причем разной длины. Как и при написании детектива, здесь мы идет от обратного — сначала создаем внешний массив, а уже после этого упаковываем в него внутренние массивы (или массивы-элементы) и заполняем их биномиальными коэффициентами (которые, кстати, вычисляем вручную на основе рекуррентных соотношений). Результат выполнения этой программы представлен на рис. 3.13. Рис. 3.13. «Треугольник Паскаля»: результат выполнения программы, в которой создается массив из массивов Проанализируем программный код, который приводит к столь замечатель- ным результатам. Начнем с малого. В программе описан статический метод show(), который не возвращает результат и у которого объявлен аргумент — целочисленный массив (на самом деле переменная массива). Код у метода простой и прогнозируе- мый: в результате выполнения метода в строчку через пробел отобража- ются значения элементов массива-аргумента. Нам метод show() еще пона- добится. ПРИМЕЧАНИЕ Конструкция вида int[][] binom должна быть более-менее понятна. Мы объявляем переменную binom, которая является переменной массива с элементами типа int[]. Инструкция new int[n][] означает, что создает- ся массив из n элементов, а элементы типа int[]. Немного неожиданным может показаться то, что размер массива указан в первых квадратных скобках, а не во-вторых, но таковы уж правила синтаксиса. Массивы экзотические и не очень 139 В главном методе программы командой int[][] binom=new int[n][] мы объявляем переменную массива binom, создаем массив и ссылку на массив присваиваем этой переменной. Для заполнения элементов массива запускается оператор цикла, в кото- ром с помощью индексной переменной i перебираются элементы массива binom. При этом размер массива определяется свойством binom.Length. Еще раз обращаем внимание читателя на то, что массив binom — одномерный. Его элементы — переменные массива, которые могут (и будут) ссылаться на одномерные числовые массивы. Командой binom[i]=new int[i+1] создаются целочисленные массивы, и ссылки на них записываются в переменные массива, которые являются элементами массива binom. Размер каждого следующего массива на едини- цу больше размера предыдущего массива. Таким образом, переменная мас- сива binom[i] ссылается на целочисленный массив размера i+1. Командой binom[i][0]=1 начальному элементу внутреннего массива binom[i] присваивается единичное значение. Такую же процедуру мы про- делываем с последним элементом массива binom[i], для чего вызываем ко- манду binom[i][binom[i].Length-1]=1. Если binom[i] — массив, то binom[i][0] — первый элемент массива binom[i]. Размер массива binom[i] (количество элементов в массиве) может быть вычислен инструкцией binom[i].Length. Тогда индекс последнего элемента binom[i].Length-1, а сам последний элемент массива возвращается инструкцией binom[i][binom[i].Length-1]. Присваивая первому и последнему элементам массива binom[i], мы вычисляем биномиальные коэффициенты 0 C 1 1 i+ = . Заполнение внутренних элементов массива binom[i] осуществляется во вложенном операторе цикла с индексной переменой k. Начальное значе- ние этой переменной 1, и за каждый цикл ее значение увеличивается на единицу до тех пор, пока выполняется условие k цикла выполняются команды binom[i][k]=binom[i-1][k-1]+binom[i-1][k] и binom[i][binom[i].Length-k-1]=binom[i][k]. После того как массив binom[i] заполнен элементами, отображаем его со- держимое с помощью команды show(binom[i]). Здесь мы еще раз исполь- зуем то обстоятельство, что binom[i] — это одномерный целочисленный массив. 140 Глава 3. Основы синтаксиса языка C# Здесь следует учесть, что элемент binom[i][k] соответствует биноми- альному коэффициенту i 1 C + k . Команда binom[i][k]=binom[i-1][k-1]+ + binom[i-1][k] является применением правила k k 1 - k C i 1 C + = i + Ci для вычисления биномиальных коэффициентов. Она применима, если значения массива binom[i-1] уже заполнены. Вызывая команду binom[i][binom[i].Length-k-1]=binom[i][k], мы применяем на прак- тике правило i 1 + k - k Ci 1 = C + i 1 + . При этом индексная переменная k не превышает значение i + 1 - k , то есть имеет место соотношение k £ i + 1 - k , или, учитывая целочисленность индексных перемен- ных, k < i + 2 - k . Важно то, что i + 2 — это общее количество биномиальных коэффициентов с нижним индексом i + 1 . Учитывая, что биномиальные коэффициенты с нижним индексом i + 1 записа- ны в массив binom[i], их количество вычисляем инструкцией binom[i]. Length. Отсюда и условие для индексной переменной k Length-k. Знакомство с указателями Ну зачем такие сложности?! Из к/ф «Приключения Шерлока Холмса и доктора Ватсона. Собака Баскервилей» В C# есть достаточно специфичный тип данных — указатели. Значени- ем переменной-указателя (или просто указателя) является адрес другой переменной. Другими словами, в качестве значения указателю можно при- своить адрес памяти. В некотором смысле указатели напоминают объект- ные переменные. Однако, в отличие от объектных переменных, поведение которых прописано и контролируется, с указателями можно проделывать многие, на первый взгляд очень необычные штуки. ПРИМЕЧАНИЕ Указатели в C# — это отголосок языка С++, в котором без них и шагу не ступить. Правда, в C# указатели намного консервативнее. Например, указатели могут ссылаться только на нессылочные данные — то есть на объект указатели не ссылаются (но зато могут ссылаться на поля объекта). Но это все же лучше, чем их полное отсутствие — как, на- пример, в языке Java. Знакомство с указателями 141 Мы не планируем массово использовать указатели, поэтому здесь состоит- ся только краткое знакомство с ними. Для начала выясним, как объявля- ется указатель. А объявляется он достаточно просто: практически так же, как обычная переменная, только в качестве типа указывается базовый тип переменной, на которую ссылается указатель, и символ * (звездочка). На- пример, если мы хотим создать указатель на переменную целочисленно- го типа, то соответствующее объявление могло бы выглядеть как int* p. Здесь p — это имя переменной-указателя, символ * есть индикатор того, что это именно указатель, а идентификатор типа int является молчаливым свидетелем того, что значением указателя может быть адрес целочислен- ной переменной типа int. Аналогично, для создания указателя на double- переменную, используем инструкцию вида double* q, и т. д. Есть два полезных оператора, которые часто используются при работе с указателем. С помощью оператора & можно получить адрес перемен- ной — достаточно указать этот оператор перед именем переменной. Об- ратную процедуру (узнать, какое значение записано по адресу, который является значением указателя) позволяет выполнить оператор *. Этот опе- ратор указывается перед переменной-указателем. Но это еще не все. Если программа содержит блок с указателями, этот блок кода должен быть по- мечен специальным ключевым словом unsafe. Нередко это ключевое слово указывают в заголовке метода, в котором использованы указатели. Более того, компилировать код с указателями можно только с использованием инструкции /unsafe. Ключевым словом unsafe отмечается небезопасный код. Дело в том, что через указатели мы получаем прямой доступ к операциям с памятью. Исполнительная система не может гарантировать пол- ную безопасность программного кода с указателями. Корректность работы программного кода с указателями является полностью зо- ной нашей ответственности. Но это совсем не означает, что код с указателями какой-то ущербный. Просто нужно реально осознавать степень риска и степень ответственности. Что касается компиляции программы с параметром /unsafe, то в среде Visual C# Express необ- ходимо в меню Проект выбрать команду Свойства, в раскрывшейся вкладке с именем проекта выбрать раздел Построение и установить флажок опции Разрешить небезопасный код. Иначе проект не от- компилируется. Методы работы с указателями рассмотрим на небольшом примере. Он приведен в листинге 3.6. 142 Глава 3. Основы синтаксиса языка C# Листинг 3.6. Знакомство с указателями using System; class PointerDemo{ // Используем атрибут unsafe: unsafe public static void Main(){ int* p; // Объявляем указатель int n; // Объявляем обычную переменную p=&n; // Указатель "помнит" адрес переменной n n=100; // Переменной n присвоили значение // По адресу-значению указателя p // записываем значение: *p=200; Console.WriteLine("n="+n); // Проверяем результат Console.ReadLine(); // Ожидание нажатия клавиши Enter } } В результате выполнения этого кода в консольном окне появится сообще- ние n=200. Проанализируем, почему происходит именно так. Для этого разберем поэтапно команды в методе Main() (который, кстати, объявлен с атрибутом unsafe). Командой int* p объявляется указатель p на целочис- ленную переменную. Целочисленная переменная объявляется следующей командой int n. Связь между указателем p и переменной n появляется по- сле выполнения команды p=&n. В результат адрес, по которому прописа- на переменная n, записывается в качестве значения в указатель p. Затем переменной n присваиваем значение 100. Но после выполнения команды *p=200 переменная n получает значение 200. Почему? Да потому, что *p — это ссылка на значение, которое прописано по адресу p. А по этому адресу прописана переменная n. Поэтому значение именно этой переменной ме- няется. То, что мы увидели, — это только вершина айсберга. У указателей множе- ство удивительных свойств. Например: Арифметические операции с указателями выполняются по особым пра- вилам — по правилам адресной арифметики. Например, разность двух указателей — это целое число, определяющее количество ячеек между адресами, на которые ссылаются указатели. Имя массива является указателем на его первый элемент. Указатели можно индексировать — почти как массивы. Из указателей можно создавать массив и делать много других удиви- тельных вещей. Однако рассмотрение всех этих вопросов не вписывается в наши планы. Перегрузка операторов Что бы мы делали без науки? Подумать страшно! Из к/ф «31 июня» У нас уже заходила речь о том, что действие некоторых базовых операторов в C# можно доопределить так, что, можно будет эти самые операторы при- менять в отношении объектов, созданных силой воображения пользовате- ля (или программиста — это как посмотреть). Называется данное действо перегрузкой операторов. Именно она будет занимать все наши помыслы вплоть до окончания данной главы. А может и больше — кому как повезет. Операторные методы и перегрузка операторов — Благородная Нинэт, я вам предлагаю маленький заговор. — А большой нельзя? — Маленький, но с большими последствиями. — Что надо делать? Я готова на все. Из к/ф «31 июня» Перегрузка операторов — это, если хотите, особая философия, в основе которой лежит понятие операторного метода. А чтобы понять, что такое 144 Глава 4. Перегрузка операторов операторный метод, придется задуматься и задаться вопросом, который может показаться странным: чем оператор отличается от метода? Если «пройтись по верхам», то ответ будет «всем». Если «копнуть вглубь», то ответ будет «ничем». А истина, как известно, всегда находится где-то по- средине между наиболее радикальными вариантами. Нам, для решения поставленной задачи по перегрузке операторов, удобно будет думать об этих самых операторах как об особого типа методах. Для обычного метода (при вызове метода в команде) аргументы указываются в круглых скобках после имени метода. Для оператора роль аргументов играют операнды. Специфическое обозначение оператора служит альтер- нативой имени метода. Поэтому задача перегрузки оператора для какого- то определенного класса может рассматриваться как определение для этого класса специального метода, который вызываться автоматически в случае, если перегружаемый оператор задействован по отношению к объекту клас- са. Другими словами, чтобы перегрузить оператор, в классе, для которого выполняется такая перегрузка, необходимо описать операторный метод. Соответствие между операторами и операторными методами устанавлива- ется просто: имя операторного метода, соответствующего определенному оператору, получается объединением ключевого слова operator и символа оператора. Например, операторный метод для оператора сложения + будет называться operator+. Для оператора умножения * операторный метод на- зывается operator*, и т. д. Существует несколько правил, которых необходимо придерживаться при описании операторных методов в классе. Операторные методы описываются с атрибутами public и static. Операторные методы должны быть открытыми и статическими, что вполне понятно, поскольку метод должен быть доступен вне класса (от- крытость метода) и относится он к классу как такому, а не к отдельному объекту (статичность метода). Количество аргументов операторного метода совпадает с количеством операндов соответствующего оператора: для бинарных операторов у опе- раторного метода два аргумента, для унарных операторов у операторного метода один аргумент. По крайней мере один из аргументов операторного метода должен быть объектом класса, в котором этот операторный метод описан. Операторный метод должен возвращать результат. Результат оператор- ного метода — это результат вычисления выражения с перегружаемым оператором и соответствующими операндами — аргументами оператор- ного метода. В листинге 4.1 приведен программный код простенькой программы, в ко- торой использована перегрузка некоторых операторов — а если быть более точным, то двух. Операторные методы и перегрузка операторов 145 ПРИМЕЧАНИЕ Прежде чем приступить к анализу программного кода, имеет смысл кратко остановиться на общей идее. А идея в том, чтобы создать небольшой валютный калькулятор, который позволил бы выполнять основные операции (условные) с денежными суммами в разной валюте. Для хранения валютных резервов создаем специальный класс, а отдельные транши будут реализовываться через объекты этого класса. У класса есть два поля: одно содержит номинальное значение в иностранной валюте, а еще одно поле содержит значение обменного курса. Задача состоит в том, чтобы научиться складывать денежные суммы в разной валюте. Понятно, что для проведения таких расчетов необходимо денежные суммы привести к общему знаменателю — выразить в одной и той же валюте. Таким знаме- нателем в нашем случае будут рубли. Однако еще остается вопрос о том, в какой валюте выражать результат. Мы будем пользоваться следующим правилом. Если к долларам прибавляем евро, получаем доллары. Если к евро прибавляем доллары, получаем евро. Если к рублям прибавляем доллары, получаем рубли. Если к долларам прибавляем рубли, получаем доллары. При этом с рублями мы будем отождествлять не только объекты (с единичным обменным курсом) созданного нами класса, но и обычные действительные числа. Листинг 4.1. Перегрузка операторов using System; // Класс с перегрузкой операторов: class Currency{ // Открытые поля класса public double nominal; public double rate; // Конструктор класса: public Currency(double nominal,double rate){ // Присваивание значений полям: this.nominal=nominal; this.rate=rate; } // Метод для вычисления стоимости (в рублях): public double price(){ return nominal*rate; } // Метод для отображения параметров объекта: public void show(){ Console.WriteLine("Номинальная сумма в валюте: "+nominal); Console.WriteLine("Обменный курс (в рублях): "+rate); продолжение 146 Глава 4. Перегрузка операторов Листинг 4.1 (продолжение) Console.WriteLine("Стоимость (в рублях): "+price()+"\n"); } // Перегрузка оператора сложения. // Операнды — объекты класса: public static Currency operator+(Currency A,Currency B){ // Объектная переменная: Currency C; // Локальные переменные: double nominal,rate; // Вычисление значений для создания // на их основе нового объекта: rate=A.rate; nominal=(A.price()+B.price())/rate; // Создание нового объекта: C=new Currency(nominal,rate); // Созданный объект возвращается // в качестве результата: return C; } // Перегрузка оператора сложения. // Операнды — объект класса и число: public static Currency operator+(Currency A,double B){ // Объектная ссылка: Currency C; // Локальные переменные: double nominal,rate; // Вычисление значений переменных // для создания на их основе объекта: rate=A.rate; nominal=(A.price()+B)/rate; // Создание объекта: C=new Currency(nominal,rate); // Созданный объект возвращается // в качестве результата: return C; } // Перегрузка оператора присваивания. // Операнды — число и объект класса: public static double operator+(double A,Currency B){ // В качестве результата возвращается число: return A+B.price(); } // Перегрузка унарного оператора !: public static double operator!(Currency A){ Операторные методы и перегрузка операторов 147 // Отображается информация об объекте: A.show(); // Результат операторного метода: return A.price(); } } // Класс с главным методом программы: class CurrencyDemo{ // Главный метод программы: public static void Main(){ // Объектные переменные: Currency Dol, Eur, Money; // Создание объектов: Dol=new Currency(100,30); Eur=new Currency(300,40); // Сложение объектов: Money=Dol+Eur; // Проверяем результат: Money.show(); // Меняем порядок слагаемых: Money=Eur+Dol; // Проверяем результат: Money.show(); // Складываем объект и число: Money=Dol+9000; // Проверяем результат: Money.show(); // Команда содержит инструкцию // суммирования числа и объекта: Console.WriteLine("Сумма в рублях: "+(0+Money)+"\n"); // Проверяем работу перегруженного // унарного оператора: Console.WriteLine("Контрольное значение: "+!Money); // Ожидание нажатия клавиши Enter: Console.ReadLine(); } } Все самое интересное описано в классе Currency. У класса два открытых поля типа double. В поле nominal записывается номинальная сумма в ино- странной валюте. В поле rate записывается обменный курс — стоимость единицы иностранной валюты в рублях. У класса конструктор с двумя аргументами, которые определяют значения полей создаваемого объекта. Также у класса есть ряд полезных методов, среди которых и операторные. Открытый метод price() вычисляет стоимость валютного объекта в ру- блях. Чтобы вычислить эту величину достаточно умножить значение поля 148 Глава 4. Перегрузка операторов nominal на значение поля rate. Именно такое значение метод возвращает в качестве результата. Также есть у класса весьма полезный метод show(), которым в консольное окно выводится вся важная информация об объекте: значения полей и их произведение. В классе перегружается, как отмечалось, два оператора — бинарный опе- ратор сложения + и унарный оператор логического отрицания !. Причем для оператора сложения в классе предлагается три варианта перегрузки, в зависимости от типа операндов: два операнда — объекты класса Currency; первый операнд — объект класса Currency, а второй аргумент — числовое значение типа double; первый аргумент — числовое значение типа double, а второй аргумент — объект класса Currency. Хотя мы привыкли к тому, что в математике операция сложения коммутативна (от перестановки слагаемых сумма не меняется), в про- граммировании изменение порядка операндов может иметь карди- нальные последствия. Наше исследование начнем с анализа операторного метода для перегрузки оператора сложения, когда операндами являются объекты класса Currency. Шапка операторного метода на этот случай выглядит так: public static Currency operator+(Currency A,Currency B) Атрибуты public и static традиционны в этом случае, и их мы уже коммен- тировали. В качестве типа результата указано ключевое слово Currency. Это означает, что в качестве результата возвращается объект класса Currency. ПРИМЕЧАНИЕ Если быть более точным, это означает, что результатом метода явля- ется ссылка на объект класса Currency. Поскольку перегружается оператор сложения, операторный метод называ- ется operator+. Аргументы A (первый операнд) и B (второй операнд) — объ- екты класса Currency. Это означает, что операторный метод будет вызы- ваться каждый раз, когда мы к объекту класса Currency будем прибавлять объект класса Currency. Теперь обратимся к программному коду в основ- ном теле операторного метода. Поскольку метод в качестве результата возвращает объект, этот объект в теле метода необходимо создать. Мы начинаем с малого — объявляем Операторные методы и перегрузка операторов 149 объектную переменную С (команда Currency C). Далее нам предстоит соз- дать объект. Но предварительно необходимо рассчитать его параметры. Для этого мы вводим две локальные переменные, nominal и rate (обе типа double). Эти переменные будут определять значения одноименных полей создаваемого объекта. Переменной rate значение присваивается коман- дой rate=A.rate. Значение поля rate объекта-результата будет таким же, как и значение поля rate первого операнда. Значение переменной nominal задается командой nominal=(A.price()+B.price())/rate. Вычисления простые: «цена в рублях» первого операнда суммируется с «ценой в ру- блях» второго операнда, а полученное значение делится на курс первой валюты, который записан в переменную rate. После проведенных нехи- трых вычислений командой C=new Currency(nominal,rate) смело создаем новый объект, а командой return C возвращаем его как результат опера- торного метода. Практически так же функционирует и версия операторного метода для оператора сложения в случае, если второй операнд B является числом типа double. Здесь достаточно учесть, что второй операнд и есть «цена в рублях», поэтому значение переменной nominal определяется командой nominal=(A. price()+B)/rate. Намного больше различий в варианте операторного метода, в котором первый аргумент A есть число, а второй аргумент B — объект. Теперь ре- зультатом метода является числовое значение типа double, а тело метода состоит всего из одной команды return A+B.price(). Результат метода вы- числяется как сумма первого аргумента и «цена в рублях» объекта — вто- рого операнда. Практически так же легко перегружается оператор логического отрица- ния !. Главная его особенность связана с тем, что оператор этот унарный. Поэтому у операторного метода operator! всего один аргумент, и это объ- ект A класса Currency. В качестве результата методом возвращается чис- ловое значение типа double. Тело метода состоит из двух команд. Коман- дой A.show() отображается информация об объекте-операнде, а командой return A.price() в качестве результата метода возвращается «рублевая цена» операнда. В главном методе программы проверяется функциональность перегружен- ных операторов. Для этого мы создаем три объектные переменные, Dol, Eur и Money, класса Currency. В переменные Dol и Eur записываем ссылки на объекты, а с переменной Money начинаем эксперименты. Складываем объ- екты (команды Money=Dol+Eur и Money=Eur+Dol), складываем объект и чис- ло (команда Money=Dol+9000), а также число и объект (команда 0+Money). Для проверки параметров объекта Money используется инструкция Money. show(). Кроме того, в программном коде есть инструкция !Money, в которой 150 Глава 4. Перегрузка операторов унарный оператор логического отрицания применяется к объекту Money. Результат выполнения программы показан на рис. 4.1. Рис. 4.1. Результат выполнения программы с перегруженными операторами Возможно, некоторого пояснения потребует результат выполнения ко- манды Console.WriteLine("Контрольное значение: "+!Money). Здесь особо необычного ничего нет. Просто при выводе текстового сообщения вместо инструкции !Money следует подставить числовое значение — результат метода Money.price(). Однако перед этим, в соответствии с программным кодом операторного метода operator!, должна быть выполнена инструк- ция Money.show(). Эта инструкция выполняется в процессе вычисления результата выражения !Money, а значит, до того, как будет выведен текст "Контрольное значение: ". Стоит также обратить внимание на инструкцию 0+Money. Если бы мы вы- полнили инструкцию Money+0, получили бы объект такой же, как Money. А результатом инструкции 0+Money является значение, возвращаемое ме- тодом Money.price(). Вот насколько важно выдерживать нужный порядок аргументов/операндов. Далеко не все операторы можно перегружать. Например, нельзя перегружать оператор присваивания и его сокращенные формы, не перегружается оператор «точка», и ряд других. Вместе с тем в С# есть некоторые трюки, которые позволяют несколько сгладить осадок в душе от таких запретов. Также стоит обратить внимание на то, что перегрузка оператора для класса пользователя никак не влияет на способ действия этого опе- ратора на базовые типы данных и библиотечные классы. Перегрузка арифметических операторов и операторов приведения типа 151 Перегрузка арифметических операторов и операторов приведения типа — Скажите, доктор Ватсон, вы понимаете всю важность моего открытия? — Да, как эксперимент это интересно. Но какое практическое применение? — Господи, именно практическое! Из к/ф «Приключения Шерлока Холмса и доктора Ватсона. Знакомство» Рассмотрим еще один пример, в котором перегружается большинство арифметических операторов. Также есть операторы-сюрпризы. Но, перед анализом программного кода, по сложившейся традиции — несколько слов об общей идее, положенной в основу программного кода. Основу кода со- ставляет класс Vector, предназначенный для работы с такими чудесными математическими объектами, как векторы. ПРИМЕЧАНИЕ Стивен Хокинг, один из выдающихся физиков современности, утверж- дает, что каждая формула в книге вдвое уменьшает количество чита- телей. В этом отношении такое чудо человеческой мысли, как вектор, способно распугать подавляющее большинство читателей. Мы при- бегаем к этой вынужденной мере по причине крайней необходимо- сти — ну на чем-то же надо перегружать арифметические операторы. Поэтому, осознавая, что многие читатели понятия не имеют, что такое вектор, приведем краткую векторную справку. Здесь мы будем вести речь о векторах в трехмерном декартовом про- странстве. С математической точки зрения такой вектор является на- бором трех числовых параметров, которые называются координатами вектора. Обычно векторы обозначаются буквой со стрелкой сверху. Так, если вектор a имеет координаты 1 a , a 2 и a 3 , то этот чудесный факт отображается записью вида a = ( 1 a , a 2, a 3) . Для векторов уста- навливаются некоторые математические операции, которые постули- руются на уровне операций с координатами векторов. Нас интересуют следующие операции (векторы a = ( 1 a , a 2, a 3) и b = ( 1 b , 2 b , 3 b )): • Сумма векторов: результатом будет являться вектор c = a + b = ( a 1 + 1 b , a 2 + 2 b , a 3 + 3 b ) c = a + b = ( a 1 + 1 b , a 2 + 2 b , a 3 + 3 b ) — вектор, координаты которого рав- ны сумме соответствующих координат суммируемых векторов. 152 Глава 4. Перегрузка операторов • Разность векторов: результатом является вектор c = a - b = ( a 1 - 1 b , a 2 - 2 b , a 3 - 3 b ) c = a - b = ( a 1 - 1 b , a 2 - 2 b , a 3 - 3 b ) — вектор, координаты которого равны разности соответствующих координат отнимаемых векто ров. • Умножение вектора на число (обозначим его как l ): λ результатом является вектор c = lλ a = (lλ λ λ 1 a ,l a 2,l a 3) — вектор, координаты которого получаются умножением на число каждой координаты исходного вектора. Деление вектора на число l означает умно- жение вектора на число 1 l.λ. • Скалярное произведение векторов: результатом является число a × b = 1 a 1 b + a 2 2 b + a 3 3 b — сумма произведений соответствую- щих координат векторов. • Модулем вектора называется корень квадратный из скаляр- ного произведения вектора на самого себя: 2 2 2 | a |= a × a = a 1 + a 2 + a 3 2 2 2 | a |= a × a = a . 1 + a 2 + a 3 • Скалярное произведение векторов может быть вычислено и так: это произведение модулей векторов и на косинус угла между ними, то есть a × b | = a | × | b | ×cos(j ), (ϕ) где через j ϕобозначен угол между векторами a и b . Это соотношение обычно используют æ ç a × b ö для вычисления угла между векторами: jϕ = arcsin ÷ ç ÷ ç ÷ ç . è| a | × | b |÷ø • Единичным вектором a e в направлении вектора a называется век- a тор a e = a . | a , то есть вектор a делится на свой модуль | | | По большому счету вектор — это набор из трех элементов, плюс специфи- ческие правила обработки этих трех элементов «в комплекте». Нам нужно подобрать удачный способ для реализации таких объектов в программном коде. Мы поступим так: для реализации вектора используем класс с по- лем — числовым массивом из трех элементов. Для выполнения основных операций с векторами переопределяем базовые арифметические опера- торы (и еще два не очень арифметических оператора). Обратимся к про- граммному коду в листинге 4.2. Листинг 4.2. Перегрузка арифметических операторов using System; // Класс для реализации векторов: class Vector{ // Массив - для записи координат вектора: public double[] coords; // Конструктор класса (с тремя аргументами): public Vector(double x,double y,double z){ // Создание массива из трех элементов: coords=new double[3]; Перегрузка арифметических операторов и операторов приведения типа 153 // Присваивание значений элементам массива: coords[0]=x; coords[1]=y; coords[2]=z; } // Перегрузка оператора сложения для // вычисления суммы векторов: public static Vector operator+(Vector a,Vector b){ // Создание массива из трех элементов: double[] x=new double[3]; // Присваивание элементам массива значений: for(int i=0;i<3;i++){ x[i]=a.coords[i]+b.coords[i]; // Сумма координат векторов } // Создание нового объекта с вычисленными // параметрами (координатами): Vector res=new Vector(x[0],x[1],x[2]); // Объект возвращается как результат: return res; } // Перегрузка оператора умножения // для вычисления скалярного произведения // векторов: public static double operator*(Vector a,Vector b){ // Локальная переменная с нулевым // начальным значением: double res=0; // Вычисление суммы попарных произведений // координат векторов: for(int i=0;i<3;i++){ res+=a.coords[i]*b.coords[i]; } // Вычисленное значение возвращается как // результат: return res; } // Перегрузка оператора умножения // для вычисления произведения вектора на число: public static Vector operator*(Vector a,double b){ // Создание массива из трех элементов: double[] x=new double[3]; // Вычисление значений элементов массива: for(int i=0;i<3;i++){ x[i]=a.coords[i]*b; } продолжение 154 Глава 4. Перегрузка операторов Листинг 4.2 (продолжение) // Создание объекта с вычисленными параметрами: Vector res=new Vector(x[0],x[1],x[2]); // Объект (ссылка на объект) возвращается // в качестве результата: return res; } // Перегрузка оператора умножения // для вычисления произведения числа на вектор: public static Vector operator*(double b,Vector a){ // То же самое, что произведение // вектора на число. // Используем перегруженный оператор // умножения: return a*b; } // Перегрузка оператора деления // для вычисления результата деления вектора на // число: public static Vector operator/(Vector a,double b){ // Определяем через операцию умножения // вектора на число: return a*(1/b); } // Перегрузка оператора деления для случая, // когда операнды - объекты // класса Vector. В результате вычисляется угол // (в радианах) между // соответствующими векторами: public static double operator/(Vector a,Vector b){ // Локальные переменные для запоминания // косинуса угла и угла: double cosinus,phi; // Вычисление косинуса угла между векторами. // Используем перегруженный оператор // произведения и оператор // явного приведения типа (см. код далее): cosinus=(a*b)/((double)a*(double)b); // Вычисление угла: phi=Math.Acos(cosinus); // Метод возвращает результат: return phi; } // Перегрузка оператора вычитания для // вычисления разности двух векторов: public static Vector operator-(Vector a,Vector b){ // Вычисляем результат с помощью Перегрузка арифметических операторов и операторов приведения типа 155 // перегруженного оператора // сложения (двух векторов) и умножения // (числа на вектор): return a+(-1)*b; } // Перегрузка унарного оператора "минус" // для вектора: public static Vector operator(Vector a){ // Вычисляется как умножение вектора на -1: return (1)*a; } // Перегрузка оператора инкремента для вектора: public static Vector operator++(Vector a){ // К вектору добавляется единичный вектор // того же направления. // Используем перегруженные операторы деления // и приведения типа: a=a+(a/(double)a); // Возвращаем аргумент как результат: return a; } // Перегрузка оператора декремента для вектора: public static Vector operator(Vector a){ // От вектора отнимается единичный вектор // того же направления. // Используем перегруженные операторы // деления и приведения типа: a=a(a/(double)a); // Возвращаем аргумент как результат: return a; } // Перегрузка оператора явного приведения типа. // Объект класса Vector приводится к значению // типа double. // Результатом является модуль // соответствующего вектора: public static explicit operator double(Vector a){ // Результат - корень квадратный из // скалярного произведения // вектора на самого себя: return Math.Sqrt(a*a); } // Перегрузка оператора неявного // приведения типа. // Объект класса Vector приводится к // текстовому значению (тип string): продолжение 156 Глава 4. Перегрузка операторов Листинг 4.2 (продолжение) public static implicit operator string(Vector a){ // Результат - текстовая строка с // координатами вектора: return "<"+a.coords[0]+";"+a.coords[1]+";"+a.coords[2]+">"; } } // Класс с главным методом программы: class VectorDemo{ // Главный метод программы: public static void Main(){ // Объектные переменные: Vector a,b,c; // Числовые переменные: double phi,cosinus,expr; // Первый объект - вектор: a=new Vector(3,0,4); // Второй объект - вектор: b=new Vector(0,6,8); // Угол между векторами: phi=a/b; // Косинус угла между векторами: cosinus=a*b/((double)a*(double)b); // Проверка тригонометрического тождества: expr=Math.Sin(phi)*Math.Sin(phi)+cosinus*cosinus; // Отображаем результат: Console.WriteLine("Проверка: sin(phi)^2+cos(phi)^2="+expr); // Используем оператор инкремента: a++; // Используем оператор декремента: b; // Вычисляем новый вектор: c=-(a*5-b/2); // Проверка результата // (с неявным преобразованием типа): Console.WriteLine("Результат: "+c); // Ожидание нажатия клавиши Enter: Console.ReadLine(); } } Как уже отмечалось, у класса Vector всего одно поле, описанное как double[] coords — полем является переменная числового массива. Соз- дание самого массива и заполнение его числовыми значениями вы- полняется в конструкторе. У конструктора три аргумента. Командой coords=new double[3] в теле конструктора сначала создается массив из Перегрузка арифметических операторов и операторов приведения типа 157 трех элементов (координаты вектора), а затем аргументы конструктора присваиваются элементам массива в качестве значений. Весь остальной код класса — это перегрузка операторов. В частности, мы перегружаем би- нарные операторы сложения (+) и вычитания () так, чтобы соответствую- щие операции можно было выполнять с объектами класса Vector (которые мы отождествляем с векторами). Сразу обращаем внимание читателя на то, что оператор вычитания (знак ) может быть как бинарным, так и унарным. Если с бинарным оператором все более-менее ясно, то унарный оператор — это знак «минус» перед операндом, то есть команда вида a, где a — объект. Обычно такая операция означает умножение на 1. Именно в таком смысле мы и понимаем такую унарную операцию. А еще мы перегружаем оператор умножения (*) так, что в зависимости от операндов, вычисляется скалярное произведение векторов или умножение вектора на число или числа на вектор. Последние две операции коммута- тивны — умножение вектора на число — это то же самое, что и умножение числа на вектор. Кроме этого, можно будет делить вектор на число, а также формально делить вектор на вектор. В последнем случае мы проявили ини- циативу — в математике такая операция недопустима. Мы же определяем ее так, что в результате возвращается значение угла между векторами. Пе- регружаются операторы инкремента и декремента. Мы эти операции ин- терпретируем, соответственно, как добавление к вектору единичного век- тора того же направления и вычитание из вектора единичного вектора того же направления. Вообще, одна и вторая операции довольно бесполезны, но они более-менее соответствуют смыслу, который изначально вкладывался в операторы инкремента и декремента. Культурологическим шоком может стать перегрузка операторов приведе- ния типа (или преобразования типа). Мы до этого вообще не подозревали, что такие операторы существуют. И где-то мы были правы. Однако этот вопрос оставим на десерт, а сейчас вернемся к вещам более прозаическим. Детальнее рассмотрим программный код перечисленных выше оператор- ных методов. ПРИМЕЧАНИЕ В программном коде использовано несколько встроенных математи- ческих функций. Все они являются статическими методами класса Math. Метод Sqrt() предназначен для вычисления квадратного корня из числа, указанного аргументом функции. С помощью метода Sin() вычисляется синус от аргумента метода, а методом Acos() вычисляется арккосинус от аргумента метода. 158 Глава 4. Перегрузка операторов С перегрузкой оператора сложения все достаточно просто. Описывается метод как Vector operator+(Vector a,Vector b), то есть мы имеем дело с двумя операндами класса Vector и результатом — объектом того же клас- са. В теле метода командой double[] x=new double[3] создается локаль- ный массив из трех элементов, а заполняется массив в операторе цикла: индексная переменная i пробегает значение от 0 до 2 включительно, и для каждого фиксированного значения индексной переменной выполняется команда x[i]=a.coords[i]+b.coords[i]. В результате элементы масси- ва x представляют собой суммы соответствующих элементов массивов- полей объектов a и b (операнды в сумме). Создание нового объекта с вычисленными параметрами (координатами) выполняется командой Vector res=new Vector(x[0],x[1],x[2]). Объект res возвращается как ре- зультат операторного метода. Оператор умножения перегружается трижды: для вычисления скалярного произведения векторов, для вычисления произведения вектора на число, и для вычисления произведения числа на вектор. С программной точки зрения произведение объекта на число и числа на объект — совершенно разные операции. Операторный метод перегрузки оператора умножения для вычисления скалярного произведения векторов описывается как double operator*(Ve ctor a,Vector b) — результатом является число типа double, а операнды — объекты класса Vector. В теле операторного метода командой double res=0 объявляется и инициализируется с нулевым начальным значением ло- кальная переменная res. Затем в операторе цикла эта переменная в резуль- тате выполнения команды res+=a.coords[i]*b.coords[i] последовательно увеличивается на попарное произведение координат объектов-операндов. Индексная переменная i пробегает значения от 0 до 2. Вычисленное значе- ние возвращается как результат. Заголовок операторного метода перегрузки оператора умножения для вы- числения произведения вектора на число имеет такой вид: Vector operator* (Vector a,double b). Результатом операции является вектор (объект клас- са Vector). Первый операнд также вектор, а второй операнд — число. В теле метода командой сначала создаем массив x из трех элементов, а затем в опе- раторе цикла заполняем его. Команда x[i]=a.coords[i]*b в теле оператора цикла свидетельствует о том, что элементы массива x получаются умноже- нием соответствующих элементов массива-поля первого операнда (объект a) на второй числовой операнд (число b). Вычисленный в результате мас- сив используется для создания нового объекта класса Vector. Этот объект и возвращается в качестве результата операции. Перегрузка арифметических операторов и операторов приведения типа 159 При перегрузке оператора умножения для вычисления произведения чис- ла на вектор мы применяем маленькую военную хитрость — вызываем пе- регруженный оператор умножения для вычисления произведения вектора на число (инструкция a*b). ПРИМЕЧАНИЕ Другим словами, операторный метод для вычисления произведения числа на вектор возвращает результатом выражение, в котором век- тор умножается на число. А на этот операторный метод перегружен в явном виде. Такой подход не только экономит время и силы, но имеет еще и далеко идущие последствия. Так, если в какой-то момент мы решим изменить правила вычисления произведения вектора на число (и числа на вектор), достаточно будет внести изменения только в операторный метод для вычисления произведения вектора на число. Произведение числа на вектор будет автоматически вычисляться по тем же правилам. Таким же приемом мы воспользовались при перегрузке оператора деления. Результат операторного метода Vector operator/(Vector a,double b) вы- числяется по-военному просто как a*(1/b). Ситуация усложняется, если мы начинаем делить вектор на вектор. Эта операция уже сама по себе является стрессовой. Но мы не теряемся и в теле операторного метода double operator/ (Vector a,Vector b) объявляем две локальные переменные, cosinus и phi, — авось на что сгодятся. Косинус угла между векторами вычисляем командой cosinus=(a*b)/((double)a*(double)b). Это очень загадочная команда. Ин- струкция a*b в числителе является командой вычисления скалярного про- изведения векторов с помощью перегруженного оператора умножения (рас- сматривался выше). Знаменатель представляет собой произведение двух чисел, (double)a и (double)b. Это произведение вычисляется по правилу вычисления самых обычных произведений самых обычных чисел. Причина в том, что значением и выражения (double)a, и выражения (double)b явля- ются числа типа double. Такой чудный эффект достигается благодаря пере- грузке оператора явного приведения объекта класса Vector в значение типа double. Этот метод описан с заголовком explicit operator double(Vector a ). Метод унарный и определяет способ приведения объектов класса Vector (аргумент операторного метода) к значению типа double (в соответствии с ключевым словом double после инструкции operator). Здесь как бы тип ре- зультата стал частью имени операторного метода. Ключевое слово explicit означает, что перегружается оператор явного приведения типа. В качестве результата оператором явного преобразования типа возвраща- ется значение Math.Sqrt(a*a). Это корень квадратный из скалярного про- изведения вектора на самого себя — другими словами, это модуль вектора. Поэтому результатом инструкции (double)a является модуль вектора a, 160 Глава 4. Перегрузка операторов а результатом инструкции (double)b — модуль вектора b соответственно. Результат выражения (a*b)/((double)a*(double)b) — косинус угла между векторами. Сам угол вычисляем командой phi=Math.Acos(cosinus). Приведение типа может быть явным и неявным. Поясним это. Си- туация первая. Есть выражение (назовем это выражение наше_вы- ражение) определенного типа (назовем этот тип наш_тип), а нам очень хочется преобразовать значение этого выражения в значение совершенно другого типа (назовем этот тип другой_тип). Команда такого преобразования будет выглядеть следующим образом: (дру- гой_тип)наше_выражение. Перед выражением в круглых скобках следует указать тот тип, к которому преобразуется значение выраже- ния. Вопрос упирается в то, имеет смысл соответствующая команда преобразования значения нашего_типа в значение другого_типа, или нет. Например, вполне логичным может представиться преоб- разование целого числа в число действительное, но крайне сложно представить, как преобразовать знания в деньги (наоборот, кстати, еще сложнее). Перегружая оператор приведения типа, мы задаем алгоритм, как приводить неприводимое. Ситуация может быть еще более запутанной. Например, пришли мы в банк заплатить кредит, а денег у нас нет. Зато мы очень умные и предлагаем банкирам погасить кредит за счет наших нетривиаль- ных познаний в области программирования. Принимая во внимание высокий социальный статус программистов и безвыходность ситуа- ции, банкиры принимают единственно верное решение — принять заемщика на работу и погашать кредит вычетами из зарплаты (ко- торая значительно выше средней зарплаты по промышленности). Более того, во все местные отделения банка поступает распоряжение: впредь кредиты программистам на C# погашать путем приема оных на работу. Все. Дримз кам тру! Выше был пример неявного приведения типа, когда значение наше- го_типа преобразуется в значение другого_типа без какой-либо явной инструкции. Для базовых типов такие преобразования или заданы, или нет — здесь уж ничего не поделаешь. А вот для классов, которые мы создаем сами, правила преобразования можно задать с помощью операторных методов приведения типа. Оператор неявного преоб- разования описывается с атрибутом implicit вместо explicit. С таким оператором мы еще столкнемся. Разность векторов вычисляется операторным методом Vector operator (Vector a,Vector b) как сумма первого вектора-операнда a со вторым вектором-операндом b, умноженным на 1 (команда a+(-1)*b). Но это еще не все. Здесь оператор «минус» выступает как бинарный. Но он может быть и унарным, когда записывается перед объектом. С математической Перегрузка арифметических операторов и операторов приведения типа 161 точки зрения такая ситуация означает умножение на 1. Именно так ее по- нимаем и мы, когда перегружаем унарный оператор «минус» для вектора: результатом метода с заголовком Vector operator(Vector a) является вы- ражение (1)*a, которое, кстати, вычисляется на основе перегруженного оператора умножения числа на вектор. Операторы инкремента и декремента для объектов класса Vector перегру- жаются практически одинаково — различия минимальны. Результат для оператора инкремента (заголовок метода Vector operator++(Vector a)) вычисляется как a=a+(a/(double)a). К вектору добавляется этот же вектор, деленный на свой модуль, и полученный результат присваивается в каче- стве значения операнду и возвращается как результат. В операторе декре- мента (заголовок операторного метода Vector operator(Vector a)) ре- зультат вычисляется как a=a(a/(double) a). От вектора отнимается этот же вектор, деленный на модуль. Новое значение присваивается операнду и является результатом метода. При перегрузке унарных операторов инкремента и декремента в каче- стве результата возвращается сам операнд. Другими словами, в резуль- тате каждой из этих операций изменяется тот объект, который указан аргументом операторного метода. Однако изменяется он специфиче- ски. Поскольку оба оператора перегружаются одинаковым образом, рассмотрим один из них — например, оператор инкремента. Через a обозначен операнд. Командой a/(double)a создается новый объект, который соответствует вектору a, деленному на свой модуль. Такой век- тор имеет единичную длину и ориентирован в пространстве так же, как исходный вектор a. Далее, в результате выполнения инструкции a+(a/ (double)a) создается еще один новый объект, который соответствует сумме векторов a и a/(double)a. До этих самых пор операнд a (аргу- мент метода) не изменился. В результате выполнения команды a=a+ (a/(double)a) ссылка в переменной a с исходного объекта-аргумента перебрасывается на объект a+(a/(double)a). Внешне иллюзия такая, что мы изменили аргумент операторного метода. На самом деле мы создали новый объект, и на этот новый объект перебросили ссылку в аргументе метода. Можно было поступить иначе: изменить значения полей именно того объекта, который передавался аргументом операторному методу. Стратегически это было бы более верно, но не так красиво. Последний перегруженный оператор — это оператор неявного приведения объекта класса Vector к текстовому значению (тип string). У этого оператор- ного метода довольно хитрый заголовок implicit operator string(Vector a). Этот заголовок по структуре очень похож на заголовок операторного ме- тода явного приведения типа, за исключением того, что конечным типом 162 Глава 4. Перегрузка операторов является string и вместо атрибута explicit использован атрибут implicit. Как уже отмечалось, последний является признаком того, что речь идет о неявном преобразовании типов. Неявное приведение типов будет выполняться каждый раз, когда в том месте, где по логике должно быть текстовое значение, окажется объ- ект класса Vector. Механизм приведения типов представляет собой достаточно эффективное средство программирования. К сожалению, для одной и той же пары типов можно перегрузить только одну форму приведения — или явную, или неявную. В качестве результата при приведении объектов класса Vector к значению типа string возвращается текстовая строка с координатами вектора (ко- манда "<"+a.coords[0]+";"+a.coords[1]+";"+a.coords[2]+">"). С описанием класса Vector мы разобрались. Теперь обратимся к программ- ному коду в главном методе программы. Основное его назначение — про- верить, как вся эта кухня работает. Для этого мы объявляем три объектные переменные, a, b и c, класса Vector. Также объявляются три числовые пере- менные, phi, cosinus и expr, — результаты вычислений нужно куда-то запи- сывать. Затем командами a=new Vector(3,0,4) и b=new Vector(0,6,8) созда- ем два вектора и вычисляем угол межу ними командой phi=a/b. Косинус угла между векторами можно посчитать командой cosinus=a*b/ ((double)a*(double)b). Если все вычисления верны, то значением выражения expr=Math.Sin(phi)*Math.Sin(phi)+cosinus*cosinus должна быть единица. ПРИМЕЧАНИЕ Здесь имеется в виду тригонометрическое тождество 2 2 sin (j ) (ϕ) + cos (j ) = 1 2 2 sin (j ) + cos (j ) (ϕ) = 1 . Командой Console.WriteLine("Проверка: sin(phi)^2+cos(phi)^2="+expr) проверяем результат вычислений. Далее командами a++ и b изменя- ем объекты a и b и на основе новых их значений с помощью команды c= (a*5-b/2) вычисляем новый вектор (переменная c). После этого выпол- няется команда Console.WriteLine("Результат: "+c), в которой объектная переменная c использована вместе с текстовым литералом в аргументе ме- тода WriteLine(). Это как раз тот случай, когда будет выполнено неявное приведение типа (объекта класса Vector к значению типа string). Существует и более надежный способ конвертировать объекты в текстовые значения. Базируется он на переопределении метода ToString(). Но об этом речь будет идти несколько позже. Перегрузка операторов отношений 163 Результат выполнения программы показан на рис. 4.2. Рис. 4.2. Результат выполнения программы с различными операторными методами ПРИМЕЧАНИЕ Выше мы использовали оператор инкремента и декремента. Один из них (оператор инкремента) вызывался в постфиксной форме, а другой (оператор декремента) — в префиксной. В языке C# перегружаются сразу обе формы операторов инкремента и декремента. Другими словами, если оператор инкремента (декремента) перегружен, то его можно использовать как в префиксной, так и в постфиксной форме. Причем обе формы работают одинаково за исключением того, как обрабатывается выражение, содержащее оператор инкремента (де- кремента). Правило здесь простое: если использована префиксная форма оператора, то сначала изменяется операнд (то есть сначала действует оператор), а уже после этого вычисляется значение вы- ражения. Если использована постфиксная форма оператора, то сна- чала вычисляется выражение, а уже после этого изменяется операнд (действует оператор). Перегрузка операторов отношений Нас всех губит отсутствие дерзости в перспективном видении проблем. Мы не можем себе позволить фантази- ровать. «От» и «до», и ни шага в сторону. Вот в чем наша главная ошибка. Из к/ф «Семнадцать мгновений весны» Еще один пример, который мы рассмотрим в этой главе, также касается перегрузки операторов, и в том числе операторов отношений. Особенность операторов отношений состоит в том, что они перегружаются парами: на- пример, если перегружен оператор > (больше), то придется перегрузить и оператор < (меньше). 164 Глава 4. Перегрузка операторов Другие пары: == (равно) и != (не равно), а также <= (меньше или равно) и >= (больше или равно). Причем при перегрузке операторов == и != необходимо также переопределить методы Object.Equals() и Object.GetHashCode(). Эти методы вызываются при сравнении объ- ектов и должны быть синхронизированы с операторами равенства/ неравенства. Однако перегрузкой лишь операторов отношений мы не ограничимся. Мы снова будем перегружать арифметические операторы, но на этот раз несколько иначе. Для перегрузки такого большого набора операторов нам понадобится подходящий объект (в обычном смысле этого слова). И такой объект у нас есть — это комплексное число. Мы опишем специ- альный класс для реализации комплексных чисел и выполнения основ- ных математических операций с этими числами. Некоторые операции имеют общепризнанные математические аналоги. Некоторые мы домыс- лим самостоятельно. Например, комплексные числа можно сравнивать на предмет равно/не равно. Но операции сравнения больше/меньше для комплексных чисел не определены, поскольку не имеют особого матема- тического смысла. Мы устраним эту досадную оплошность. Для сравне- ния комплексных чисел будем использовать модули комплексных чисел: из двух комплексных чисел больше/меньше то, у которого больше/мень- ше модуль. ПРИМЕЧАНИЕ Напомним, что комплексным числом z называется выражение вида z = x + iy. Здесь i — мнимая единица (по определению 2 i = -1), а x и y являются действительными числами и обозначаются как x = Re( z) (действительная часть комплексного числа) и y = Im( z) (мнимая часть комплексного числа). Основные арифметические операции с комплексными числами выполняются так же, как и с действительными, лишь с поправкой на соотношение 2 i = -1. Резуль- татом суммы двух комплексных чисел, z 1 = x 1 + i 1 y и z 2 = x 2 + iy 2, называется число z 1 + z 2 = ( x 1 + x 2) + i( 1 y + y 2) (складывают- ся отдельно действительные и мнимые части комплексных чисел). Аналогично вычисляется разность z 1 - z 2 = ( x 1 - x 2) + i( 1 y - y 2). Произведением двух комплексных чисел называется число z z 1 1 × × z z 2 2== ( x( x 1 1 x 2 2 --1 y y 1 y y 2)2) ++ i( ix( x 2 y 21 1 y++ x x 1 y 1 y 2)2.) Частное комплексных чисел z x x + y y x y - x y вычисляется по формуле 1 1 2 1 2 2 1 1 2 = + i . Комплек- 2 2 2 2 z 2 x 2 + y 2 x 2 + y 2 сно спряженным к числу z = x + iy называется число * z = x - iy. Модулем комплексного числа называется действительное неотрица- тельное число * 2 2 | z |= z × z = x + y . Перегрузка операторов отношений 165 Теперь, когда мы вооружены необходимыми теоретическими познаниями в области комплексных чисел, проанализируем программный код, пред- ставленный в листинге 4.3. Листинг 4.3. Перегрузка операторов сравнения using System; // Класс для реализации комплексных чисел: class Compl{ // Поле - действительная часть // комплексного числа: public double Re; // Поле - мнимая часть комплексного числа: public double Im; // Конструктор класса с двумя аргументами: public Compl(double x,double y){ Re=x; // Действительная часть Im=y; // Мнимая часть } // Конструктор класса с одним аргументом: public Compl(double x):this(x,0){} // Оператор неявного приведения // типа double к типу Compl: public static implicit operator Compl(double a){ // Объект-результат создается // на основе действительного числа: return new Compl(a); } // Оператор явного приведения типа Compl к типу double: public static explicit operator double(Compl a){ // Вычисляется модуль комплексного числа: return Math.Sqrt(a.Re*a.Re+a.Im*a.Im); } // Оператор неявного приведения // типа Compl к типу bool: public static implicit operator bool(Compl a){ if(a.Im==0) return true; // Если число действительное else return false; // Если есть мнимая часть } // Оператор неявного приведения // типа Compl к типу string: public static implicit operator string(Compl a){ // В условном операторе используем // перегруженный оператор неявного // приведения типа Compl к типу bool: продолжение 166 Глава 4. Перегрузка операторов Листинг 4.3 (продолжение) if(a) return ""+a.Re; // Если действительное число else{ // Если нулевая действительная часть: if(a.Re==0) return a.Im+"i"; else return a.Re+((a.Im<0)?"":"+")+a.Im+"i"; // Все прочие // случаи } } // Оператор побитового отрицания // перегружается для вычисления // комплексно-сопряженного числа: public static Compl operator~(Compl a){ // Комплексно-сопряженное число: return new Compl(a.Re,-a.Im); // Меняет знак мнимая часть } // Оператор умножения комплексных чисел: public static Compl operator*(Compl a,Compl b){ // Явно используем правило умножения // комплексных чисел: return new Compl(a.Re*b.Re-a.Im*b.Im,a.Re*b.Im+a.Im*b.Re); } // Оператор деления комплексных чисел: public static Compl operator/(Compl a,Compl b){ // Результат определяем через // перегруженные операторы умножения // комплексных чисел и вычисления комплексно- // сопряженного числа: return a*(~b)*(1/(double)b/(double)b); } // Оператор сложения комплексных чисел: public static Compl operator+(Compl a,Compl b){ // Явно используем правило сложения // комплексных чисел: return new Compl(a.Re+b.Re,a.Im+b.Im); } // Оператор вычитания комплексных чисел: public static Compl operator-(Compl a,Compl b){ // Используем перегруженные операторы // умножения и сложения комплексных чисел: return a+(-1)*b; } // Перегрузка оператора "больше": public static bool operator>(Compl a,Compl b){ // Сравниваются модули комплексных чисел: return (double)a>(double)b; Перегрузка операторов отношений 167 } // Перегрузка оператора "меньше": public static bool operator<(Compl a,Compl b){ // Сравниваются модули комплексных чисел: return (double)a<(double)b; } // Перегрузка оператора "больше или равно": public static bool operator>=(Compl a,Compl b){ // Сравниваются модули комплексных чисел: return (double)a>=(double)b; } // Перегрузка оператора "меньше или равно": public static bool operator<=(Compl a,Compl b){ // Сравниваются модули комплексных чисел: return (double)a<=(double)b; } // Перегрузка оператора "равно": public static bool operator==(Compl a, Compl b){ // Вызывается метод Equals(): return a.Equals(b); } // Перегрузка оператора "не равно": public static bool operator!=(Compl a,Compl b){ // Вызывается метод Equals(): return !a.Equals(b); } // Переопределение метода Equals(): public override bool Equals(Object obj){ Compl b=obj as Compl; // Отдельно сравниваются действительные // и мнимые части чисел: if((Re==b.Re)&(Im==b.Im)) return true; else return false; } // Переопределение метода GetHashCode(): public override int GetHashCode(){ return Re.GetHashCode(); } } // Класс с главным методом программы: class ComplDemo{ // Главный метод программы: public static void Main(){ // Объекты для комплексных чисел: Compl a=new Compl(4,-3); продолжение 168 Глава 4. Перегрузка операторов Листинг 4.3 (продолжение) Compl b=new Compl(-1,2); // Формируем текстовую строку: string str="Арифметические операции:\n"; str+="a+b="+(a+b)+"\na-b="+(a-b)+"\na*b="+(a*b)+ "\na/b="+(a/b)+"\n"; str+="Операции сравнения:\n"; str+="a "\na>=b->"+(a>=b); str+="\na==b->"+(a==b)+"\na!=b->"+(a!=b); // Проверка результатов вычислений: Console.WriteLine(str); // Ожидание нажатия клавиши Enter: Console.ReadLine(); } } Класс для реализации комплексных чисел называется Compl. У класса есть два числовых (типа double) поля: поле Re для записи действительной ча- сти комплексного числа и поле Im для записи мнимой части комплексного числа. Также у класса есть два конструктора: конструктор с двумя аргу- ментами и конструктор с одним аргументом. Если объект создается кон- структором с двумя аргументами, то аргументы конструктора определяют действительную и мнимую части комплексного числа. Если мы использу- ем конструктор с одним аргументом, то этот аргумент определяет действи- тельную часть комплексного числа, а мнимая равна нулю. Инструкция this(x,0) в определении конструктора класса Compl(double x) с одним аргументом означает, что на самом деле в этом случае вызывается конструктор с двумя аргументами — первый совпадает с аргументом конструктора с одним аргументом, а второй нулевой. Этот конструктор будет использоваться нами при перегрузке операторов. Конструктор имеет особое значение в силу простого и очевидного обстоятельства: в математическом плане дей- ствительные числа являются подмножеством множества комплексных чисел. Поэтому, например, действительное число — это частный случай комплексного числа, у которого мнимая часть равна нулю. Мы перегружаем несколько операторов приведения типов — в основном неявного. Решающую роль в нашем деле имеет перегрузка оператора не- явного приведения типа double к типу Compl. Заголовок этого оператора имеет вид implicit operator Compl(double a). Тело операторного метода состоит всего из одной команды return new Compl(a), которой в качестве Перегрузка операторов отношений 169 результата метода возвращается объект класса Coml, созданный с помощью конструктора с одним аргументом — действительным числом. Это имен- но то число, которое преобразуется к типу Compl. Что это нам дает? Если в какой-то команде или выражении в определенном месте вместо операнда типа Compl встретится double-значение, это double-значение будет авто- матически преобразовано в объект класса Compl. Эта ситуация полностью соответствует математической сути проблемы. И этим мы неоднократно воспользуемся при перегрузке арифметических операторов. Также мы определяем обратное преобразование — объекта класса Compl в значение типа double. В этом случае в качестве результата возвраща- ется модуль комплексного числа. Заголовок у оператора explicit oper ator double(Compl a), то есть в данном случае речь идет о явном приве- дении типов. Оператор в качестве результата возвращает значение Math. Sqrt(a.Re*a.Re+a.Im*a.Im). Таким образом, в результате явного приведе- ния типа Compl к типу double в качестве результата возвращается модуль комплексного числа. Операторный метод неявного преобразования типа Compl к типу bool (за- головок метода implicit operator bool(Compl a)) мы определяем так, что для комплексных чисел с нулевой мнимой частью (когда число на самом деле действительное) возвращается значение true. Если мнимая часть от- лична от нуля, возвращается значение false. Перегрузив оператор неявного приведения типа Compl к типу string, мы обеспечиваем удобный механизм преобразования содержимого объекта класса Compl в приемлемый текстовый формат. Под приемлемым форма- том подразумевается общепринятый в математике способ написания ком- плексных чисел. Метод с заголовком implicit operator string(Compl a) имеет немного запутанный код. В условном операторе проверяется, явля- ется ли объект a представлением действительного числа. Это важно, по- скольку в таком случае, очевидно, нет необходимости отображать мни- мую часть. В условном операторе встречается инструкция if(a), которая в обычных условиях не имела бы смысла. В скобках после ключевого слова if должно быть выражение логического типа. Поскольку мы перегрузили оператор неявного приведения к логическому типу, то тут все в порядке. Если у числа мнимая часть нулевая, то это равносильно значению true в условии. В этом случае методом возвращается значение ""+a.Re (к пу- стой текстовой сроке "" дописывается значение поля a.Re). ПРИМЕЧАНИЕ Пустая текстовая строка нам понадобилась для автоматического пре- образования числового значения в текст. Как отмечалось ранее, для преобразования объектов (и числовых переменных) в текст может использоваться метод ToString(). 170 Глава 4. Перегрузка операторов Если число не является действительным, нам нужно проверить, отлична ли от нуля действительная часть этого числа — нулевую действительную часть отображать не принято. Опять используем условный оператор. Если число является чисто мнимым, результатом операторного метода возвра- щается текстовое выражение a.Im+"i" — к мнимой части мы приписыва- ем букву i, обозначающую мнимую единицу. Однако может статься, что и здесь нам не повезло — у числа есть как действительная, так и мнимая части. Тогда актуальным становится вопрос, какого знака мнимая часть. Для положительной мнимой части придется в явном виде вставить знак "+". Такая вставка формируется с помощью тернарного оператора: резуль- татом инструкции ((a.Im<0)?"":"+") является пустая текстовая строка "", если выполнено условие (a.Im<0), и текстовая строка "+" в противном слу- чае. Вся текстовая строка, возвращаемая в качестве результата, определя- ется выражением a.Re+((a.Im<0)?"":"+")+a.Im+"i". Оператор побитового отрицания перегружаем для вычисления комплекс- но-сопряженного числа. Заголовок этого операторного метода имеет вид Compl operator~(Compl a). В качестве результата методом возвращается новый объект new Compl(a.Re,-a.Im), который создается на основе объекта- операнда заменой знака поля Im (мнимая часть числа). Все, что мы рассмотрели выше, — предварительные приготовления. В бой вступаем, переопределяя арифметические операторы. Оператор умножения комплексных чисел описывается с заголовком Compl operator*(Compl a,Compl b), а значением является новый объект, который создается инструкцией new Compl(a.Re*b.Re-a.Im*b.Im,a.Re*b.Im+a.Im*b. Re). Здесь мы фактически в явном виде использовали правило вычисления произведения двух комплексных чисел. По-хорошему стоило бы еще пере- грузить оператор сложения для того, чтобы можно было складывать дей- ствительные числа с комплексными, и наоборот. К счастью, здесь в этом нет необходимости. И все благодаря тому, что мы перегрузили оператор неявного приведения типа double к типу Compl. Поэтому если встретится команда, в которой складывается значение типа double с объектом класса Compl (не важно, в каком порядке), то, поскольку такая операция явно не перегружена, double-аргумент будет автоматически приведен к типу Compl, и дальше команда обрабатывается в соответствии со всеми правилами жанра. Оператор деления комплексных чисел имеет заголовок Compl operator/ (Compl a,Compl b), и его результат вычисляется еще хитрее. Значение опе- ратора вычисляется в виде выражения a*(~b)*(1/(double)b/(double)b). Если подойти к вопросу формально, то результат вычисляется как про- изведение первого операнда на комплексно-спряженный второй операнд и делится на квадрат модуля второго операнда. Причем операция деле- ния на квадрат модуля реализуется как умножение на единицу, деленную Перегрузка операторов отношений 171 на квадрат модуля. При этом следует помнить, что модуль комплексного числа есть число действительное. Таким образом, операция деления двух комплексных чисел сведена к произведению комплексных чисел (два ком- плексных и одно действительное, которое автоматически приводится к формату комплексного числа). А оператор произведения уже перегружен. Здесь мы воспользовались рядом тождеств. Так, если a и b — * * a a × b a × b 1 комплексные числа, то * = = = a × b × . Все как * 2 2 b b × b | b | | b | в жизни — все новое и незнакомое сводится к старому и хорошо известному. Просто обстоят дела с перегрузкой оператора сложения. Заголовок опе- раторного метода имеет вид Compl operator+(Compl a,Compl b), а в каче- стве результата возвращается объект new Compl(a.Re+b.Re,a.Im+b.Im). Как и в случае с оператором произведения, здесь мы в явном виде используем правило (или формулу) — только формулу сложения комплексных чисел. Оператор вычитания комплексных чисел с заголовком Compl operator (Compl a,Compl b) очень прост — тело оператора состоит всего из одной команды return a+(-1)*b. Здесь все просто и очевидно — разность двух комплексных чисел вычисляется как сумма первого числа и второго, умно- женного на 1. Что касается перегрузки операторов сравнения «больше», «меньше», «больше или равно» и «меньше или равно», то соответствующая операция с комплексными числами придумана нами лично. Поэтому перегружа- ем, как хотим. В частности, сводим все к сравнению модулей комплекс- ных чисел. Например, оператор «больше» с заголовком bool operator> (Compl a,Compl b) в качестве результата возвращает значение выражения (double)a>(double)b, которое представляет собой команду сравнения двух действительных чисел и выполняется по классическим канонам. Немного сложнее обстоят дела с перегрузкой операторов «равно» и «не равно». Что касается самого кода перегружаемых операторных методов, то он несложный. Например, оператор «равно» перегружается с заголовком bool operator==(Compl a, Compl b). Как результат возвращается значение выражения a.Equals(b) — из объекта a (первый операнд) вызывается ме- тод Equals() с аргументом b (второй операнд). Оператор «не равно» пере- гружается синхронно: у метода заголовок bool operator!=(Compl a,Compl b), а в качестве значения возвращается выражение !a.Equals(b). Таким об- разом, если один из методов «равно» или «не равно» возвращает значение true, то другой возвращает значение false. В основе перегрузки этих опе- раторов — метод Equals(). Этот метод переопределяется в классе Compl. 172 Глава 4. Перегрузка операторов