Основы объектного программирования в классах на C# 3.0

Классы

Разбить на страницы
Показывать лекцию целиком

Проект к данной лекции Вы можете скачать здесь.

Классы и ООП

Объектно-ориентированное программирование и проектирование построено на классах. Любую программную систему, построенную в объектном стиле, можно рассматривать как совокупность классов, возможно, объединенных в пространства имен, проекты, решения, как это делается при программировании в 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

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

    Модификатор protected

    Этот модификатор открывает поля классам наследникам. Если класс A объявил некоторое поле с модификатором protected, то методы класса B, который является наследником класса A и, следовательно, наследует поля класса A, могут непосредственно работать с наследуемыми полями.

    Модификатор internal

    Этот модификатор открывает поля дружественным классам. Два класса A и B называются дружественными, если они принадлежат одной сборке - одному проекту. Если класс A объявил некоторое поле с модификатором internal, то методы дружественного класса B, являющегося клиентом класса A, могут непосредственно работать с таким полем.

    Комбинация атрибутов protected и internal

    Эта комбинация открывает поле тем классам, которые являются либо наследниками, либо дружественными классами. Если требуется более строгое ограничение доступа к полю, чтобы оно было доступно только тем наследникам, которые являются дружественными классами, то сам класс нужно объявить с модификатором 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, health, характеризующих, соответственно, фамилию, статус, зарплату, возраст и здоровье персоны. Все поля закрыты для клиента, так что клиент не может непосредственно читать или записывать данные в поля класса. Для каждого из этих полей может быть разумной своя стратегия доступа. При проектировании класса будем предполагать, что возраст доступен для чтения и записи, фамилию можно задать только один раз, статус можно только читать, зарплата недоступна для чтения, а здоровье закрыто для доступа и только специальные методы класса могут сообщать некоторую информацию о здоровье персоны. Вот как на 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

    Для полноты картины добавим в класс 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

    В заключение этой лекции займемся проектированием класса Rational, описывающего известный в математике тип данных - рациональные числа. По ходу проектирования будут вводиться новые детали, связанные с описанием класса. Начнем проектирование, как обычно, с задания тэга summary, описывающего назначение класса, его свойства и поведение. Вот этот текст:

    /// <summary>
    /// Класс Rational.
    /// определяет новый тип данных - рациональные числа и основные
    /// операции над ними - сложение, умножение, вычитание и деление.
    /// Рациональное число задается парой целых чисел (m,n) и изображается
    /// обычно в виде дроби m/n. Число m называется числителем,
    /// n - знаменателем. Для каждого рационального числа существует
    /// множество его представлений, например, 1/2, 2/4, 3/6, 6/12.
    /// Среди всех представлений
    /// можно выделить то, в котором числитель и знаменатель взаимно
    /// несократимы. Такой представитель будет храниться в полях класса.   
    /// </summary>
    public class Rational
    {
          // Описание тела класса Rational
    }//Rational

    Свойства класса Rational

    Рациональное число представимо парой целых чисел - m и n. Они и становятся полями класса. Совершенно естественно сделать эти поля закрытыми. Разумная стратегия доступа к ним - "ни чтения, ни записи", поскольку пользователь не должен знать, как представлено рациональное число в классе, и не должен иметь доступа к составляющим рационального числа. Поэтому для таких закрытых полей не будут определяться методы-свойства. Вот объявление полей и констант класса:

    //Поля класса. Числитель и знаменатель рационального числа.
    const string NONE_EXIST =
         "Не существует рационального  числа " +
         "со знаменателем, равным нулю!";      
    int m,n;

    Конструкторы класса Rational

    Зададим два конструктора - конструктор без аргументов, создающий рациональное число 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. Это типичная ситуация, когда совместно с проектируемым классом создается и класс исключений, характеризующий исключительные ситуации, которые могут возникнуть при работе клиента с объектами класса. Чуть позже приведу объявление этого класса.

    Методы класса Rational

    Если поля класса почти всегда закрываются, чтобы скрыть от пользователя представление данных класса, то методы класса всегда имеют открытую часть - те сервисы (службы), которые класс предоставляет своим клиентам и наследникам. Но не все методы открываются. Большая часть методов класса может быть закрытой, скрывая от клиентов детали реализации, необходимые для внутреннего использования. Заметьте, сокрытие представления и реализации делается не по соображениям утаивания того, как реализована система. Чаще всего, ничто не мешает клиентам ознакомиться с полным текстом класса. Сокрытие делается в интересах самих клиентов. При сопровождении программной системы изменения в ней неизбежны. Клиенты не почувствуют на себе негативные последствия изменений, если они делаются в закрытой части класса. Чем больше закрытая часть класса, тем меньше влияние изменений на клиентов класса.

    Закрытый метод НОД

    Метод, вычисляющий наибольший общий делитель пары чисел, понадобится не только конструктору класса, но и всем операциям над рациональными числами. Алгоритм нахождения общего делителя хорошо известен со времен Эвклида. Я приведу программный код метода без особых пояснений:

    /// <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, Divide, соответственно. Поскольку рациональные числа - это прежде всего именно числа, то для выполнения операций над ними часто удобнее пользоваться привычными знаками операций ( +, -, *, / ). Язык 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) Операции и выражения над рациональными числами

    Константы класса Rational

    Рассмотрим важную проблему определения констант в собственном классе. Определим две константы 0 и 1 класса Rational. Кажется, что сделать это невозможно из-за ограничений, накладываемых на объявление констант. Напомню, константы должны быть инициализированы в момент объявления и их значения должны быть заданы константными выражениями, известными в момент компиляции. Но в момент компиляции у класса Rational нет никаких известных константных выражений. Как же быть? Справиться с проблемой поможет статический конструктор. Нам также будет полезен закрытый конструктор класса. Роль констант класса будут играть статические поля, объявленные с атрибутом readonly, доступные только для чтения.

    //Константы класса 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

    Класс RationalException

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

    Как правило, при проектировании класса параллельно с ним проектируется и класс, задающий исключительные ситуации. Такой класс должен быть объявлен как наследник класса Exception из библиотеки FCL, наследуя тем самым свойства и методы, общие для всех объектов, задающих исключения. Чаще всего, как это имеет быть место и в нашем случае, класс потомок устроен довольно просто и содержит набор конструкторов, вызывающих конструктор родительского класса, передавая ему информацию о возникшей ситуации. Более подробно поговорим об этом в соответствующей лекции, посвященной исключениям, а сейчас приведу код класса 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, присоединенных к консольному проекту.

    Задачи

  • Создать DLL с классом Point, описывающим точку на плоскости, заданную декартовыми и полярными координатами. Среди методов класса Point предусмотреть метод Distance, вычисляющий расстояние до заданной точки. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с классом Point.
  • Создать DLL с классами Point и Line, описывающими точку и линию на плоскости. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL. Среди методов класса Line предусмотреть метод, определяющий принадлежность заданной точки линии.
  • Создать DLL с классами Point и Square, описывающими точку и квадрат на плоскости со сторонами, параллельными осям координат. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL. Среди методов класса Square предусмотреть метод, определяющий принадлежность заданной точки квадрату.
  • Создать DLL с классами Point и Circle, описывающими точку и круг на плоскости. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL. Среди методов класса Circle предусмотреть метод, определяющий принадлежность заданной точки кругу.
  • Создать DLL с классами Point и Rectangle, описывающими точку и прямоугольник на плоскости со сторонами, параллельными осям координат. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL. Среди методов класса Rectangle предусмотреть метод, определяющий принадлежность заданной точки прямоугольнику.
  • Создать DLL с классами Point и Rhomb, описывающими точку и ромб на плоскости с осями, параллельными осям координат. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL. Среди методов класса Rhomb предусмотреть метод, определяющий принадлежность заданной точки ромбу.
  • Создать DLL с классами Point и Tetragon, описывающими точку и четырехугольник на плоскости. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL. Среди методов класса Tetragon предусмотреть метод, определяющий принадлежность заданной точки четырехугольнику.
  • Создать DLL с классами Point и Triangle, описывающими точку и треугольник на плоскости. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL. Среди методов класса Triangle предусмотреть метод, определяющий принадлежность заданной точки треугольнику.
  • Создать DLL с классами 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 не пуст, читается очередная лексема Lex с вершины стека. Затем производится анализ типа этой лексемы и приоритета, если лексема задает операцию. Если лексема - операнд, то она помещается в очередь queueBackExpression. Если лексема - открывающая скобка, то она записывается в очередь queueOperations. Если лексема - закрывающая скобка, то из очереди queueOperations выгружаются все элементы вплоть до открывающей скобки. Эти элементы записываются в выходную очередь queueBackExpression. Знаки скобок в выходной стек не записываются. Если лексема - операция, то анализируется ее приоритет. Если приоритет лексемы выше приоритета последней операции, записанной в queueOperations, то новая операция записывается в очередь queueOperations. В противном случае операция из очереди выгружается и записывается в очередь queueBackExpression. Когда стек stackExpression опустоша ется, то все операции, оставшиеся в очереди queueOperations, выгружаются и записываются в очередь queueBackExpression.

    Очередь queueBackExpression является входом для метода, вычисляющего значение выражения. Для вычисления выражения можно использовать стек stackResult. Алгоритм вычисления значения выражения можно описать следующим образом. Пока очередь queueBackExpression не пуста, читается очередная лексема. Если лексема - операнд, то она записывается в стек stackResult. Если лексема - операция, то из стека stackResult выгружаются предшествующие операции операнды, один или два в зависимости от того, является операция унарной или бинарной. Далее выполняется операция над операндами, и ее результат как операнд записывается в стек stackResult. Если все корректно, то по опустошению очереди на вершине стека stackResult будет операнд, представляющий значение выражения.

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

    Задачи на трансляцию выражений

  • Создать DLL с классом Lexeme, описывающим лексему. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL и позволяющий выполнить лексический анализ текста, задающего программу (фрагмент программы) на языке C#.
  • (*) Создать DLL с классами Lexeme и AriphmeticExpression, описывающими лексему и арифметическое выражение. Выражение строится из чисел, функций, знаков операций и скобок и задается строкой текста. В классе предусмотреть метод, позволяющий вычислить значение выражения. Среди методов класса AriphmeticExpression предусмотреть метод разбора выражения, представляющий выражение в виде последовательности лексем. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL.
  • (*) Создать DLL с классами Lexeme и AriphmeticExpression, описывающими лексему и арифметическое выражение с одной переменной x. Выражение строится из чисел, переменной x, функций, знаков операций и скобок и задается строкой текста. В классе предусмотреть метод, позволяющий вычислить значение выражения. Среди методов класса AriphmeticExpression предусмотреть метод разбора выражения, представляющий выражение в виде последовательности лексем. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL.
  • (*) Создать DLL с классами Lexeme и AriphmeticExpression, описывающими лексему и арифметическое выражение с двумя переменными x и y. Выражение строится из чисел, переменных x и y, функций, знаков операций и скобок и задается строкой текста. В классе предусмотреть метод, позволяющий вычислить значение выражения. Среди методов класса AriphmeticExpression предусмотреть метод разбора выражения, представляющий выражение в виде последовательности лексем. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL.
  • (**) Создать DLL с классами Lexeme и LogicExpression, описывающими лексему и логическое выражение. Выражение строится из констант true и false, отношений, функций, знаков операций и скобок и задается строкой текста. Отношение представляет собой пару арифметических выражений, связанных операцией отношения. В классе предусмотреть метод, позволяющий вычислить значение выражения. Среди методов класса LogicExpression предусмотреть метод, представляющий выражение в виде последовательности лексем. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL.
  • (**) Создать DLL с классами Lexeme и LogicExpression, описывающими лексему и логическое выражение с одной переменной x. Выражение строится из констант true и false, отношений, функций, знаков операций и скобок и задается строкой текста. Отношение представляет собой пару арифметических выражений c переменной x, связанных операцией отношения. В классе предусмотреть метод, позволяющий вычислить значение выражения. Среди методов класса LogicExpression предусмотреть метод, представляющий выражение в виде последовательности лексем. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL.
  • (**) Создать DLL с классами Lexeme и LogicExpression, описывающими лексему и логическое выражение c двумя переменными x и y. Выражение строится из констант true и false, отношений, функций, знаков операций и скобок и задается строкой текста. Отношение представляет собой пару арифметических выражений с переменными x и y, связанных операцией отношения. В классе предусмотреть метод, позволяющий вычислить значение выражения. Среди методов класса LogicExpression предусмотреть метод, представляющий выражение в виде последовательности лексем. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL.
  • (**) Создать DLL с классами Lexeme, Expression и Assignment, описывающими лексему, выражение и оператор присваивания вида z = expression. Выражение в правой части оператора присваивания может быть одним из выражений, заданных в предыдущих задачах этого раздела. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL.
  • Создать DLL с классами Student и Exam, описывающими студента и экзамен. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL.
  • Создать DLL с сервисным классом Poisk, содержащего методы линейного и бинарного поиска. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL.
  • Создать DLL с сервисным классом Sorting, содержащего методы сортировки. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL.
  • Создать DLL с интерфейсным классом InputArray, позволяющий вводить одномерный массив с проверкой корректности ввода элементов. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL.
  • Проекты

  • Дано множество точек P1, P2, $$\dots$$ Pn со случайными координатами. Построить множество точек P1, P2, $$\dots$$ Pm ( m <= n ), обход которых позволяет построить многоугольник без пересечений, если это возможно. Уметь вычислять площадь многоугольника, определять, является ли он выпуклым, принадлежит ли заданная точка внутренней области (границам) многоугольника. Построить реализацию в виде класса Polygon, включенного в состав DLL. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с объектами класса Polygon.
  • Построить Windows-проект, реализующий трансляцию выражений.
  • Практикум 2. Создание простого приложения в Visual Studio 2010

    В практикуме показывается пример создания простой программы в 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

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

    Модификатор protected

    Этот модификатор открывает поля классам наследникам. Если класс A объявил некоторое поле с модификатором protected, то методы класса B, который является наследником класса A и, следовательно, наследует поля класса A, могут непосредственно работать с наследуемыми полями.

    Модификатор internal

    Этот модификатор открывает поля дружественным классам. Два класса A и B называются дружественными, если они принадлежат одной сборке - одному проекту. Если класс A объявил некоторое поле с модификатором internal, то методы дружественного класса B, являющегося клиентом класса A, могут непосредственно работать с таким полем.

    Комбинация атрибутов protected и internal

    Эта комбинация открывает поле тем классам, которые являются либо наследниками, либо дружественными классами. Если требуется более строгое ограничение доступа к полю, чтобы оно было доступно только тем наследникам, которые являются дружественными классами, то сам класс нужно объявить с модификатором 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, health, характеризующих, соответственно, фамилию, статус, зарплату, возраст и здоровье персоны. Все поля закрыты для клиента, так что клиент не может непосредственно читать или записывать данные в поля класса. Для каждого из этих полей может быть разумной своя стратегия доступа. При проектировании класса будем предполагать, что возраст доступен для чтения и записи, фамилию можно задать только один раз, статус можно только читать, зарплата недоступна для чтения, а здоровье закрыто для доступа и только специальные методы класса могут сообщать некоторую информацию о здоровье персоны. Вот как на 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

    Для полноты картины добавим в класс 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

    В заключение этой лекции займемся проектированием класса Rational, описывающего известный в математике тип данных - рациональные числа. По ходу проектирования будут вводиться новые детали, связанные с описанием класса. Начнем проектирование, как обычно, с задания тэга summary, описывающего назначение класса, его свойства и поведение. Вот этот текст:

    /// <summary>
    /// Класс Rational.
    /// определяет новый тип данных - рациональные числа и основные
    /// операции над ними - сложение, умножение, вычитание и деление.
    /// Рациональное число задается парой целых чисел (m,n) и изображается
    /// обычно в виде дроби m/n. Число m называется числителем,
    /// n - знаменателем. Для каждого рационального числа существует
    /// множество его представлений, например, 1/2, 2/4, 3/6, 6/12.
    /// Среди всех представлений
    /// можно выделить то, в котором числитель и знаменатель взаимно
    /// несократимы. Такой представитель будет храниться в полях класса.   
    /// </summary>
    public class Rational
    {
          // Описание тела класса Rational
    }//Rational

    Свойства класса Rational

    Рациональное число представимо парой целых чисел - m и n. Они и становятся полями класса. Совершенно естественно сделать эти поля закрытыми. Разумная стратегия доступа к ним - "ни чтения, ни записи", поскольку пользователь не должен знать, как представлено рациональное число в классе, и не должен иметь доступа к составляющим рационального числа. Поэтому для таких закрытых полей не будут определяться методы-свойства. Вот объявление полей и констант класса:

    //Поля класса. Числитель и знаменатель рационального числа.
    const string NONE_EXIST =
         "Не существует рационального  числа " +
         "со знаменателем, равным нулю!";      
    int m,n;

    Конструкторы класса Rational

    Зададим два конструктора - конструктор без аргументов, создающий рациональное число 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. Это типичная ситуация, когда совместно с проектируемым классом создается и класс исключений, характеризующий исключительные ситуации, которые могут возникнуть при работе клиента с объектами класса. Чуть позже приведу объявление этого класса.

    Методы класса Rational

    Если поля класса почти всегда закрываются, чтобы скрыть от пользователя представление данных класса, то методы класса всегда имеют открытую часть - те сервисы (службы), которые класс предоставляет своим клиентам и наследникам. Но не все методы открываются. Большая часть методов класса может быть закрытой, скрывая от клиентов детали реализации, необходимые для внутреннего использования. Заметьте, сокрытие представления и реализации делается не по соображениям утаивания того, как реализована система. Чаще всего, ничто не мешает клиентам ознакомиться с полным текстом класса. Сокрытие делается в интересах самих клиентов. При сопровождении программной системы изменения в ней неизбежны. Клиенты не почувствуют на себе негативные последствия изменений, если они делаются в закрытой части класса. Чем больше закрытая часть класса, тем меньше влияние изменений на клиентов класса.

    Закрытый метод НОД

    Метод, вычисляющий наибольший общий делитель пары чисел, понадобится не только конструктору класса, но и всем операциям над рациональными числами. Алгоритм нахождения общего делителя хорошо известен со времен Эвклида. Я приведу программный код метода без особых пояснений:

    /// <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, Divide, соответственно. Поскольку рациональные числа - это прежде всего именно числа, то для выполнения операций над ними часто удобнее пользоваться привычными знаками операций ( +, -, *, / ). Язык 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) Операции и выражения над рациональными числами

    Константы класса Rational

    Рассмотрим важную проблему определения констант в собственном классе. Определим две константы 0 и 1 класса Rational. Кажется, что сделать это невозможно из-за ограничений, накладываемых на объявление констант. Напомню, константы должны быть инициализированы в момент объявления и их значения должны быть заданы константными выражениями, известными в момент компиляции. Но в момент компиляции у класса Rational нет никаких известных константных выражений. Как же быть? Справиться с проблемой поможет статический конструктор. Нам также будет полезен закрытый конструктор класса. Роль констант класса будут играть статические поля, объявленные с атрибутом readonly, доступные только для чтения.

    //Константы класса 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

    Класс RationalException

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

    Как правило, при проектировании класса параллельно с ним проектируется и класс, задающий исключительные ситуации. Такой класс должен быть объявлен как наследник класса Exception из библиотеки FCL, наследуя тем самым свойства и методы, общие для всех объектов, задающих исключения. Чаще всего, как это имеет быть место и в нашем случае, класс потомок устроен довольно просто и содержит набор конструкторов, вызывающих конструктор родительского класса, передавая ему информацию о возникшей ситуации. Более подробно поговорим об этом в соответствующей лекции, посвященной исключениям, а сейчас приведу код класса 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, присоединенных к консольному проекту.

    Задачи

  • Создать DLL с классом Point, описывающим точку на плоскости, заданную декартовыми и полярными координатами. Среди методов класса Point предусмотреть метод Distance, вычисляющий расстояние до заданной точки. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с классом Point.
  • Создать DLL с классами Point и Line, описывающими точку и линию на плоскости. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL. Среди методов класса Line предусмотреть метод, определяющий принадлежность заданной точки линии.
  • Создать DLL с классами Point и Square, описывающими точку и квадрат на плоскости со сторонами, параллельными осям координат. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL. Среди методов класса Square предусмотреть метод, определяющий принадлежность заданной точки квадрату.
  • Создать DLL с классами Point и Circle, описывающими точку и круг на плоскости. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL. Среди методов класса Circle предусмотреть метод, определяющий принадлежность заданной точки кругу.
  • Создать DLL с классами Point и Rectangle, описывающими точку и прямоугольник на плоскости со сторонами, параллельными осям координат. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL. Среди методов класса Rectangle предусмотреть метод, определяющий принадлежность заданной точки прямоугольнику.
  • Создать DLL с классами Point и Rhomb, описывающими точку и ромб на плоскости с осями, параллельными осям координат. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL. Среди методов класса Rhomb предусмотреть метод, определяющий принадлежность заданной точки ромбу.
  • Создать DLL с классами Point и Tetragon, описывающими точку и четырехугольник на плоскости. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL. Среди методов класса Tetragon предусмотреть метод, определяющий принадлежность заданной точки четырехугольнику.
  • Создать DLL с классами Point и Triangle, описывающими точку и треугольник на плоскости. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL. Среди методов класса Triangle предусмотреть метод, определяющий принадлежность заданной точки треугольнику.
  • Создать DLL с классами 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 не пуст, читается очередная лексема Lex с вершины стека. Затем производится анализ типа этой лексемы и приоритета, если лексема задает операцию. Если лексема - операнд, то она помещается в очередь queueBackExpression. Если лексема - открывающая скобка, то она записывается в очередь queueOperations. Если лексема - закрывающая скобка, то из очереди queueOperations выгружаются все элементы вплоть до открывающей скобки. Эти элементы записываются в выходную очередь queueBackExpression. Знаки скобок в выходной стек не записываются. Если лексема - операция, то анализируется ее приоритет. Если приоритет лексемы выше приоритета последней операции, записанной в queueOperations, то новая операция записывается в очередь queueOperations. В противном случае операция из очереди выгружается и записывается в очередь queueBackExpression. Когда стек stackExpression опустоша ется, то все операции, оставшиеся в очереди queueOperations, выгружаются и записываются в очередь queueBackExpression.

    Очередь queueBackExpression является входом для метода, вычисляющего значение выражения. Для вычисления выражения можно использовать стек stackResult. Алгоритм вычисления значения выражения можно описать следующим образом. Пока очередь queueBackExpression не пуста, читается очередная лексема. Если лексема - операнд, то она записывается в стек stackResult. Если лексема - операция, то из стека stackResult выгружаются предшествующие операции операнды, один или два в зависимости от того, является операция унарной или бинарной. Далее выполняется операция над операндами, и ее результат как операнд записывается в стек stackResult. Если все корректно, то по опустошению очереди на вершине стека stackResult будет операнд, представляющий значение выражения.

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

    Задачи на трансляцию выражений

  • Создать DLL с классом Lexeme, описывающим лексему. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL и позволяющий выполнить лексический анализ текста, задающего программу (фрагмент программы) на языке C#.
  • (*) Создать DLL с классами Lexeme и AriphmeticExpression, описывающими лексему и арифметическое выражение. Выражение строится из чисел, функций, знаков операций и скобок и задается строкой текста. В классе предусмотреть метод, позволяющий вычислить значение выражения. Среди методов класса AriphmeticExpression предусмотреть метод разбора выражения, представляющий выражение в виде последовательности лексем. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL.
  • (*) Создать DLL с классами Lexeme и AriphmeticExpression, описывающими лексему и арифметическое выражение с одной переменной x. Выражение строится из чисел, переменной x, функций, знаков операций и скобок и задается строкой текста. В классе предусмотреть метод, позволяющий вычислить значение выражения. Среди методов класса AriphmeticExpression предусмотреть метод разбора выражения, представляющий выражение в виде последовательности лексем. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL.
  • (*) Создать DLL с классами Lexeme и AriphmeticExpression, описывающими лексему и арифметическое выражение с двумя переменными x и y. Выражение строится из чисел, переменных x и y, функций, знаков операций и скобок и задается строкой текста. В классе предусмотреть метод, позволяющий вычислить значение выражения. Среди методов класса AriphmeticExpression предусмотреть метод разбора выражения, представляющий выражение в виде последовательности лексем. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL.
  • (**) Создать DLL с классами Lexeme и LogicExpression, описывающими лексему и логическое выражение. Выражение строится из констант true и false, отношений, функций, знаков операций и скобок и задается строкой текста. Отношение представляет собой пару арифметических выражений, связанных операцией отношения. В классе предусмотреть метод, позволяющий вычислить значение выражения. Среди методов класса LogicExpression предусмотреть метод, представляющий выражение в виде последовательности лексем. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL.
  • (**) Создать DLL с классами Lexeme и LogicExpression, описывающими лексему и логическое выражение с одной переменной x. Выражение строится из констант true и false, отношений, функций, знаков операций и скобок и задается строкой текста. Отношение представляет собой пару арифметических выражений c переменной x, связанных операцией отношения. В классе предусмотреть метод, позволяющий вычислить значение выражения. Среди методов класса LogicExpression предусмотреть метод, представляющий выражение в виде последовательности лексем. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL.
  • (**) Создать DLL с классами Lexeme и LogicExpression, описывающими лексему и логическое выражение c двумя переменными x и y. Выражение строится из констант true и false, отношений, функций, знаков операций и скобок и задается строкой текста. Отношение представляет собой пару арифметических выражений с переменными x и y, связанных операцией отношения. В классе предусмотреть метод, позволяющий вычислить значение выражения. Среди методов класса LogicExpression предусмотреть метод, представляющий выражение в виде последовательности лексем. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL.
  • (**) Создать DLL с классами Lexeme, Expression и Assignment, описывающими лексему, выражение и оператор присваивания вида z = expression. Выражение в правой части оператора присваивания может быть одним из выражений, заданных в предыдущих задачах этого раздела. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL.
  • Создать DLL с классами Student и Exam, описывающими студента и экзамен. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL.
  • Создать DLL с сервисным классом Poisk, содержащего методы линейного и бинарного поиска. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL.
  • Создать DLL с сервисным классом Sorting, содержащего методы сортировки. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL.
  • Создать DLL с интерфейсным классом InputArray, позволяющий вводить одномерный массив с проверкой корректности ввода элементов. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с DLL.
  • Проекты

  • Дано множество точек P1, P2, $$\dots$$ Pn со случайными координатами. Построить множество точек P1, P2, $$\dots$$ Pm ( m <= n ), обход которых позволяет построить многоугольник без пересечений, если это возможно. Уметь вычислять площадь многоугольника, определять, является ли он выпуклым, принадлежит ли заданная точка внутренней области (границам) многоугольника. Построить реализацию в виде класса Polygon, включенного в состав DLL. Построить Windows-проект, предоставляющий пользователю интерфейс для работы с объектами класса Polygon.
  • Построить Windows-проект, реализующий трансляцию выражений.
  • Практикум 2. Создание простого приложения в Visual Studio 2010

    В практикуме показывается пример создания простой программы в Visual Studio 2010 на языке C#.

    Вернуться к учебному плану