Проект к данной лекции Вы можете скачать здесь.
Объектно-ориентированное программирование и проектирование построено на классах. Любую программную систему, построенную в объектном стиле, можно рассматривать как совокупность классов, возможно, объединенных в пространства имен, проекты, решения, как это делается при программировании в Visual Studio .Net.
У класса две различные роли: модуля и типа данных. Класс - это модуль, архитектурная единица построения программной системы. Модульность построения - основное свойство программных систем. В ООП программная система, строящаяся по модульному принципу, состоит из классов, являющихся основным видом модуля. Модуль может не представлять собой содержательную единицу; его размер и содержание определяется архитектурными соображениями, а не семантическими. Теоретически можно построить монолитную систему, состоящую из одного модуля, решающую ту же задачу, что и система, состоящая из многих модулей. Практически большую систему, создаваемую коллективом разработчиков, без разделения системы на модули построить не удается. Модульность построения - основное средство борьбы со сложностью системы.
Вторая роль класса не менее важна. Класс - это тип данных, задающий реализацию некоторой абстракции данных, характерной для проблемной области, в интересах которой создается программная система. С этих позиций классы - не просто кирпичики, из которых строится система. Каждый кирпичик теперь имеет важную содержательную начинку. Представьте себе современный дом, построенный из блоков, и дом будущего, где каждый блок выполняет определенную функцию: один следит за температурой, другой - за составом воздуха в доме. ОО-программная система напоминает дом будущего.
Состав класса как типа данных определяется не архитектурными соображениями, а той абстракцией данных, которую должен реализовать класс. Если вы создаете класс Account, реализующий такую абстракцию, как банковский счет, то в этот класс нельзя добавить поля из класса Car, задающего автомобиль. С другой стороны, в этом классе должны быть все поля и все методы, которые необходимы клиентам класса, оперирующим с банковскими счетами.
Объектно-ориентированная разработка программной системы основана на стиле, называемом проектированием от данных. Проектирование системы сводится к поиску абстракций данных, подходящих для конкретной задачи. Каждая из таких абстракций реализуется в виде класса, который и становится модулем - архитектурной единицей построения нашей системы. В основе класса лежит абстрактный тип данных.
В хорошо спроектированной ОО-системе большинство классов играет обе роли, так что каждый модуль системы имеет вполне определенную смысловую нагрузку. Типичная ошибка - рассматривать класс только как архитектурную единицу, объединяя под обложкой класса разнородные поля и функции, после чего становится неясным, какой же тип данных задает этот класс.
В принципе, возможна ситуация, когда класс играет только одну роль - роль модуля. Такие модули обычно, как и типы данных, имеют содержательную начинку. В чем главное отличие этих двух видов классов? Если класс представляет собой тип данных, то можно объявлять объекты этого класса. Как следствие, в процессе работы системы может существовать множество объектов этого класса, каждый со своими свойствами, каждый из них может вызывать методы класса, у каждого могут возникать события. Классы, играющие единственную роль модуля, объектов создавать не могут. Точнее, существует единственный объект этого класса, представляющий модуль. Поля и методы этого модуля доступны клиентам такого класса.
Язык C# допускает как классы, являющиеся типами данных, так и классы, играющие единственную роль модуля. Классы, являющиеся только модулями и предоставляющие свои сервисы другим классам, хорошо знакомы, поскольку появлялись в самых первых программах. К ним относятся, например, такие классы, как Console, Convert, Math.
Ни одна из предыдущих лекций не обходилась без появления классов и обсуждения многих деталей, связанных с ними. Сейчас попробуем сделать некоторые уточнения, подвести итоги и с новых позиций взглянуть на уже знакомые вещи. Начнем с синтаксиса описания класса:
[атрибуты][модификаторы]class имя_класса[:список_родителей]
{тело_класса}
Атрибутам будет посвящена отдельная лекция. Возможными модификаторами в объявлении класса могут быть модификаторы abstract, sealed, о которых подробно будет говориться при рассмотрении наследования, и четыре private и protected - могут быть заданы только для вложенных классов. По умолчанию класс имеет атрибут доступа internal.Чтобы сделать класс доступным не только классам одного проекта, его явно нужно объявить с атрибутом public. Так что в простых случаях объявление класса может выглядеть так:
public class Rational {тело_класса}
В теле класса могут быть объявлены:
Из синтаксиса следует, что классы могут быть вложенными. Такая ситуация довольно редкая. Ее стоит использовать, когда некоторый класс носит вспомогательный характер, разрабатывается в интересах другого класса, и есть полная уверенность, что внутренний класс никому не понадобится, кроме класса, в который он вложен и, возможно, его потомков. Внутренние классы обычно имеют модификатор доступа private или protected.
Основу любого класса, представляющего тип данных, составляют его конструкторы, поля и методы. Поля класса синтаксически являются обычными переменными (объектами) языка. Их описание удовлетворяет обычным правилам объявления переменных.
Содержательно поля задают представление абстракции данных, которую реализует класс. Поля характеризуют свойства объектов класса. Если проектируется класс Car, то поля задают свойства автомобилей, для класса Person поля задают свойства личности.
Напомню, что, когда создается новый объект класса, он содержит набор полей класса. Два объекта одного класса имеют один и тот же набор полей, но разнятся значениями, хранимыми в этих полях. Все объекты класса Person могут иметь поле, характеризующее рост персоны, но один объект может быть высокого роста, другой - низкого, а третий - среднего роста.
К проектированию полей класса нужно подходить весьма тщательно. Полями класса должны быть только необходимые свойства объекта, которые могут понадобиться в одном из проектов, связанных с данным классом. Поля класса существенным образом влияют на память, необходимую программной системе в процессе ее работы. Если проектируется класс Account, задающий банковский счет, то можно включить в его состав поле balance, задающее текущее значение счета. Но тогда сотни тысяч объектов, задающих счета клиентов, будут иметь это поле, и суммарная память, требуемая системе, существенно возрастет. Альтернативой является включение в класс Account метода balance, который будет вычислять баланс, всякий раз, когда тот понадобится. Возрастет объем вычислений, но существенно сократится требуемая память. На этот компромисс между памятью и временем всегда приходится идти при проектировании класса.
Каждое поле имеет модификатор доступа, принимающий одно из четырех значений: public, private, protected, internal. Возможно совместное задание двух атрибутов protected и internal.
Модификатор private является атрибутом доступа по умолчанию. Он закрывает поля от всех других классов, разрешая прямой доступ к ним (чтение и запись) только методам самого класса. Помните, все поля всегда доступны всем методам класса. Они являются для методов класса глобальной информацией, с которой работают все методы, извлекая из полей нужные им данные и изменяя их значения в ходе работы.
Этот модификатор открывает поля классам наследникам. Если класс A объявил некоторое поле с модификатором protected, то методы класса B, который является наследником класса A и, следовательно, наследует поля класса A, могут непосредственно работать с наследуемыми полями.
Этот модификатор открывает поля дружественным классам.
Два класса A и B называются дружественными, если они принадлежат одной сборке - одному проекту.
Если класс A объявил некоторое поле с модификатором internal, то методы дружественного класса B, являющегося клиентом класса A, могут непосредственно работать с таким полем.
Эта комбинация открывает поле тем классам, которые являются либо наследниками, либо дружественными классами. Если требуется более строгое ограничение доступа к полю, чтобы оно было доступно только тем наследникам, которые являются дружественными классами, то сам класс нужно объявить с модификатором internal, а соответствующее поле - с модификатором protected.
Если поля доступны только для методов класса, то они имеют модификатор доступа private, который можно опускать. Такие поля считаются закрытыми, но часто желательно, чтобы некоторые из них были доступны в более широком контексте. Если некоторые поля класса A должны быть доступны для методов класса B, являющегося потомком класса A, то эти поля следует снабдить модификатором protected. Такие поля называются защищенными. Если некоторые поля должны быть доступны для методов классов B1, B2, и так далее, дружественных по отношению к классу A, то эти поля следует снабдить модификатором internal, а все дружественные классы B поместить в один проект ( assembly ). Такие поля называются дружественными. Наконец, если некоторые поля должны быть доступны для методов любого класса B, которому доступен сам класс A, то эти поля следует снабдить модификатором public. Такие поля называются общедоступными или открытыми.
Все процедуры и функции, объявленные в классе, являются методами класса. Их описание удовлетворяет обычным правилам синтаксиса. Методы содержат описания операций, доступных над объектами класса, определяя, тем самым, поведение объектов. Все объекты одного класса имеют один и тот же набор методов.
Каждый метод имеет модификатор доступа, принимающий одно из четырех значений: public, private, protected, internal. private. Независимо от значения private, возможно, опущенный, то тогда они доступны для вызова только внутри методов самого класса. Такие методы считаются закрытыми. Понятно, что класс, у которого все методы закрыты, абсурден, поскольку никто не смог бы вызвать ни один из его методов. Как правило, у класса есть
Если некоторые методы класса A должны быть доступны для вызовов в методах класса B, являющегося потомком класса A, то такие методы следует снабдить атрибутом protected. Если некоторые методы должны быть доступны только для методов классов B1, B2 и так далее, дружественных по отношению к классу A, то такие методы следует снабдить атрибутом internal, а все дружественные классы B поместить в один проект. Наконец, если некоторые методы должны быть доступны для методов любого класса B, которому доступен сам класс A, то такие методы снабжаются атрибутом public.
У класса есть методы, решающие специальные задачи, - конструкторы, методы-свойства, - к рассмотрению которых сейчас и переходим.
Конструктор - неотъемлемый компонент класса. Нет классов, задающих тип данных и не имеющих конструкторов. Конструктор представляет собой специальный метод класса, позволяющий создавать объекты класса. У конструкторов две синтаксические особенности:
Из первого свойства следует, что конструкторы класса представляют собой множество
Если в классе явно не задан ни один конструктор, то к классу автоматически добавляется конструктор по умолчанию - конструктор без аргументов. Заметьте, что если в классе явно определен один или несколько конструкторов, то автоматического добавления конструктора без аргументов не происходит.
Большинство конструкторов класса имеют модификатор доступа public. Помните, что класс создается в интересах своих клиентов. Конструкторы позволяют клиентам класса создавать объекты класса и работать с ними. Конструкторы позволяют не только создать объект класса, но и задать начальные значения полей класса, то есть позволяют создать объект с заданными свойствами. Аргументы конструктора задают значения, позволяющие инициализировать поля класса. Если конструктор по умолчанию, не имеющий аргументов, инициализирует поля класса значениями по умолчанию, то полный конструктор - это такой конструктор, который инициализирует поля класса значениями, переданными в аргументах конструктора. Как правило, у полного конструктора число аргументов совпадает с числом полей класса. Каждый аргумент содержит значение соответствующего поля. Поскольку в момент создания объекта не всегда можно задать полный набор его свойств, класс старается обеспечить клиентам широкий набор конструкторов, позволяющих создать и
инициализировать объект свойствами, известными клиенту в момент создания того или иного объекта.
Как и когда происходит создание объектов? Чаще всего, при объявлении сущности в момент ее инициализации. Давайте обратимся к примеру и рассмотрим создание трех объектов класса Person:
Person pers1 = new Person(), pers2 = new Person();
Person pers3= new Person("Петрова");
Сущности pers1, pers2, pers3 класса Person объявляются с инициализацией, задаваемой унарной операцией new, которой в качестве аргумента передается конструктор класса Person. У класса Person несколько конструкторов - это типичная практика, - отличающихся сигнатурой. В данном примере в первой строке вызывается конструктор без аргументов, во второй строке для сущности pers3 вызывается конструктор с одним аргументом типа string. Разберем в деталях процесс создания.
Первым делом в стеке для сущностей pers создается ссылка, пока висячая со значением null.
Затем в динамической памяти создается объект - структура данных с полями, определяемыми классом Person. Поля объекта инициализируются значениями по умолчанию: ссылочные поля - значением null, арифметические - нулями, строковые - пустой строкой. Эту работу выполняет конструктор по умолчанию, который, можно считать, всегда вызывается в начале процесса создания.
Если поля класса объявлены с инициализацией, то выполняется инициализация полей объекта заданными значениями.
Если вызван конструктор с аргументами, то начинает выполняться тело этого конструктора. Как правило, при этом происходит инициализация тех полей объекта, значения которых переданы конструктору. Так, например, поле fam объекта pers3 получит значение "Петрова".
На заключительном этапе ссылка связывается с созданным объектом.
Реально, процесс создания объектов сложнее из-за проблем наследования. Когда у класса есть родительский класс, поля объекта, наследуемые от родителя, создаются конструктором родителя, который и должен быть вызван в первую очередь. Но об этих тонкостях поговорим при рассмотрении наследования.
Конструктор может быть объявлен с атрибутом private. Понятно, что в этом случае внешний пользователь не может применить его для создания объектов. Но это могут делать методы класса, создавая объекты для собственных нужд со специальной инициализацией. Пример такого конструктора будет дан позже.
Подводя итоги, можно отметить, что объекты создаются динамически в процессе выполнения программы, для создания объекта всегда вызывается тот или иной конструктор класса.
Если задача создания объектов полностью возлагается на программиста, то задача удаления ненужных объектов в Visual Studio .Net снята с программиста и возложена на соответствующий инструментарий - сборщик мусора. В классическом варианте деструктор служит для удаления объектов и освобождения ресурсов, занятых объектом, в первую очередь оперативной памяти.
В языке C# y класса может быть деструктор, но он не занимается удалением объектов и не вызывается нормальным образом в ходе выполнения программы. Деструктор класса, если он есть, вызывается автоматически в процессе сборки мусора. Его роль - в освобождении других ресурсов, например, файлов, открытых объектом. Деструктор C# фактически является финализатором ( finalizer ), с которым мы еще встретимся при обсуждении исключительных ситуаций. Приведу формальное описание Person:
~Person()
{
//Код деструктора
}
Имя деструктора строится из имени класса с предшествующим ему символом ~ (
Методы, называемые свойствами ( Properties ), представляют специальную синтаксическую конструкцию, предназначенную для обеспечения эффективной работы со свойствами. Напомню, правильной стратегией является закрытие полей от клиента - поля объявляются с модификатором protected или private. Клиенты класса не должны использовать информацию о том, как устроены поля. Это облегчает возможную модификацию класса в будущем. Класс сможет изменить представление данных, сохранив интерфейс, предоставляемый клиентам. В этом случае изменения в полях не отразятся на клиентах.
Закрытие полей не означает, что клиенты класса не могут работать с данными, хранящимися в полях класса. Возможны различные стратегии доступа клиента к закрытым полям класса. Перечислю пять наиболее употребительных стратегий:
Read, Write );Read, Write-once );Read-only );Write-only );Not Read, Not Write ).Для эффективной поддержки этих стратегий и введены специальные методы, называемые свойствами. Приведу вначале пример, а потом уточню синтаксис этих методов. Рассмотрим класс Person, у которого пять полей: fam, status, salary, age, , характеризующих, соответственно, фамилию, статус, зарплату, возраст и здоровье персоны. Все поля закрыты для клиента, так что клиент не может непосредственно читать или записывать данные в поля класса. Для каждого из этих полей может быть разумной своя стратегия доступа. При проектировании класса будем предполагать, что возраст доступен для чтения и записи, фамилию можно задать только один раз, статус можно только читать, зарплата недоступна для чтения, а здоровье закрыто для доступа и только специальные методы класса могут сообщать некоторую информацию о здоровье персоны. Вот как на C# можно обеспечить эти стратегии доступа к закрытым полям класса:
/// <summary>
/// Класс, задающий общие свойства
/// и поведение личности
/// </summary>
public class Person
{
public enum Status
{
ребенок, школьник,
студент, работник, пенсионер
}
//поля (все закрыты)
string fam = "", health = "";
int age = 0, salary = 0;
Status status = Status.работник;
//методы - свойства
/// <summary>
///стратегия: Read,Write-once (Чтение, запись при первом обращении)
/// </summary>
public string Fam
{
set { if (fam == "") fam = value; }
get { return (fam); }
}
/// <summary>
///стратегия: Read-only(Только чтение)
/// </summary>
public Status GetStatus
{
get { return (status); }
}
/// <summary>
///стратегия: Read,Write (Чтение, запись)
/// </summary>
public int Age
{
set
{
age = value;
//Изменение статуса
if (age < 7) status = Status.ребенок;
else if (age < 17) status = Status.школьник;
else if (age < 22) status = Status.студент;
else if (age < 65) status = Status.работник;
else status = Status.пенсионер;
}
get { return (age); }
}
/// <summary>
///стратегия: Write-only (Только запись)
/// </summary>
public int Salary
{
set { salary = value; }
}
}
Рассмотрим теперь общий синтаксис методов-свойств. Пусть name - это закрытое свойство. Тогда для него можно определить name. Имя метода обычно близко к имени поля, отличаясь от него, например, только заглавной буквой ( Name ). Тело свойства содержит два метода - get и set, один из которых может быть опущен. Метод get возвращает значение закрытого поля, метод set устанавливает значение, используя значение, передаваемое ему в момент вызова и хранящееся в служебной переменной со стандартным именем value. Поскольку get и set - это обычные процедуры языка, программно можно реализовать сколь угодно сложные стратегии доступа. В нашем примере фамилия меняется, только если ее значение равно пустой строке, и это означает, что фамилия персоны ни разу еще не задавалась. Статус персоны пересчитывается автоматически при всяком изменении возраста, явно изменять его нельзя. Вот пример, показывающий, как некоторый клиент создает поля персоны и работает с ними:
/// <summary>
/// Тестирование свойств объектов
/// класса Person
/// </summary>
public void TestPersonProps()
{
Person pers1 = new Person(),
pers2 = new Person();
pers1.Fam = "Хохлова";
pers1.Age = 21;
ConsolePrintPerson(pers1);
pers2.Fam = "Петров";
pers2.Age = pers1.Age + 1;
ConsolePrintPerson(pers2);
pers1.Fam = "Петрова";
ConsolePrintPerson(pers1);
}//TestPersonProps
/// <summary>
/// Вывод на консоль полей, открытых для чтения
/// </summary>
void ConsolePrintPerson(Person pers)
{
Console.WriteLine("Фам={0}, возраст={1}, статус={2}",
pers.Fam, pers.Age, pers.GetStatus);
}
Заметьте, клиент работает с методами-свойствами так, словно они являются настоящими полями, вызывая их как в правой, так и в левой части оператора присваивания. Заметьте также, что с каждым полем можно работать только в полном соответствии с той стратегией, которую реализует данное свойство. Попытка изменения фамилии не принесет успеха, а изменение возраста приведет и к одновременному изменению статуса.
Свойства являются частным случаем метода класса с особым синтаксисом. Еще одним частным случаем является индексатор. Метод-
Синтаксически объявление get и set приобретают аргументы по числу размерности массива, задающего индексы элемента, значение которого читается или обновляется. Важным ограничением является то, что у класса может быть только один this. Так что если среди полей класса есть несколько массивов, то индексация объектов может быть выполнена только по одному из них.
Добавим в класс Person свойство children, задающее детей персоны, сделаем это свойство закрытым, а доступ к нему обеспечит
const int Child_Max = 20; //максимальное число детей
Person[] children = new Person[Child_Max];
int count_children = 0; //число детей
/// <summary>
/// Метод-свойство
/// </summary>
public int Count_children
{ get { return count_children; } }
/// <summary>
/// Индексатор по массиву children
/// </summary>
/// <param name="i">индекс элемента в массиве</param>
/// <returns>элемент с заданным индексом</returns>
public Person this[int i] //индексатор
{
get
{
if (i >= 0 i < count_children) return (children[i]);
else return (children[0]);
}
set
{
if (i == count_children i < Child_Max)
{ children[i] = value; count_children++; }
}
}
Имя у this, в квадратных скобках в заголовке перечисляются индексы. В методах get, set, обеспечивающих доступ к массиву children, по которому ведется индексирование, анализируется корректность задания индекса. Закрытое поле count_children, хранящее текущее число детей, доступно только для чтения благодаря добавлению соответствующего метода-свойства. Запись в это поле происходит в методе set children добавляется новый элемент.
Протестируем процесс добавления детей персоны и работу
/// <summary>
/// Тест индексатора
/// </summary>
public void TestPersonChildren()
{
Person pers1, pers2, pers3, pers4;
pers1 = new Person();
pers1.Fam = "Петров"; pers1.Age = 42;
pers2 = new Person();
pers2.Fam = pers1.Fam; pers2.Age = 7;
pers1[pers1.Count_children] = pers2;
pers3 = new Person();
pers3.Fam = pers1.Fam + "а"; pers3.Age = 4;
pers1[pers1.Count_children] = pers3;
pers4 = new Person();
pers4.Fam = pers1.Fam; pers4.Age = 2;
pers1[pers1.Count_children] = pers4;
ConsolePrintPerson(pers1);
ConsolePrintPerson(pers1[0]);
ConsolePrintPerson(pers1[1]);
ConsolePrintPerson(pers1[2]);
}
Заметьте, pers1 по соответствующему полю, в данном случае - по полю children.
Еще одним частным случаем являются методы, задающие над объектами классами бинарную или унарную операцию. Введение в класс таких методов позволяет строить выражения, аналогичные арифметическим и булевым выражениям с обычно применяемыми знаками операций и сохранением приоритетов операций. Синтаксис задания таких методов и детали применения опишу чуть позже при проектировании класса рациональных чисел Rational, где введение операций вполне оправдано.
Для полноты картины добавим в класс Person множество конструкторов класса.
//Конструкторы класса
public Person(){}
public Person( string fam)
{
this.fam = fam;
}
public Person(string fam, int age)
{
this.fam = fam; this.age = age;
}
public Person(string fam, int age,
string health, int zarplata, Status s)
{
this.fam = fam; this.age = age; status = s;
this.health = health; salary = zarplata;
}
Заметьте, правильный стиль требует, чтобы среди конструкторов класса всегда присутствовал конструктор по умолчанию - конструктор без аргументов. В нашем случае тело этого конструктора пусто, поскольку все поля объявлены с инициализацией и конструктору нет смысла изменять эти значения. Часто имена this. Обратите внимание, кроме конструктора без аргументов и полного конструктора, заданы конструкторы для частных случаев, когда в момент создания объекта известны только фамилия или фамилия и возраст личности.
В языке C# разрешается объявить класс, который не рассматривается как тип данных и у которого сохраняется единственная роль - роль модуля. Такой класс объявляется с модификатором static. У такого класса могут быть заданы константы, только
Утверждение о том, что для статического класса не создаются объекты, требует уточнения. Один объект создается. Более того, этот new, как это делается для динамически создаваемых объектов. Этот объект получает имя, совпадающее с именем класса. static. Информация в полях статического объекта независимо от public, то поля доступны и клиентам класса. Так, например, константы PI и E из статического класса Math доступны всем клиентам класса Math.
Содержательно, статический класс - это сервисный класс, предоставляющий свои сервисы клиентам класса. Клиенты не создают объекты статического класса, они все работают с одним статическим объектом - модулем, у которого есть полезные для клиентов методы и поля, хранящие общую для всех информацию. Методы класса Math позволяют вычислять математические функции, необходимые всем арифметическим классам, методы класса Convert позволяют осуществлять преобразование одного типа данных в другой, не принадлежат ни одному из этих типов.
Рассмотрим теперь общую для C# ситуацию, когда класс, представляющий тип данных, объявленный без модификатора static, имеет как обычные, так и
Статическим методам класса доступны только
Независимо от того, какой объект вызвал метод, используются одни и те же статические поля, позволяя методу использовать информацию, созданную другими объектами класса. Person может быть статическое поле message, в котором каждый объект может оставить сообщение для других объектов класса.
Напомню, что всякий вызов метода в объектных вычислениях имеет вид x.F(...) ; где x - цель вызова. Обычно целью вызова является динамически созданный объект x, вызывающий обычные методы класса, называемые динамическими или
В заключение этой лекции займемся проектированием класса Rational, описывающего известный в математике тип данных - рациональные числа. По ходу проектирования будут вводиться новые детали, связанные с описанием класса. Начнем проектирование, как обычно, с задания тэга summary, описывающего назначение класса, его свойства и поведение. Вот этот текст:
/// <summary>
/// Класс Rational.
/// определяет новый тип данных - рациональные числа и основные
/// операции над ними - сложение, умножение, вычитание и деление.
/// Рациональное число задается парой целых чисел (m,n) и изображается
/// обычно в виде дроби m/n. Число m называется числителем,
/// n - знаменателем. Для каждого рационального числа существует
/// множество его представлений, например, 1/2, 2/4, 3/6, 6/12.
/// Среди всех представлений
/// можно выделить то, в котором числитель и знаменатель взаимно
/// несократимы. Такой представитель будет храниться в полях класса.
/// </summary>
public class Rational
{
// Описание тела класса Rational
}//Rational
Рациональное число представимо парой целых чисел - m и n. Они и становятся полями класса. Совершенно естественно сделать эти поля закрытыми. Разумная стратегия доступа к ним - "ни чтения, ни записи", поскольку пользователь не должен знать, как представлено рациональное число в классе, и не должен иметь доступа к составляющим рационального числа. Поэтому для таких закрытых полей не будут определяться методы-свойства. Вот объявление полей и констант класса:
//Поля класса. Числитель и знаменатель рационального числа.
const string NONE_EXIST =
"Не существует рационального числа " +
"со знаменателем, равным нулю!";
int m,n;
Зададим два конструктора - конструктор без аргументов, создающий рациональное число 0 и полный конструктор, которому передаются числитель и знаменатель рационального числа. Чуть позже добавим в класс закрытый конструктор и статический конструктор, позволяющий создать константы нашего класса. Вот определение конструкторов:
//Констукторы класса
/// <summary>
/// Конструктор по умолчанию.
/// Создает рациональный 0 - пару (0/1)
/// </summary>
public Rational()
{
m = 0; n = 1;
}
/// <summary>
/// Конструктор класса. Создает рациональное число
/// m/n, эквивалентное a/b, но с взаимно несократимыми
/// числителем и знаменателем.
/// Если b = 0, то выбрасывается исключение,
/// сообщающее о невозможности создать
/// рациональное число со знаменателем 0
/// </summary>
/// <param name="a">числитель</param>
/// <param name="b">знаменатель</param>
public Rational(int a, int b)
{
if (b == 0) throw new RationalException(NONE_EXIST);
//приведение знака
if (b < 0) { b = -b; a = -a; }
//приведение к несократимой дроби
int d = Nod(a, b);
m = a / d; n = b / d;
}
Как видите, конструктор класса может быть довольно сложным. В конструкторе с аргументами проверяется корректность задаваемых аргументов. Если данные заданы корректно, то пара чисел приводится к эквивалентному рациональному числу с несократимыми числителем и знаменателем. По ходу дела вызывается НОД(a,b) - наибольшего общего делителя чисел a и b.
При попытке создать рациональное число со знаменателем 0 выбрасывается исключение, представляющее объект класса RationalException. Это типичная ситуация, когда совместно с проектируемым классом создается и
Если поля класса почти всегда закрываются, чтобы скрыть от пользователя представление данных класса, то методы класса всегда имеют открытую часть - те сервисы (службы), которые класс предоставляет своим клиентам и наследникам. Но не все методы открываются. Большая часть методов класса может быть закрытой, скрывая от клиентов детали реализации, необходимые для внутреннего использования. Заметьте, сокрытие представления и реализации делается не по соображениям утаивания того, как реализована система. Чаще всего, ничто не мешает клиентам ознакомиться с полным текстом класса. Сокрытие делается в интересах самих клиентов. При сопровождении программной системы изменения в ней неизбежны. Клиенты не почувствуют на себе негативные последствия изменений, если они делаются в закрытой части класса. Чем больше закрытая часть класса, тем меньше влияние изменений на клиентов класса.
Метод, вычисляющий наибольший общий делитель пары чисел, понадобится не только конструктору класса, но и всем операциям над рациональными числами. Алгоритм нахождения общего делителя хорошо известен со времен Эвклида. Я приведу программный код метода без особых пояснений:
/// <summary>
/// Закрытый метод класса.
/// Возвращает наибольший общий делитель чисел a,b
/// </summary>
/// <param name="a">первое число</param>
/// <param name="b">второе число</param>
/// <returns>НОД(a,b)</returns>
int Nod(int m, int n)
{
int p=0;
m=Math.Abs(m); n =Math.Abs(n);
do
{
p = m%n; m=n; n=p;
}while (n!=0);
return(m);
}//Nod
Любой класс содержит метод ToString, наследованный от родительского класса object. Переопределим этот метод для класса Rational так, чтобы он задавал строковое представление рационального числа в формате m/n. Тогда печать рациональных чисел ничем не будет отличаться от печати чисел арифметического типа. Вот код метода ToString для класса Rational:
/// <summary>
/// Представление рационального числа
/// в виде строки
/// </summary>
/// <returns>строка в формате m/n</returns>
public override string ToString()
{
return m + "/" + n;
}
В классе Testing, предназначенном для тестирования нашей работы и являющегося клиентом класса Rational, создадим процедуру, позволяющую проверить корректность создания рациональных чисел. Вот эта процедура:
public void TestCreateRational()
{
try
{
Rational r1 = new Rational(-5, -25);
Console.WriteLine("r1 = " + r1.ToString());
Rational r2 = new Rational();
Console.WriteLine("r2 = " + r2.ToString());
Rational r3 = new Rational(4, -8);
Console.WriteLine("r3 = " + r3.ToString());
Rational r4 = new Rational(2, 6);
Console.WriteLine("r4 = " + r4.ToString());
Rational r5 = new Rational(0, 0);
Console.WriteLine("r5 = " + r5.ToString());
}
catch (RationalException e)
{
Console.WriteLine(e.Message);
}
}
Вот как выглядят результаты ее работы:
(рис 1.1) Создание и печать рациональных чисел
Определим над рациональными числами стандартный набор операций - сложение и вычитание, умножение и деление. Реализуем эти операции методами с именами Plus, Minus, Mult, , соответственно. Поскольку рациональные числа - это прежде всего именно числа, то для выполнения операций над ними часто удобнее пользоваться привычными знаками операций ( +, -, *, / ). Язык C# допускает определение операций, заданных указанными символами. Этот процесс называется
Конечно, можно было бы обойтись только перегруженными операциями, но полезно иметь в арсенале оба способа выполнения операций над рациональными числами. Пользователь сам будет решать, какой из способов применять в конкретной ситуации - вызывать метод или операцию.
Покажем вначале реализацию метода Plus и операции +:
public Rational Plus(Rational a)
{
int u, v;
u = m * a.n + n * a.m; v = n * a.n;
return (new Rational(u, v));
}//Plus
public static Rational operator +(Rational r1, Rational r2)
{
return (r1.Plus(r2));
}
Метод Plus реализуется просто. По правилам сложения дробей вычисляется числитель и знаменатель результата, и эти данные становятся аргументами конструктора, создающего требуемое рациональное число, которое удовлетворяет правилам класса.
Операция является статическим методом класса с модификатором static. Именем соответствующего метода является сам знак операции, которому предшествует ключевое слово operator. Важно понимать, что метод Plus является динамическим, + " является статическим методом, частью модуля, связанного с классом Rational.
Аналогичным образом определим остальные операции над рациональными числами:
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)
{
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()
{
Rational r1 = new Rational(1, 2), r2 = new Rational(1, 3);
Rational r3, r4, r5, r6;
r3 = r1 - r2; r4 = r1 * r2; r5 = r1 / r2;
r6 = (r3 + r4) * (r5 - r3)/r4;
Console.WriteLine("r1 = " + r1.ToString());
Console.WriteLine("r2 = " + r2.ToString());
Console.WriteLine("r3 = " + r3.ToString());
Console.WriteLine("r4 = " + r4.ToString());
Console.WriteLine("r5 = " + r5.ToString());
Console.WriteLine("r6 = " + r6.ToString());
}
Результаты работы этого теста показаны на рис. 1.2. Обратите внимание: при
(рис 1.2) Операции и выражения над рациональными числами
Рассмотрим важную проблему определения констант в собственном классе. Определим две константы 0 и 1 класса Rational. Кажется, что сделать это невозможно из-за ограничений, накладываемых на объявление констант. Напомню, константы должны быть инициализированы в момент объявления и их значения должны быть заданы константными выражениями, известными в момент компиляции. Но в момент компиляции у класса Rational нет никаких известных константных выражений. Как же быть? Справиться с проблемой поможет статический конструктор. Нам также будет полезен закрытый конструктор класса. Роль констант класса будут играть
//Константы класса 0 и 1 - Zero и One
public static readonly Rational Zero, One;
А теперь зададим закрытый и статический конструкторы. Статический конструктор будет определять константы класса. Чтобы создать рациональное число, играющее роль константы, статический конструктор вызывает обычный конструктор. Для этого можно было бы воспользоваться имеющимся открытым конструктором, но давайте для полноты картины определим закрытый конструктор.
Не забудем, что при перегрузке t во избежание конфликтов. Поскольку конструктор закрытый, то гарантируется корректное задание аргументов при его вызове:
Rational(int a, int b, string t)
{
m = a; n = b;
}//Закрытый конструктор
static Rational()
{
Zero = new Rational(0, 1, "");
One = new Rational(1, 1, "");
}//Статический конструктор
Как это все работает? Статический конструктор вызывается автоматически один раз до начала работы с объектами класса. Он и задаст значения Zero, One, представляющих рациональные числа с заданным значением. Поскольку эти поля имеют модификатор public 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);
}
public override bool Equals(object obj)
{
return this == (Rational)obj;
}
public override int GetHashCode()
{
return m + n;
}
Определив операции сравнения рациональных чисел, логично переопределить и метод Equals, наследуемый от object, а вслед за ним и метод GetHashCode, завершив тем самым определение собственного класса.
Наш последний пример демонстрирует работу с константами, булевыми и арифметическими выражениями над рациональными числами:
public void TestRationalConst()
{
Rational r1 = new Rational(0, 8), r2 = new Rational(2, 5);
Rational r3 = new Rational(4, 10), r4 = new Rational(3, 7);
Rational r5 = Rational.Zero, r6, r7;
if ((r1 != Rational.Zero) (r2 == r3))
r6 = (r3 + Rational.One) * r4;
else r6 = Rational.One + Rational.One;
if ((r6 > r3) (r4 > 0.5)) r7 = r1;
else r7 = r2;
Console.WriteLine("r1 = " + r1.ToString());
Console.WriteLine("r2 = " + r2.ToString());
Console.WriteLine("r3 = " + r3.ToString());
Console.WriteLine("r4 = " + r4.ToString());
Console.WriteLine("r5 = " + r5.ToString());
Console.WriteLine("r6 = " + r6.ToString());
Console.WriteLine("r7 = " + r7.ToString());
}
Результаты работы этого примера показаны на рис. 1.3
(рис 1.3) Константы и выражения типа Rational
При проектировании класса необходимо предусмотреть возможность возникновения исключительных ситуаций при работе с объектами класса, не дающими возможность нормально продолжить работу. Когда такая ситуация возникает, класс должен выбросить исключение, указывающее на причину возникновения ситуации, и тогда клиент, вызвавший метод, приведший к возникновению ситуации, должен позаботиться о том, чтобы корректно ее обработать и по возможности продолжить работу.
Как правило, при проектировании класса параллельно с ним проектируется и класс, задающий исключительные ситуации. Такой класс должен быть объявлен как наследник класса Exception из RationalException:
/// <summary>
/// Класс, задающий исключения при работе
/// с рациональными числами.
/// </summary>
public class RationalException : Exception
{
public RationalException() { }
public RationalException(string message) :
base(message) { }
public RationalException(string message,
Exception e)
: base(message, e) { }
}
Класс Rational является простым, но хорошим примером проектирования класса. Давайте перечислим основные вопросы, которые приходится решать при создании класса.
Rational эта проблема решается просто, поскольку свойств у рациональных чисел немного и они понятны.Rational естественной является стратегия "ни чтения, ни записи", поэтому такие методы отсутствуют у класса. Рациональное число рассматривается в операциях как неделимое целое, поэтому у клиентов не должно быть доступа к отдельным полям, представляющим части числа.Rational служит хорошим примером. Здесь есть конструктор без параметров, полный конструктор, закрытый конструктор и статический конструктор.Rational.Rational набор таких методов определяется естественным образом. Это методы, выполняющие основные операции над числами - сложение, умножение и другие операции.Object, в первую очередь переопределить метод ToString.Как обычно, для демонстрации примеров данной лекции построено Решение с именем главы Ch3_1. В Решение включены три проекта. Проект DLL с именем ClassSamples содержит три класса - Person, Rational и SRational, служащих примерами описания классов и структур. Структура SRational этой DLL используется в проекте, построенном для примеров следующей лекции этого курса. Проект DLL с именем Geometrical_Figures содержит примеры классов, задающих геометрические фигуры и понятия, используемые в вычислительной геометрии. Эта DLL также будет использоваться в примерах, приводимых в последующих лекции курса. Консольный проект ConsoleWithClasses содержит традиционный класс Testing, большое число методов которого представляют собой различные тесты, иллюстрирующие работу с различными объектами классов из DLL, присоединенных к консольному проекту.
Point, описывающим точку на плоскости, заданную декартовыми и полярными координатами. Среди методов класса Point предусмотреть метод Distance, вычисляющий расстояние до заданной точки. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с классом Point.Point и Line, описывающими точку и линию на плоскости. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL. Среди методов класса Line предусмотреть метод, определяющий принадлежность заданной точки линии.Point и Square, описывающими точку и квадрат на плоскости со сторонами, параллельными осям координат. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL. Среди методов класса Square предусмотреть метод, определяющий принадлежность заданной точки квадрату.Point и Circle, описывающими точку и круг на плоскости. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL. Среди методов класса Circle предусмотреть метод, определяющий принадлежность заданной точки кругу.Point и Rectangle, описывающими точку и прямоугольник на плоскости со сторонами, параллельными осям координат. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL. Среди методов класса Rectangle предусмотреть метод, определяющий принадлежность заданной точки прямоугольнику.Point и Rhomb, описывающими точку и ромб на плоскости с осями, параллельными осям координат. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL. Среди методов класса Rhomb предусмотреть метод, определяющий принадлежность заданной точки ромбу.Point и Tetragon, описывающими точку и четырехугольник на плоскости. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL. Среди методов класса Tetragon предусмотреть метод, определяющий принадлежность заданной точки четырехугольнику.Point и Triangle , описывающими точку и треугольник на плоскости. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL. Среди методов класса Triangle предусмотреть метод, определяющий принадлежность заданной точки треугольнику.Account и Accounts, описывающими счет в банке и множество счетов клиентов, хранящихся в файле. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL.Пусть строка s задает некоторое выражение, значение которого требуется вычислить. Вот несколько примеров возможных выражений:
S1 = 2 +3*5/7; S2 = 2 * sin(x) - (3 + cos(x)) * (x +5); S3 = (x > y) (x < 10) || (y > 5).
Рассмотрим классическую схему решения этой задачи. Первым делом производится разбор выражения - лексический анализ, где выражение преобразуется в последовательность лексем. Затем выражение записывается в виде обратной польской записи, а затем уже вычисляется значение преобразованного выражения.
На этапе
Обратная польская запись выражения - это S1 - S3 в обратной польской записи имеют вид:
CS1 = 2 3 5 * 7 / +; CS2 = 2 x sin * 3 x cos + x 5 + * -; CS3 = x y > x 10 < y 5 > ||;
Рассмотрим возможный алгоритм решения задачи. Пусть результатом stackExpression. queueBackExpression, представляющая queueOperations.
Пока stackExpression не пуст, читается очередная лексема с вершины стека. Затем производится анализ типа этой лексемы и приоритета, если лексема задает операцию. Если лексема - операнд, то она помещается в очередь queueBackExpression. Если лексема - открывающая скобка, то она записывается в очередь queueOperations. Если лексема - закрывающая скобка, то из очереди queueOperations выгружаются все элементы вплоть до открывающей скобки. Эти элементы записываются в выходную очередь queueBackExpression. Знаки скобок в выходной стек не записываются. Если лексема - операция, то анализируется ее приоритет. Если приоритет лексемы выше приоритета последней операции, записанной в queueOperations, то новая операция записывается в очередь queueOperations. В противном случае операция из очереди выгружается и записывается в очередь queueBackExpression. Когда стек stackExpression опустоша
ется, то все операции, оставшиеся в очереди queueOperations, выгружаются и записываются в очередь queueBackExpression.
Очередь queueBackExpression является входом для метода, вычисляющего значение выражения. Для вычисления выражения можно использовать стек stackResult. Алгоритм вычисления значения выражения можно описать следующим образом. Пока очередь queueBackExpression не пуста, читается очередная лексема. Если лексема - операнд, то она записывается в стек stackResult. Если лексема - операция, то из стека stackResult выгружаются предшествующие операции операнды, один или два в зависимости от того, является операция унарной или бинарной. Далее выполняется операция над операндами, и ее результат как операнд записывается в стек stackResult. Если все корректно, то по опустошению очереди на вершине стека stackResult будет операнд, представляющий значение выражения.
В следующих задачах требуется построить классы, поддерживающие процесс трансляции выражений в соответствии с описанным алгоритмом или какой либо его модификацией.
Lexeme, описывающим лексему. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL и позволяющий выполнить лексический анализ текста, задающего программу (фрагмент программы) на языке C#.Lexeme и AriphmeticExpression, описывающими лексему и арифметическое выражение. Выражение строится из чисел, функций, знаков операций и скобок и задается строкой текста. В классе предусмотреть метод, позволяющий вычислить значение выражения. Среди методов класса AriphmeticExpression предусмотреть метод разбора выражения, представляющий выражение в виде последовательности лексем. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL.Lexeme и AriphmeticExpression, описывающими лексему и арифметическое выражение с одной переменной x. Выражение строится из чисел, переменной x, функций, знаков операций и скобок и задается строкой текста. В классе предусмотреть метод, позволяющий вычислить значение выражения. Среди методов класса AriphmeticExpression предусмотреть метод разбора выражения, представляющий выражение в виде последовательности лексем. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL.Lexeme и AriphmeticExpression, описывающими лексему и арифметическое выражение с двумя переменными x и y. Выражение строится из чисел, переменных x и y, функций, знаков операций и скобок и задается строкой текста. В классе предусмотреть метод, позволяющий вычислить значение выражения. Среди методов класса AriphmeticExpression предусмотреть метод разбора выражения, представляющий выражение в виде последовательности лексем. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL.Lexeme и LogicExpression, описывающими лексему и логическое выражение. Выражение строится из констант true и false, отношений, функций, знаков операций и скобок и задается строкой текста. Отношение представляет собой пару арифметических выражений, связанных операцией отношения. В классе предусмотреть метод, позволяющий вычислить значение выражения. Среди методов класса LogicExpression предусмотреть метод, представляющий выражение в виде последовательности лексем. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL.Lexeme и LogicExpression, описывающими лексему и логическое выражение с одной переменной x. Выражение строится из констант true и false, отношений, функций, знаков операций и скобок и задается строкой текста. Отношение представляет собой пару арифметических выражений c переменной x, связанных операцией отношения. В классе предусмотреть метод, позволяющий вычислить значение выражения. Среди методов класса LogicExpression предусмотреть метод, представляющий выражение в виде последовательности лексем. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL.Lexeme и LogicExpression, описывающими лексему и логическое выражение c двумя переменными x и y. Выражение строится из констант true и false, отношений, функций, знаков операций и скобок и задается строкой текста. Отношение представляет собой пару арифметических выражений с переменными x и y, связанных операцией отношения. В классе предусмотреть метод, позволяющий вычислить значение выражения. Среди методов класса LogicExpression предусмотреть метод, представляющий выражение в виде последовательности лексем. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL.Lexeme, Expression и Assignment, описывающими лексему, выражение и оператор присваивания вида z = expression. Выражение в правой части оператора присваивания может быть одним из выражений, заданных в предыдущих задачах этого раздела. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL.Student и Exam, описывающими студента и экзамен. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL.Poisk, содержащего методы линейного и бинарного поиска. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL.Sorting, содержащего методы сортировки. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL.InputArray, позволяющий вводить одномерный массив с проверкой корректности ввода элементов. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL.P1, P2, $$\dots$$ Pn со случайными координатами. Построить множество точек P1, P2, $$\dots$$ Pm ( m <= n ), обход которых позволяет построить многоугольник без пересечений, если это возможно. Уметь вычислять площадь многоугольника, определять, является ли он выпуклым, принадлежит ли заданная точка внутренней области (границам) многоугольника. Построить реализацию в виде класса Polygon, включенного в состав DLL. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с объектами класса Polygon.В практикуме показывается пример создания простой программы в Visual Studio 2010 на языке C#.
Проект к данной лекции Вы можете скачать здесь.
Объектно-ориентированное программирование и проектирование построено на классах. Любую программную систему, построенную в объектном стиле, можно рассматривать как совокупность классов, возможно, объединенных в пространства имен, проекты, решения, как это делается при программировании в Visual Studio .Net.
У класса две различные роли: модуля и типа данных. Класс - это модуль, архитектурная единица построения программной системы. Модульность построения - основное свойство программных систем. В ООП программная система, строящаяся по модульному принципу, состоит из классов, являющихся основным видом модуля. Модуль может не представлять собой содержательную единицу; его размер и содержание определяется архитектурными соображениями, а не семантическими. Теоретически можно построить монолитную систему, состоящую из одного модуля, решающую ту же задачу, что и система, состоящая из многих модулей. Практически большую систему, создаваемую коллективом разработчиков, без разделения системы на модули построить не удается. Модульность построения - основное средство борьбы со сложностью системы.
Вторая роль класса не менее важна. Класс - это тип данных, задающий реализацию некоторой абстракции данных, характерной для проблемной области, в интересах которой создается программная система. С этих позиций классы - не просто кирпичики, из которых строится система. Каждый кирпичик теперь имеет важную содержательную начинку. Представьте себе современный дом, построенный из блоков, и дом будущего, где каждый блок выполняет определенную функцию: один следит за температурой, другой - за составом воздуха в доме. ОО-программная система напоминает дом будущего.
Состав класса как типа данных определяется не архитектурными соображениями, а той абстракцией данных, которую должен реализовать класс. Если вы создаете класс Account, реализующий такую абстракцию, как банковский счет, то в этот класс нельзя добавить поля из класса Car, задающего автомобиль. С другой стороны, в этом классе должны быть все поля и все методы, которые необходимы клиентам класса, оперирующим с банковскими счетами.
Объектно-ориентированная разработка программной системы основана на стиле, называемом проектированием от данных. Проектирование системы сводится к поиску абстракций данных, подходящих для конкретной задачи. Каждая из таких абстракций реализуется в виде класса, который и становится модулем - архитектурной единицей построения нашей системы. В основе класса лежит абстрактный тип данных.
В хорошо спроектированной ОО-системе большинство классов играет обе роли, так что каждый модуль системы имеет вполне определенную смысловую нагрузку. Типичная ошибка - рассматривать класс только как архитектурную единицу, объединяя под обложкой класса разнородные поля и функции, после чего становится неясным, какой же тип данных задает этот класс.
В принципе, возможна ситуация, когда класс играет только одну роль - роль модуля. Такие модули обычно, как и типы данных, имеют содержательную начинку. В чем главное отличие этих двух видов классов? Если класс представляет собой тип данных, то можно объявлять объекты этого класса. Как следствие, в процессе работы системы может существовать множество объектов этого класса, каждый со своими свойствами, каждый из них может вызывать методы класса, у каждого могут возникать события. Классы, играющие единственную роль модуля, объектов создавать не могут. Точнее, существует единственный объект этого класса, представляющий модуль. Поля и методы этого модуля доступны клиентам такого класса.
Язык C# допускает как классы, являющиеся типами данных, так и классы, играющие единственную роль модуля. Классы, являющиеся только модулями и предоставляющие свои сервисы другим классам, хорошо знакомы, поскольку появлялись в самых первых программах. К ним относятся, например, такие классы, как Console, Convert, Math.
Ни одна из предыдущих лекций не обходилась без появления классов и обсуждения многих деталей, связанных с ними. Сейчас попробуем сделать некоторые уточнения, подвести итоги и с новых позиций взглянуть на уже знакомые вещи. Начнем с синтаксиса описания класса:
[атрибуты][модификаторы]class имя_класса[:список_родителей]
{тело_класса}
Атрибутам будет посвящена отдельная лекция. Возможными модификаторами в объявлении класса могут быть модификаторы abstract, sealed, о которых подробно будет говориться при рассмотрении наследования, и четыре private и protected - могут быть заданы только для вложенных классов. По умолчанию класс имеет атрибут доступа internal.Чтобы сделать класс доступным не только классам одного проекта, его явно нужно объявить с атрибутом public. Так что в простых случаях объявление класса может выглядеть так:
public class Rational {тело_класса}
В теле класса могут быть объявлены:
Из синтаксиса следует, что классы могут быть вложенными. Такая ситуация довольно редкая. Ее стоит использовать, когда некоторый класс носит вспомогательный характер, разрабатывается в интересах другого класса, и есть полная уверенность, что внутренний класс никому не понадобится, кроме класса, в который он вложен и, возможно, его потомков. Внутренние классы обычно имеют модификатор доступа private или protected.
Основу любого класса, представляющего тип данных, составляют его конструкторы, поля и методы. Поля класса синтаксически являются обычными переменными (объектами) языка. Их описание удовлетворяет обычным правилам объявления переменных.
Содержательно поля задают представление абстракции данных, которую реализует класс. Поля характеризуют свойства объектов класса. Если проектируется класс Car, то поля задают свойства автомобилей, для класса Person поля задают свойства личности.
Напомню, что, когда создается новый объект класса, он содержит набор полей класса. Два объекта одного класса имеют один и тот же набор полей, но разнятся значениями, хранимыми в этих полях. Все объекты класса Person могут иметь поле, характеризующее рост персоны, но один объект может быть высокого роста, другой - низкого, а третий - среднего роста.
К проектированию полей класса нужно подходить весьма тщательно. Полями класса должны быть только необходимые свойства объекта, которые могут понадобиться в одном из проектов, связанных с данным классом. Поля класса существенным образом влияют на память, необходимую программной системе в процессе ее работы. Если проектируется класс Account, задающий банковский счет, то можно включить в его состав поле balance, задающее текущее значение счета. Но тогда сотни тысяч объектов, задающих счета клиентов, будут иметь это поле, и суммарная память, требуемая системе, существенно возрастет. Альтернативой является включение в класс Account метода balance, который будет вычислять баланс, всякий раз, когда тот понадобится. Возрастет объем вычислений, но существенно сократится требуемая память. На этот компромисс между памятью и временем всегда приходится идти при проектировании класса.
Каждое поле имеет модификатор доступа, принимающий одно из четырех значений: public, private, protected, internal. Возможно совместное задание двух атрибутов protected и internal.
Модификатор private является атрибутом доступа по умолчанию. Он закрывает поля от всех других классов, разрешая прямой доступ к ним (чтение и запись) только методам самого класса. Помните, все поля всегда доступны всем методам класса. Они являются для методов класса глобальной информацией, с которой работают все методы, извлекая из полей нужные им данные и изменяя их значения в ходе работы.
Этот модификатор открывает поля классам наследникам. Если класс A объявил некоторое поле с модификатором protected, то методы класса B, который является наследником класса A и, следовательно, наследует поля класса A, могут непосредственно работать с наследуемыми полями.
Этот модификатор открывает поля дружественным классам.
Два класса A и B называются дружественными, если они принадлежат одной сборке - одному проекту.
Если класс A объявил некоторое поле с модификатором internal, то методы дружественного класса B, являющегося клиентом класса A, могут непосредственно работать с таким полем.
Эта комбинация открывает поле тем классам, которые являются либо наследниками, либо дружественными классами. Если требуется более строгое ограничение доступа к полю, чтобы оно было доступно только тем наследникам, которые являются дружественными классами, то сам класс нужно объявить с модификатором internal, а соответствующее поле - с модификатором protected.
Если поля доступны только для методов класса, то они имеют модификатор доступа private, который можно опускать. Такие поля считаются закрытыми, но часто желательно, чтобы некоторые из них были доступны в более широком контексте. Если некоторые поля класса A должны быть доступны для методов класса B, являющегося потомком класса A, то эти поля следует снабдить модификатором protected. Такие поля называются защищенными. Если некоторые поля должны быть доступны для методов классов B1, B2, и так далее, дружественных по отношению к классу A, то эти поля следует снабдить модификатором internal, а все дружественные классы B поместить в один проект ( assembly ). Такие поля называются дружественными. Наконец, если некоторые поля должны быть доступны для методов любого класса B, которому доступен сам класс A, то эти поля следует снабдить модификатором public. Такие поля называются общедоступными или открытыми.
Все процедуры и функции, объявленные в классе, являются методами класса. Их описание удовлетворяет обычным правилам синтаксиса. Методы содержат описания операций, доступных над объектами класса, определяя, тем самым, поведение объектов. Все объекты одного класса имеют один и тот же набор методов.
Каждый метод имеет модификатор доступа, принимающий одно из четырех значений: public, private, protected, internal. private. Независимо от значения private, возможно, опущенный, то тогда они доступны для вызова только внутри методов самого класса. Такие методы считаются закрытыми. Понятно, что класс, у которого все методы закрыты, абсурден, поскольку никто не смог бы вызвать ни один из его методов. Как правило, у класса есть
Если некоторые методы класса A должны быть доступны для вызовов в методах класса B, являющегося потомком класса A, то такие методы следует снабдить атрибутом protected. Если некоторые методы должны быть доступны только для методов классов B1, B2 и так далее, дружественных по отношению к классу A, то такие методы следует снабдить атрибутом internal, а все дружественные классы B поместить в один проект. Наконец, если некоторые методы должны быть доступны для методов любого класса B, которому доступен сам класс A, то такие методы снабжаются атрибутом public.
У класса есть методы, решающие специальные задачи, - конструкторы, методы-свойства, - к рассмотрению которых сейчас и переходим.
Конструктор - неотъемлемый компонент класса. Нет классов, задающих тип данных и не имеющих конструкторов. Конструктор представляет собой специальный метод класса, позволяющий создавать объекты класса. У конструкторов две синтаксические особенности:
Из первого свойства следует, что конструкторы класса представляют собой множество
Если в классе явно не задан ни один конструктор, то к классу автоматически добавляется конструктор по умолчанию - конструктор без аргументов. Заметьте, что если в классе явно определен один или несколько конструкторов, то автоматического добавления конструктора без аргументов не происходит.
Большинство конструкторов класса имеют модификатор доступа public. Помните, что класс создается в интересах своих клиентов. Конструкторы позволяют клиентам класса создавать объекты класса и работать с ними. Конструкторы позволяют не только создать объект класса, но и задать начальные значения полей класса, то есть позволяют создать объект с заданными свойствами. Аргументы конструктора задают значения, позволяющие инициализировать поля класса. Если конструктор по умолчанию, не имеющий аргументов, инициализирует поля класса значениями по умолчанию, то полный конструктор - это такой конструктор, который инициализирует поля класса значениями, переданными в аргументах конструктора. Как правило, у полного конструктора число аргументов совпадает с числом полей класса. Каждый аргумент содержит значение соответствующего поля. Поскольку в момент создания объекта не всегда можно задать полный набор его свойств, класс старается обеспечить клиентам широкий набор конструкторов, позволяющих создать и
инициализировать объект свойствами, известными клиенту в момент создания того или иного объекта.
Как и когда происходит создание объектов? Чаще всего, при объявлении сущности в момент ее инициализации. Давайте обратимся к примеру и рассмотрим создание трех объектов класса Person:
Person pers1 = new Person(), pers2 = new Person();
Person pers3= new Person("Петрова");
Сущности pers1, pers2, pers3 класса Person объявляются с инициализацией, задаваемой унарной операцией new, которой в качестве аргумента передается конструктор класса Person. У класса Person несколько конструкторов - это типичная практика, - отличающихся сигнатурой. В данном примере в первой строке вызывается конструктор без аргументов, во второй строке для сущности pers3 вызывается конструктор с одним аргументом типа string. Разберем в деталях процесс создания.
Первым делом в стеке для сущностей pers создается ссылка, пока висячая со значением null.
Затем в динамической памяти создается объект - структура данных с полями, определяемыми классом Person. Поля объекта инициализируются значениями по умолчанию: ссылочные поля - значением null, арифметические - нулями, строковые - пустой строкой. Эту работу выполняет конструктор по умолчанию, который, можно считать, всегда вызывается в начале процесса создания.
Если поля класса объявлены с инициализацией, то выполняется инициализация полей объекта заданными значениями.
Если вызван конструктор с аргументами, то начинает выполняться тело этого конструктора. Как правило, при этом происходит инициализация тех полей объекта, значения которых переданы конструктору. Так, например, поле fam объекта pers3 получит значение "Петрова".
На заключительном этапе ссылка связывается с созданным объектом.
Реально, процесс создания объектов сложнее из-за проблем наследования. Когда у класса есть родительский класс, поля объекта, наследуемые от родителя, создаются конструктором родителя, который и должен быть вызван в первую очередь. Но об этих тонкостях поговорим при рассмотрении наследования.
Конструктор может быть объявлен с атрибутом private. Понятно, что в этом случае внешний пользователь не может применить его для создания объектов. Но это могут делать методы класса, создавая объекты для собственных нужд со специальной инициализацией. Пример такого конструктора будет дан позже.
Подводя итоги, можно отметить, что объекты создаются динамически в процессе выполнения программы, для создания объекта всегда вызывается тот или иной конструктор класса.
Если задача создания объектов полностью возлагается на программиста, то задача удаления ненужных объектов в Visual Studio .Net снята с программиста и возложена на соответствующий инструментарий - сборщик мусора. В классическом варианте деструктор служит для удаления объектов и освобождения ресурсов, занятых объектом, в первую очередь оперативной памяти.
В языке C# y класса может быть деструктор, но он не занимается удалением объектов и не вызывается нормальным образом в ходе выполнения программы. Деструктор класса, если он есть, вызывается автоматически в процессе сборки мусора. Его роль - в освобождении других ресурсов, например, файлов, открытых объектом. Деструктор C# фактически является финализатором ( finalizer ), с которым мы еще встретимся при обсуждении исключительных ситуаций. Приведу формальное описание Person:
~Person()
{
//Код деструктора
}
Имя деструктора строится из имени класса с предшествующим ему символом ~ (
Методы, называемые свойствами ( Properties ), представляют специальную синтаксическую конструкцию, предназначенную для обеспечения эффективной работы со свойствами. Напомню, правильной стратегией является закрытие полей от клиента - поля объявляются с модификатором protected или private. Клиенты класса не должны использовать информацию о том, как устроены поля. Это облегчает возможную модификацию класса в будущем. Класс сможет изменить представление данных, сохранив интерфейс, предоставляемый клиентам. В этом случае изменения в полях не отразятся на клиентах.
Закрытие полей не означает, что клиенты класса не могут работать с данными, хранящимися в полях класса. Возможны различные стратегии доступа клиента к закрытым полям класса. Перечислю пять наиболее употребительных стратегий:
Read, Write );Read, Write-once );Read-only );Write-only );Not Read, Not Write ).Для эффективной поддержки этих стратегий и введены специальные методы, называемые свойствами. Приведу вначале пример, а потом уточню синтаксис этих методов. Рассмотрим класс Person, у которого пять полей: fam, status, salary, age, , характеризующих, соответственно, фамилию, статус, зарплату, возраст и здоровье персоны. Все поля закрыты для клиента, так что клиент не может непосредственно читать или записывать данные в поля класса. Для каждого из этих полей может быть разумной своя стратегия доступа. При проектировании класса будем предполагать, что возраст доступен для чтения и записи, фамилию можно задать только один раз, статус можно только читать, зарплата недоступна для чтения, а здоровье закрыто для доступа и только специальные методы класса могут сообщать некоторую информацию о здоровье персоны. Вот как на C# можно обеспечить эти стратегии доступа к закрытым полям класса:
/// <summary>
/// Класс, задающий общие свойства
/// и поведение личности
/// </summary>
public class Person
{
public enum Status
{
ребенок, школьник,
студент, работник, пенсионер
}
//поля (все закрыты)
string fam = "", health = "";
int age = 0, salary = 0;
Status status = Status.работник;
//методы - свойства
/// <summary>
///стратегия: Read,Write-once (Чтение, запись при первом обращении)
/// </summary>
public string Fam
{
set { if (fam == "") fam = value; }
get { return (fam); }
}
/// <summary>
///стратегия: Read-only(Только чтение)
/// </summary>
public Status GetStatus
{
get { return (status); }
}
/// <summary>
///стратегия: Read,Write (Чтение, запись)
/// </summary>
public int Age
{
set
{
age = value;
//Изменение статуса
if (age < 7) status = Status.ребенок;
else if (age < 17) status = Status.школьник;
else if (age < 22) status = Status.студент;
else if (age < 65) status = Status.работник;
else status = Status.пенсионер;
}
get { return (age); }
}
/// <summary>
///стратегия: Write-only (Только запись)
/// </summary>
public int Salary
{
set { salary = value; }
}
}
Рассмотрим теперь общий синтаксис методов-свойств. Пусть name - это закрытое свойство. Тогда для него можно определить name. Имя метода обычно близко к имени поля, отличаясь от него, например, только заглавной буквой ( Name ). Тело свойства содержит два метода - get и set, один из которых может быть опущен. Метод get возвращает значение закрытого поля, метод set устанавливает значение, используя значение, передаваемое ему в момент вызова и хранящееся в служебной переменной со стандартным именем value. Поскольку get и set - это обычные процедуры языка, программно можно реализовать сколь угодно сложные стратегии доступа. В нашем примере фамилия меняется, только если ее значение равно пустой строке, и это означает, что фамилия персоны ни разу еще не задавалась. Статус персоны пересчитывается автоматически при всяком изменении возраста, явно изменять его нельзя. Вот пример, показывающий, как некоторый клиент создает поля персоны и работает с ними:
/// <summary>
/// Тестирование свойств объектов
/// класса Person
/// </summary>
public void TestPersonProps()
{
Person pers1 = new Person(),
pers2 = new Person();
pers1.Fam = "Хохлова";
pers1.Age = 21;
ConsolePrintPerson(pers1);
pers2.Fam = "Петров";
pers2.Age = pers1.Age + 1;
ConsolePrintPerson(pers2);
pers1.Fam = "Петрова";
ConsolePrintPerson(pers1);
}//TestPersonProps
/// <summary>
/// Вывод на консоль полей, открытых для чтения
/// </summary>
void ConsolePrintPerson(Person pers)
{
Console.WriteLine("Фам={0}, возраст={1}, статус={2}",
pers.Fam, pers.Age, pers.GetStatus);
}
Заметьте, клиент работает с методами-свойствами так, словно они являются настоящими полями, вызывая их как в правой, так и в левой части оператора присваивания. Заметьте также, что с каждым полем можно работать только в полном соответствии с той стратегией, которую реализует данное свойство. Попытка изменения фамилии не принесет успеха, а изменение возраста приведет и к одновременному изменению статуса.
Свойства являются частным случаем метода класса с особым синтаксисом. Еще одним частным случаем является индексатор. Метод-
Синтаксически объявление get и set приобретают аргументы по числу размерности массива, задающего индексы элемента, значение которого читается или обновляется. Важным ограничением является то, что у класса может быть только один this. Так что если среди полей класса есть несколько массивов, то индексация объектов может быть выполнена только по одному из них.
Добавим в класс Person свойство children, задающее детей персоны, сделаем это свойство закрытым, а доступ к нему обеспечит
const int Child_Max = 20; //максимальное число детей
Person[] children = new Person[Child_Max];
int count_children = 0; //число детей
/// <summary>
/// Метод-свойство
/// </summary>
public int Count_children
{ get { return count_children; } }
/// <summary>
/// Индексатор по массиву children
/// </summary>
/// <param name="i">индекс элемента в массиве</param>
/// <returns>элемент с заданным индексом</returns>
public Person this[int i] //индексатор
{
get
{
if (i >= 0 i < count_children) return (children[i]);
else return (children[0]);
}
set
{
if (i == count_children i < Child_Max)
{ children[i] = value; count_children++; }
}
}
Имя у this, в квадратных скобках в заголовке перечисляются индексы. В методах get, set, обеспечивающих доступ к массиву children, по которому ведется индексирование, анализируется корректность задания индекса. Закрытое поле count_children, хранящее текущее число детей, доступно только для чтения благодаря добавлению соответствующего метода-свойства. Запись в это поле происходит в методе set children добавляется новый элемент.
Протестируем процесс добавления детей персоны и работу
/// <summary>
/// Тест индексатора
/// </summary>
public void TestPersonChildren()
{
Person pers1, pers2, pers3, pers4;
pers1 = new Person();
pers1.Fam = "Петров"; pers1.Age = 42;
pers2 = new Person();
pers2.Fam = pers1.Fam; pers2.Age = 7;
pers1[pers1.Count_children] = pers2;
pers3 = new Person();
pers3.Fam = pers1.Fam + "а"; pers3.Age = 4;
pers1[pers1.Count_children] = pers3;
pers4 = new Person();
pers4.Fam = pers1.Fam; pers4.Age = 2;
pers1[pers1.Count_children] = pers4;
ConsolePrintPerson(pers1);
ConsolePrintPerson(pers1[0]);
ConsolePrintPerson(pers1[1]);
ConsolePrintPerson(pers1[2]);
}
Заметьте, pers1 по соответствующему полю, в данном случае - по полю children.
Еще одним частным случаем являются методы, задающие над объектами классами бинарную или унарную операцию. Введение в класс таких методов позволяет строить выражения, аналогичные арифметическим и булевым выражениям с обычно применяемыми знаками операций и сохранением приоритетов операций. Синтаксис задания таких методов и детали применения опишу чуть позже при проектировании класса рациональных чисел Rational, где введение операций вполне оправдано.
Для полноты картины добавим в класс Person множество конструкторов класса.
//Конструкторы класса
public Person(){}
public Person( string fam)
{
this.fam = fam;
}
public Person(string fam, int age)
{
this.fam = fam; this.age = age;
}
public Person(string fam, int age,
string health, int zarplata, Status s)
{
this.fam = fam; this.age = age; status = s;
this.health = health; salary = zarplata;
}
Заметьте, правильный стиль требует, чтобы среди конструкторов класса всегда присутствовал конструктор по умолчанию - конструктор без аргументов. В нашем случае тело этого конструктора пусто, поскольку все поля объявлены с инициализацией и конструктору нет смысла изменять эти значения. Часто имена this. Обратите внимание, кроме конструктора без аргументов и полного конструктора, заданы конструкторы для частных случаев, когда в момент создания объекта известны только фамилия или фамилия и возраст личности.
В языке C# разрешается объявить класс, который не рассматривается как тип данных и у которого сохраняется единственная роль - роль модуля. Такой класс объявляется с модификатором static. У такого класса могут быть заданы константы, только
Утверждение о том, что для статического класса не создаются объекты, требует уточнения. Один объект создается. Более того, этот new, как это делается для динамически создаваемых объектов. Этот объект получает имя, совпадающее с именем класса. static. Информация в полях статического объекта независимо от public, то поля доступны и клиентам класса. Так, например, константы PI и E из статического класса Math доступны всем клиентам класса Math.
Содержательно, статический класс - это сервисный класс, предоставляющий свои сервисы клиентам класса. Клиенты не создают объекты статического класса, они все работают с одним статическим объектом - модулем, у которого есть полезные для клиентов методы и поля, хранящие общую для всех информацию. Методы класса Math позволяют вычислять математические функции, необходимые всем арифметическим классам, методы класса Convert позволяют осуществлять преобразование одного типа данных в другой, не принадлежат ни одному из этих типов.
Рассмотрим теперь общую для C# ситуацию, когда класс, представляющий тип данных, объявленный без модификатора static, имеет как обычные, так и
Статическим методам класса доступны только
Независимо от того, какой объект вызвал метод, используются одни и те же статические поля, позволяя методу использовать информацию, созданную другими объектами класса. Person может быть статическое поле message, в котором каждый объект может оставить сообщение для других объектов класса.
Напомню, что всякий вызов метода в объектных вычислениях имеет вид x.F(...) ; где x - цель вызова. Обычно целью вызова является динамически созданный объект x, вызывающий обычные методы класса, называемые динамическими или
В заключение этой лекции займемся проектированием класса Rational, описывающего известный в математике тип данных - рациональные числа. По ходу проектирования будут вводиться новые детали, связанные с описанием класса. Начнем проектирование, как обычно, с задания тэга summary, описывающего назначение класса, его свойства и поведение. Вот этот текст:
/// <summary>
/// Класс Rational.
/// определяет новый тип данных - рациональные числа и основные
/// операции над ними - сложение, умножение, вычитание и деление.
/// Рациональное число задается парой целых чисел (m,n) и изображается
/// обычно в виде дроби m/n. Число m называется числителем,
/// n - знаменателем. Для каждого рационального числа существует
/// множество его представлений, например, 1/2, 2/4, 3/6, 6/12.
/// Среди всех представлений
/// можно выделить то, в котором числитель и знаменатель взаимно
/// несократимы. Такой представитель будет храниться в полях класса.
/// </summary>
public class Rational
{
// Описание тела класса Rational
}//Rational
Рациональное число представимо парой целых чисел - m и n. Они и становятся полями класса. Совершенно естественно сделать эти поля закрытыми. Разумная стратегия доступа к ним - "ни чтения, ни записи", поскольку пользователь не должен знать, как представлено рациональное число в классе, и не должен иметь доступа к составляющим рационального числа. Поэтому для таких закрытых полей не будут определяться методы-свойства. Вот объявление полей и констант класса:
//Поля класса. Числитель и знаменатель рационального числа.
const string NONE_EXIST =
"Не существует рационального числа " +
"со знаменателем, равным нулю!";
int m,n;
Зададим два конструктора - конструктор без аргументов, создающий рациональное число 0 и полный конструктор, которому передаются числитель и знаменатель рационального числа. Чуть позже добавим в класс закрытый конструктор и статический конструктор, позволяющий создать константы нашего класса. Вот определение конструкторов:
//Констукторы класса
/// <summary>
/// Конструктор по умолчанию.
/// Создает рациональный 0 - пару (0/1)
/// </summary>
public Rational()
{
m = 0; n = 1;
}
/// <summary>
/// Конструктор класса. Создает рациональное число
/// m/n, эквивалентное a/b, но с взаимно несократимыми
/// числителем и знаменателем.
/// Если b = 0, то выбрасывается исключение,
/// сообщающее о невозможности создать
/// рациональное число со знаменателем 0
/// </summary>
/// <param name="a">числитель</param>
/// <param name="b">знаменатель</param>
public Rational(int a, int b)
{
if (b == 0) throw new RationalException(NONE_EXIST);
//приведение знака
if (b < 0) { b = -b; a = -a; }
//приведение к несократимой дроби
int d = Nod(a, b);
m = a / d; n = b / d;
}
Как видите, конструктор класса может быть довольно сложным. В конструкторе с аргументами проверяется корректность задаваемых аргументов. Если данные заданы корректно, то пара чисел приводится к эквивалентному рациональному числу с несократимыми числителем и знаменателем. По ходу дела вызывается НОД(a,b) - наибольшего общего делителя чисел a и b.
При попытке создать рациональное число со знаменателем 0 выбрасывается исключение, представляющее объект класса RationalException. Это типичная ситуация, когда совместно с проектируемым классом создается и
Если поля класса почти всегда закрываются, чтобы скрыть от пользователя представление данных класса, то методы класса всегда имеют открытую часть - те сервисы (службы), которые класс предоставляет своим клиентам и наследникам. Но не все методы открываются. Большая часть методов класса может быть закрытой, скрывая от клиентов детали реализации, необходимые для внутреннего использования. Заметьте, сокрытие представления и реализации делается не по соображениям утаивания того, как реализована система. Чаще всего, ничто не мешает клиентам ознакомиться с полным текстом класса. Сокрытие делается в интересах самих клиентов. При сопровождении программной системы изменения в ней неизбежны. Клиенты не почувствуют на себе негативные последствия изменений, если они делаются в закрытой части класса. Чем больше закрытая часть класса, тем меньше влияние изменений на клиентов класса.
Метод, вычисляющий наибольший общий делитель пары чисел, понадобится не только конструктору класса, но и всем операциям над рациональными числами. Алгоритм нахождения общего делителя хорошо известен со времен Эвклида. Я приведу программный код метода без особых пояснений:
/// <summary>
/// Закрытый метод класса.
/// Возвращает наибольший общий делитель чисел a,b
/// </summary>
/// <param name="a">первое число</param>
/// <param name="b">второе число</param>
/// <returns>НОД(a,b)</returns>
int Nod(int m, int n)
{
int p=0;
m=Math.Abs(m); n =Math.Abs(n);
do
{
p = m%n; m=n; n=p;
}while (n!=0);
return(m);
}//Nod
Любой класс содержит метод ToString, наследованный от родительского класса object. Переопределим этот метод для класса Rational так, чтобы он задавал строковое представление рационального числа в формате m/n. Тогда печать рациональных чисел ничем не будет отличаться от печати чисел арифметического типа. Вот код метода ToString для класса Rational:
/// <summary>
/// Представление рационального числа
/// в виде строки
/// </summary>
/// <returns>строка в формате m/n</returns>
public override string ToString()
{
return m + "/" + n;
}
В классе Testing, предназначенном для тестирования нашей работы и являющегося клиентом класса Rational, создадим процедуру, позволяющую проверить корректность создания рациональных чисел. Вот эта процедура:
public void TestCreateRational()
{
try
{
Rational r1 = new Rational(-5, -25);
Console.WriteLine("r1 = " + r1.ToString());
Rational r2 = new Rational();
Console.WriteLine("r2 = " + r2.ToString());
Rational r3 = new Rational(4, -8);
Console.WriteLine("r3 = " + r3.ToString());
Rational r4 = new Rational(2, 6);
Console.WriteLine("r4 = " + r4.ToString());
Rational r5 = new Rational(0, 0);
Console.WriteLine("r5 = " + r5.ToString());
}
catch (RationalException e)
{
Console.WriteLine(e.Message);
}
}
Вот как выглядят результаты ее работы:
(рис 1.1) Создание и печать рациональных чисел
Определим над рациональными числами стандартный набор операций - сложение и вычитание, умножение и деление. Реализуем эти операции методами с именами Plus, Minus, Mult, , соответственно. Поскольку рациональные числа - это прежде всего именно числа, то для выполнения операций над ними часто удобнее пользоваться привычными знаками операций ( +, -, *, / ). Язык C# допускает определение операций, заданных указанными символами. Этот процесс называется
Конечно, можно было бы обойтись только перегруженными операциями, но полезно иметь в арсенале оба способа выполнения операций над рациональными числами. Пользователь сам будет решать, какой из способов применять в конкретной ситуации - вызывать метод или операцию.
Покажем вначале реализацию метода Plus и операции +:
public Rational Plus(Rational a)
{
int u, v;
u = m * a.n + n * a.m; v = n * a.n;
return (new Rational(u, v));
}//Plus
public static Rational operator +(Rational r1, Rational r2)
{
return (r1.Plus(r2));
}
Метод Plus реализуется просто. По правилам сложения дробей вычисляется числитель и знаменатель результата, и эти данные становятся аргументами конструктора, создающего требуемое рациональное число, которое удовлетворяет правилам класса.
Операция является статическим методом класса с модификатором static. Именем соответствующего метода является сам знак операции, которому предшествует ключевое слово operator. Важно понимать, что метод Plus является динамическим, + " является статическим методом, частью модуля, связанного с классом Rational.
Аналогичным образом определим остальные операции над рациональными числами:
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)
{
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()
{
Rational r1 = new Rational(1, 2), r2 = new Rational(1, 3);
Rational r3, r4, r5, r6;
r3 = r1 - r2; r4 = r1 * r2; r5 = r1 / r2;
r6 = (r3 + r4) * (r5 - r3)/r4;
Console.WriteLine("r1 = " + r1.ToString());
Console.WriteLine("r2 = " + r2.ToString());
Console.WriteLine("r3 = " + r3.ToString());
Console.WriteLine("r4 = " + r4.ToString());
Console.WriteLine("r5 = " + r5.ToString());
Console.WriteLine("r6 = " + r6.ToString());
}
Результаты работы этого теста показаны на рис. 1.2. Обратите внимание: при
(рис 1.2) Операции и выражения над рациональными числами
Рассмотрим важную проблему определения констант в собственном классе. Определим две константы 0 и 1 класса Rational. Кажется, что сделать это невозможно из-за ограничений, накладываемых на объявление констант. Напомню, константы должны быть инициализированы в момент объявления и их значения должны быть заданы константными выражениями, известными в момент компиляции. Но в момент компиляции у класса Rational нет никаких известных константных выражений. Как же быть? Справиться с проблемой поможет статический конструктор. Нам также будет полезен закрытый конструктор класса. Роль констант класса будут играть
//Константы класса 0 и 1 - Zero и One
public static readonly Rational Zero, One;
А теперь зададим закрытый и статический конструкторы. Статический конструктор будет определять константы класса. Чтобы создать рациональное число, играющее роль константы, статический конструктор вызывает обычный конструктор. Для этого можно было бы воспользоваться имеющимся открытым конструктором, но давайте для полноты картины определим закрытый конструктор.
Не забудем, что при перегрузке t во избежание конфликтов. Поскольку конструктор закрытый, то гарантируется корректное задание аргументов при его вызове:
Rational(int a, int b, string t)
{
m = a; n = b;
}//Закрытый конструктор
static Rational()
{
Zero = new Rational(0, 1, "");
One = new Rational(1, 1, "");
}//Статический конструктор
Как это все работает? Статический конструктор вызывается автоматически один раз до начала работы с объектами класса. Он и задаст значения Zero, One, представляющих рациональные числа с заданным значением. Поскольку эти поля имеют модификатор public 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);
}
public override bool Equals(object obj)
{
return this == (Rational)obj;
}
public override int GetHashCode()
{
return m + n;
}
Определив операции сравнения рациональных чисел, логично переопределить и метод Equals, наследуемый от object, а вслед за ним и метод GetHashCode, завершив тем самым определение собственного класса.
Наш последний пример демонстрирует работу с константами, булевыми и арифметическими выражениями над рациональными числами:
public void TestRationalConst()
{
Rational r1 = new Rational(0, 8), r2 = new Rational(2, 5);
Rational r3 = new Rational(4, 10), r4 = new Rational(3, 7);
Rational r5 = Rational.Zero, r6, r7;
if ((r1 != Rational.Zero) (r2 == r3))
r6 = (r3 + Rational.One) * r4;
else r6 = Rational.One + Rational.One;
if ((r6 > r3) (r4 > 0.5)) r7 = r1;
else r7 = r2;
Console.WriteLine("r1 = " + r1.ToString());
Console.WriteLine("r2 = " + r2.ToString());
Console.WriteLine("r3 = " + r3.ToString());
Console.WriteLine("r4 = " + r4.ToString());
Console.WriteLine("r5 = " + r5.ToString());
Console.WriteLine("r6 = " + r6.ToString());
Console.WriteLine("r7 = " + r7.ToString());
}
Результаты работы этого примера показаны на рис. 1.3
(рис 1.3) Константы и выражения типа Rational
При проектировании класса необходимо предусмотреть возможность возникновения исключительных ситуаций при работе с объектами класса, не дающими возможность нормально продолжить работу. Когда такая ситуация возникает, класс должен выбросить исключение, указывающее на причину возникновения ситуации, и тогда клиент, вызвавший метод, приведший к возникновению ситуации, должен позаботиться о том, чтобы корректно ее обработать и по возможности продолжить работу.
Как правило, при проектировании класса параллельно с ним проектируется и класс, задающий исключительные ситуации. Такой класс должен быть объявлен как наследник класса Exception из RationalException:
/// <summary>
/// Класс, задающий исключения при работе
/// с рациональными числами.
/// </summary>
public class RationalException : Exception
{
public RationalException() { }
public RationalException(string message) :
base(message) { }
public RationalException(string message,
Exception e)
: base(message, e) { }
}
Класс Rational является простым, но хорошим примером проектирования класса. Давайте перечислим основные вопросы, которые приходится решать при создании класса.
Rational эта проблема решается просто, поскольку свойств у рациональных чисел немного и они понятны.Rational естественной является стратегия "ни чтения, ни записи", поэтому такие методы отсутствуют у класса. Рациональное число рассматривается в операциях как неделимое целое, поэтому у клиентов не должно быть доступа к отдельным полям, представляющим части числа.Rational служит хорошим примером. Здесь есть конструктор без параметров, полный конструктор, закрытый конструктор и статический конструктор.Rational.Rational набор таких методов определяется естественным образом. Это методы, выполняющие основные операции над числами - сложение, умножение и другие операции.Object, в первую очередь переопределить метод ToString.Как обычно, для демонстрации примеров данной лекции построено Решение с именем главы Ch3_1. В Решение включены три проекта. Проект DLL с именем ClassSamples содержит три класса - Person, Rational и SRational, служащих примерами описания классов и структур. Структура SRational этой DLL используется в проекте, построенном для примеров следующей лекции этого курса. Проект DLL с именем Geometrical_Figures содержит примеры классов, задающих геометрические фигуры и понятия, используемые в вычислительной геометрии. Эта DLL также будет использоваться в примерах, приводимых в последующих лекции курса. Консольный проект ConsoleWithClasses содержит традиционный класс Testing, большое число методов которого представляют собой различные тесты, иллюстрирующие работу с различными объектами классов из DLL, присоединенных к консольному проекту.
Point, описывающим точку на плоскости, заданную декартовыми и полярными координатами. Среди методов класса Point предусмотреть метод Distance, вычисляющий расстояние до заданной точки. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с классом Point.Point и Line, описывающими точку и линию на плоскости. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL. Среди методов класса Line предусмотреть метод, определяющий принадлежность заданной точки линии.Point и Square, описывающими точку и квадрат на плоскости со сторонами, параллельными осям координат. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL. Среди методов класса Square предусмотреть метод, определяющий принадлежность заданной точки квадрату.Point и Circle, описывающими точку и круг на плоскости. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL. Среди методов класса Circle предусмотреть метод, определяющий принадлежность заданной точки кругу.Point и Rectangle, описывающими точку и прямоугольник на плоскости со сторонами, параллельными осям координат. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL. Среди методов класса Rectangle предусмотреть метод, определяющий принадлежность заданной точки прямоугольнику.Point и Rhomb, описывающими точку и ромб на плоскости с осями, параллельными осям координат. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL. Среди методов класса Rhomb предусмотреть метод, определяющий принадлежность заданной точки ромбу.Point и Tetragon, описывающими точку и четырехугольник на плоскости. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL. Среди методов класса Tetragon предусмотреть метод, определяющий принадлежность заданной точки четырехугольнику.Point и Triangle , описывающими точку и треугольник на плоскости. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL. Среди методов класса Triangle предусмотреть метод, определяющий принадлежность заданной точки треугольнику.Account и Accounts, описывающими счет в банке и множество счетов клиентов, хранящихся в файле. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL.Пусть строка s задает некоторое выражение, значение которого требуется вычислить. Вот несколько примеров возможных выражений:
S1 = 2 +3*5/7; S2 = 2 * sin(x) - (3 + cos(x)) * (x +5); S3 = (x > y) (x < 10) || (y > 5).
Рассмотрим классическую схему решения этой задачи. Первым делом производится разбор выражения - лексический анализ, где выражение преобразуется в последовательность лексем. Затем выражение записывается в виде обратной польской записи, а затем уже вычисляется значение преобразованного выражения.
На этапе
Обратная польская запись выражения - это S1 - S3 в обратной польской записи имеют вид:
CS1 = 2 3 5 * 7 / +; CS2 = 2 x sin * 3 x cos + x 5 + * -; CS3 = x y > x 10 < y 5 > ||;
Рассмотрим возможный алгоритм решения задачи. Пусть результатом stackExpression. queueBackExpression, представляющая queueOperations.
Пока stackExpression не пуст, читается очередная лексема с вершины стека. Затем производится анализ типа этой лексемы и приоритета, если лексема задает операцию. Если лексема - операнд, то она помещается в очередь queueBackExpression. Если лексема - открывающая скобка, то она записывается в очередь queueOperations. Если лексема - закрывающая скобка, то из очереди queueOperations выгружаются все элементы вплоть до открывающей скобки. Эти элементы записываются в выходную очередь queueBackExpression. Знаки скобок в выходной стек не записываются. Если лексема - операция, то анализируется ее приоритет. Если приоритет лексемы выше приоритета последней операции, записанной в queueOperations, то новая операция записывается в очередь queueOperations. В противном случае операция из очереди выгружается и записывается в очередь queueBackExpression. Когда стек stackExpression опустоша
ется, то все операции, оставшиеся в очереди queueOperations, выгружаются и записываются в очередь queueBackExpression.
Очередь queueBackExpression является входом для метода, вычисляющего значение выражения. Для вычисления выражения можно использовать стек stackResult. Алгоритм вычисления значения выражения можно описать следующим образом. Пока очередь queueBackExpression не пуста, читается очередная лексема. Если лексема - операнд, то она записывается в стек stackResult. Если лексема - операция, то из стека stackResult выгружаются предшествующие операции операнды, один или два в зависимости от того, является операция унарной или бинарной. Далее выполняется операция над операндами, и ее результат как операнд записывается в стек stackResult. Если все корректно, то по опустошению очереди на вершине стека stackResult будет операнд, представляющий значение выражения.
В следующих задачах требуется построить классы, поддерживающие процесс трансляции выражений в соответствии с описанным алгоритмом или какой либо его модификацией.
Lexeme, описывающим лексему. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL и позволяющий выполнить лексический анализ текста, задающего программу (фрагмент программы) на языке C#.Lexeme и AriphmeticExpression, описывающими лексему и арифметическое выражение. Выражение строится из чисел, функций, знаков операций и скобок и задается строкой текста. В классе предусмотреть метод, позволяющий вычислить значение выражения. Среди методов класса AriphmeticExpression предусмотреть метод разбора выражения, представляющий выражение в виде последовательности лексем. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL.Lexeme и AriphmeticExpression, описывающими лексему и арифметическое выражение с одной переменной x. Выражение строится из чисел, переменной x, функций, знаков операций и скобок и задается строкой текста. В классе предусмотреть метод, позволяющий вычислить значение выражения. Среди методов класса AriphmeticExpression предусмотреть метод разбора выражения, представляющий выражение в виде последовательности лексем. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL.Lexeme и AriphmeticExpression, описывающими лексему и арифметическое выражение с двумя переменными x и y. Выражение строится из чисел, переменных x и y, функций, знаков операций и скобок и задается строкой текста. В классе предусмотреть метод, позволяющий вычислить значение выражения. Среди методов класса AriphmeticExpression предусмотреть метод разбора выражения, представляющий выражение в виде последовательности лексем. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL.Lexeme и LogicExpression, описывающими лексему и логическое выражение. Выражение строится из констант true и false, отношений, функций, знаков операций и скобок и задается строкой текста. Отношение представляет собой пару арифметических выражений, связанных операцией отношения. В классе предусмотреть метод, позволяющий вычислить значение выражения. Среди методов класса LogicExpression предусмотреть метод, представляющий выражение в виде последовательности лексем. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL.Lexeme и LogicExpression, описывающими лексему и логическое выражение с одной переменной x. Выражение строится из констант true и false, отношений, функций, знаков операций и скобок и задается строкой текста. Отношение представляет собой пару арифметических выражений c переменной x, связанных операцией отношения. В классе предусмотреть метод, позволяющий вычислить значение выражения. Среди методов класса LogicExpression предусмотреть метод, представляющий выражение в виде последовательности лексем. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL.Lexeme и LogicExpression, описывающими лексему и логическое выражение c двумя переменными x и y. Выражение строится из констант true и false, отношений, функций, знаков операций и скобок и задается строкой текста. Отношение представляет собой пару арифметических выражений с переменными x и y, связанных операцией отношения. В классе предусмотреть метод, позволяющий вычислить значение выражения. Среди методов класса LogicExpression предусмотреть метод, представляющий выражение в виде последовательности лексем. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL.Lexeme, Expression и Assignment, описывающими лексему, выражение и оператор присваивания вида z = expression. Выражение в правой части оператора присваивания может быть одним из выражений, заданных в предыдущих задачах этого раздела. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL.Student и Exam, описывающими студента и экзамен. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL.Poisk, содержащего методы линейного и бинарного поиска. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL.Sorting, содержащего методы сортировки. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL.InputArray, позволяющий вводить одномерный массив с проверкой корректности ввода элементов. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL.P1, P2, $$\dots$$ Pn со случайными координатами. Построить множество точек P1, P2, $$\dots$$ Pm ( m <= n ), обход которых позволяет построить многоугольник без пересечений, если это возможно. Уметь вычислять площадь многоугольника, определять, является ли он выпуклым, принадлежит ли заданная точка внутренней области (границам) многоугольника. Построить реализацию в виде класса Polygon, включенного в состав DLL. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с объектами класса Polygon.В практикуме показывается пример создания простой программы в Visual Studio 2010 на языке C#.
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.