
ооп теория
.pdfr10 = r1+r2; r11 = r3+r4; r12 = r5+r6+r10+r11; r1.PrintRational("r1:(0,0)");
r2.PrintRational("r2:(1,1)");
r3.PrintRational("r3:(10,8)");
r4.PrintRational("r4:(2,6)");
r5.PrintRational("r5: (4,-12)"); r6.PrintRational ("r6: (-12,-14)");
r7.PrintRational("r7: (r1+r2)"); r8.PrintRational ("r8: (r3+r4)");
r9.PrintRational("r9: (r5+r6)"); r10.PrintRational ("r10: (r1+r2)");
r11.PrintRational("r11: (r3+r4)"); r12.PrintRational("r12: (r5+r6+r10+r11)");
}
Обратите внимание на вычисление r12: здесь ощутимо видно преимущество операций, позволяющих записывать сложные выражения в простой форме. Результаты вычислений показаны на рис. 16.4.
Аналогичным образом определим остальные операции над рациональными числами:
public Rational Minus(Rational a)
{
int u,v;
u = m*a.n - n*a.m; v= n*a.n;
return( new Rational(u, v));
}//Minus
public static Rational operator -(Rational r1,
Rational r2)
301
{
return (r1.Minus(r2));
}
public Rational Mult(Rational a)
{
int u,v;
u = m*a.m; v= n*a.n; return( new Rational(u, v));
}//Mult
public static Rational operator *(Rational r1, Rational r2)
{
return (r1.Mult(r2));
}
public Rational Divide(Rational a)
{
int u,v;
u = m*a.n; v= n*a.m; return( new Rational(u, v));
}//Divide
public static Rational operator /(Rational r1, Rational r2)
{
return (r1.Divide(r2));
}
Вот тест, проверяющий работу этих операций:
public void TestOperRational()
{
302

Rational |
r1=new |
Rational(1,2), |
r2 |
= |
new |
Rational(1,3); |
|
|
|
|
|
Rational r3, r4, r5, r6 ; |
|
|
|
||
r3 = r1r2; r4 = r1*r2; r5 = |
r1/r2; |
r6 = |
|||
r3+r4*r5; |
|
|
|
|
|
r1.PrintRational("r1: |
|
(1,2)"); |
|||
r2.PrintRational("r2: (1,3)"); |
|
|
|
||
r3.PrintRational("r3: |
|
(r1-r2)"); |
|||
r4.PrintRational("r4: (r1*r2)"); |
|
|
|
r5.PrintRational("r5: (r1/r2)"); r6.PrintRational("r6: (r3+r4*r5)");
}
Результаты работы этого теста показаны на рис. 16.5. Обратите внимание: при перегрузке операций сохраняется общепринятый приоритет операций. Поэтому при вычислении выражения r3+r4*r5 вначале будет выполняться умножение рациональных чисел, а потом уже сложение.
Рис. 16.5. Операции и выражения над рациональными числами
Константы класса Rational
Рассмотрим важную проблему определения констант в собственном классе. Определим две константы 0 и 1 класса Rational. Кажется, что сделать это невозможно из-за ограничений, накладываемых на объявление констант.
303
Напомню, константы должны быть инициализированы в момент объявления,
и их значения должны быть заданы константными выражениями, известными в момент компиляции. Но в момент компиляции у класса Rational нет никаких известных константных выражений. Как же быть? Справиться с проблемой поможет статический конструктор, созданный для решения подобных задач. Роль констант класса будут играть статические поля,
объявленные с атрибутом readonly, то есть доступные только для чтения. Нам также будет полезен закрытый конструктор класса. Еще укажем, что введение констант класса требует использования экзотических средств языка
C#. Вначале определим закрытый конструктор:
private Rational(int a, int b, string t)
{
m = a; n = b;
}
Не забудем, что при перегрузке методов (в данном случае конструкторов) сигнатуры должны различаться, и поэтому пришлось ввести дополнительный аргумент t для избежания конфликтов. Поскольку конструктор закрытый, то гарантируется корректное задание аргументов при его вызове. Определим теперь константы класса, которые, как я уже говорил,
задаются статическими полями с атрибутом readonly:
//Константы класса 0 и 1 - Zero и One
public static readonly Rational Zero, One;
А теперь зададим статический конструктор, в котором определяются
значения констант:
static Rational()
{
304
Console.WriteLine("static constructor Rational"); Zero = new Rational(0, 1, "private");
One = new Rational (1, 1, "private");
}//Статический конструктор
Как это все работает? Статический конструктор вызывается автоматически один раз до начала работы с объектами класса. Он и задаст значения статических полей Zero, One, представляющих рациональные числа с заданным значением. Поскольку эти поля имеют атрибут static и readonly,
то они доступны для всех объектов класса и не изменяются в ходе вычислений, являясь настоящими константами класса. Прежде чем привести пример работы с константами, давайте добавим в наш класс важные булевы операции над рациональными числами - равенство и неравенство, больше и меньше. При этом две последние операции сделаем перегруженными,
позволяя сравнивать рациональные числа с числами типа double:
public static bool operator ==(Rational r1, Rational r2)
{
return((r1.m ==r2.m)&& (r1.n ==r2.n));
}
public static bool operator !=(Rational r1, Rational r2)
{
return((r1.m !=r2.m)|| (r1.n !=r2.n));
}
public static bool operator <(Rational r1, Rational r2)
{
return(r1.m * r2.n < r2.m* r1.n);
}
public static bool operator >(Rational r1, Rational r2)
{
return(r1.m * r2.n > r2.m* r1.n);
}
public static bool operator <(Rational r1, double r2)
{
return((double)r1.m / (double)r1.n < r2);
}
public static bool operator >(Rational r1, double r2)
{
return((double)r1.m / (double)r1.n > r2);
}
305

Наш последний пример демонстрирует работу с константами,
булевыми и арифметическими выражениями над рациональными числами:
public void TestRationalConst()
{
Rational r1 = new Rational(2,8), r2 =new Rational(2,5); Rational r3 = new Rational(4, 10), r4 = new Rational(3,7); Rational r5 = Rational.Zero, r6 = Rational.Zero;
if ((r1 != Rational.Zero) && (r2 == r3))r5 = (r3+Rational.One)*r4;
r6 = Rational.One + Rational.One; r1.PrintRational("r1: (2,8)"); r2.PrintRational ("r2: (2,5)"); r3.PrintRational("r3: (4,10)"); r4.PrintRational("r4: (3,7)"); r5.PrintRational("r5: ((r3 +1)*r4)"); r6.PrintRational("r6: (1+1)");
}
Результаты работы этого примера показаны на рис. 16.6.
Рис. 16.6. Константы и выражения типа Rational
306

Тема 17. СТРУКТУРЫ И ПЕРЕЧИСЛЕНИЯ СОДЕРЖАНИЕ ЛЕКЦИИ:
РАЗВЕРНУТЫЕ И ССЫЛОЧНЫЕ ТИПЫ
o КЛАССЫ И СТРУКТУРЫ
СТРУКТУРЫ
o СИНТАКСИС СТРУКТУР
o КЛАСС RATIONAL ИЛИ СТРУКТУРА RATIONAL
oВСТРОЕННЫЕ СТРУКТУРЫ
ЕЩЕ РАЗ О ДВУХ СЕМАНТИКАХ ПРИСВАИВАНИЯ
ПЕРЕЧИСЛЕНИЯ
o ПЕРСОНЫ И ПРОФЕССИИ
РАЗВЕРНУТЫЕ И ССЫЛОЧНЫЕ ТИПЫ
Рассмотрим объявление объекта класса T с инициализацией:
T x = new T();
Напомню, как выполняется этот оператор. В памяти создается объект типа T,
основанного на классе T, и сущность x связывается с этим объектом.
Сущность, не прошедшая инициализацию (явную или неявную), не связана ни с одним объектом, а потому не может использоваться в вычислениях - у
нее нет полей, хранящих значения, она не может вызывать методы класса.
Объектам нужна память, чтобы с ними можно было работать. Есть две классические стратегии выделения памяти и связывания объекта,
создаваемого в памяти, и сущности, объявленной в тексте.
Определение 1. Класс T относится к развернутому типу, если память отводится сущности x; объект разворачивается на памяти, жестко связанной с сущностью.
307
Определение 2. Класс T относится к ссылочному типу, если память отводится объекту; сущность x является ссылкой на объект.
Для развернутого типа характерно то, что каждая сущность ни с кем не разделяет свою память; сущность жестко связывается со своим объектом. В
этом случае сущность и объект можно и не различать, они становятся неделимым понятием. Для ссылочных типов ситуация иная - несколько сущностей могут ссылаться на один и тот же объект. Такие сущности разделяют память и являются разными именами одного объекта. Полезно понимать разницу между сущностью, заданной ссылкой, и объектом, на который в текущий момент указывает ссылка.
Развернутые и ссылочные типы порождают две различные семантики присваивания - развернутое присваивание и ссылочное присваивание.
Рассмотрим присваивание:
y = x;
Когда сущность y и выражение x принадлежат развернутому типу, то при присваивании изменяется объект. Значения полей объекта, связанного с сущностью y, изменяются, получая значения полей объекта, связанного с x.
Когда сущность y и выражение x принадлежат ссылочному типу, то изменяется ссылка, но не объект. Ссылка y получает значение ссылки x, и обе они после присваивания указывают на один и тот же объект.
Язык программирования должен позволять программисту в момент определения класса указать, к развернутому или ссылочному типу относится класс. К сожалению, язык C# не позволяет этого сделать напрямую - в нем у класса нет модификатора, позволяющего задать развернутый или ссылочный тип. Какие же средства языка позволяют частично решить эту важную задачу? В лекции 3, где рассматривалась система типов языка C#,
308

отмечалось, что все типы языка делятся на ссылочные и значимые. Термин
"значимый" является синонимом термина "развернутый". Беда только в том,
что деление на значимые и ссылочные типы предопределено языком и не управляется программистом. Напомню, к значимым типам относятся все встроенные арифметические типы, булев тип, структуры; к ссылочным типам
- массивы, строки, классы. Так можно ли в C# спроектировать свой собственный класс так, чтобы он относился к значимым типам? Ответ на этот вопрос положительный, хотя и с рядом оговорок. Для того чтобы класс отнести к значимым типам, его нужно реализовать как структуру.
КЛАССЫ И СТРУКТУРЫ
Структура - это частный случай класса. Исторически структуры используются в языках программирования раньше классов. В языках PL/1, C
и Pascal они представляли собой только совокупность данных (полей класса),
но не включали ни методов, ни событий. В языке С++ возможности структур были существенно расширены и они стали настоящими классами, хотя и c
некоторыми ограничениями. В языке C# - наследнике С++ - сохранен именно такой подход к структурам.
Чем следует руководствоваться, делая выбор между структурой и классом?
Полагаю, можно пользоваться следующими правилами:
если необходимо отнести класс к развернутому типу, делайте его структурой;
если у класса число полей относительно невелико, а число возможных объектов относительно велико, делайте его структурой. В этом случае память объектам будет отводиться в стеке, не будут создаваться лишние ссылки, что позволит повысить эффективность работы;
в остальных случаях проектируйте настоящие классы.
309

Поскольку на структуры накладываются дополнительные ограничения, то может возникнуть необходимость в компромиссе - согласиться с ограничениями и использовать структуру либо пожертвовать развернутостью и эффективностью и работать с настоящим классом. Стоит отметить: когда говорится, что все встроенные типы - int и другие - представляют собой классы, то, на самом деле, речь идет о классах, реализованных в виде структур.
Структуры
Рассмотрим теперь более подробно вопросы описания структур, их синтаксиса, семантики и тех особенностей, что отличают их от классов.
Синтаксис структур
Синтаксис объявления структуры аналогичен синтаксису объявления класса:
[атрибуты][модификаторы]struct имя_структуры[:список_интерфейсов] {тело_структуры}
Какие изменения произошли в синтаксисе в сравнении с синтаксисом класса,
описанным в лекции 16? Их немного. Перечислим их:
ключевое слово class изменено на слово struct;
список родителей, который для классов, наряду с именами интерфейсов, мог включать имя родительского класса, заменен списком интерфейсов. Для структур не может быть задан родитель
(класс или структура). Заметьте, структура может наследовать интерфейсы;
для структур неприменимы модификаторы abstract и sealed. Причиной является отсутствие механизма наследования.
310