Тверской государственный университет
Опубликован: 02.12.2009 | Доступ: свободный | Студентов: 2376 / 262 | Оценка: 4.47 / 4.24 | Длительность: 14:45:00
Лекция 2:

Классы

< Лекция 1 || Лекция 2: 123456 || Лекция 3 >
Аннотация: В лекции рассматриваются все детали проектирования и построения класса. Обсуждается роль полей, как глобальной информации, доступной всем методам класса. Рассматривается интерфейс – открытая часть класса, методы класса и их частные случаи – конструкторы, свойства, операции. Лекция сопровождается задачами на построение классов.

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

Классы и ООП

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

Имя деструктора строится из имени класса с предшествующим ему символом ~ (тильда). Как и у статического конструктора, у деструктора не указывается модификатор доступа.

< Лекция 1 || Лекция 2: 123456 || Лекция 3 >
Илья Ардов
Илья Ардов

Добрый день!

Я записан на программу. Куда высылать договор и диплом?

Дарья Федотова
Дарья Федотова