5484

С# и объектно-ориентированное программирование

Реферат

Информатика, кибернетика и программирование

С# и объектно-ориентированное программирование Формальное определение класса в С# Класс в С#, как и в других языках программирования, - это пользовательский тип данных (userdefinedtype, UDT), который состоит из данных (часто называе...

Русский

2012-12-12

236.5 KB

111 чел.

С# и объектно-ориентированное программирование

Формальное определение класса в С#

Класс в С#, как и в других языках программирования, — это пользовательский тип данных (user defined type, UDT), который состоит из данных (часто называемых атрибутами или свойствами) и функциями для выполнения с этими данными различных действий (эти функции обычно называются методами). Классы позволяют группировать в единое целое данные и функциональность, моделируя объекты реального мира. Именно это свойство классов и обеспечивает одно из наиболее важных преимуществ объектно-ориентированных языков программирования

К примеру, предположим, что нам потребовалось создать модель сотрудника нашей организации. Конечно же, для этой цели удобнее всего создать специальный класс. Этот класс, как минимум, должен хранить данные об имени работника, его идентификационном номере и текущей заработной плате. Помимо этого, пусть в нашем классе будут определены два метода — GiveBonus( ) для увеличения заработной платы сотрудника и DisplayStats ( ) для вывода всех имеющихся данных of этом сотруднике (рис. 3.1).

Рис. 3.1. Определение простого класса

Как уже говорилось в главе 2, для классов СП можно определить любое количество конструкторов. Эта, специальные методы классов позволяют создавать объекты данного класса, настраивая при этом их исходное состояние нужным вам образом. Любой класс С# автоматически снабжается конструктором по умолчанию (не принимающим никаких параметров). Этот конструктор в С# (в отличие от C++) при создании объекта автоматически присвоит всем переменным-членам класса безопасные значения по умолчанию. Кроме того, вы можете как переопределить конструктор по умолчанию, так и создать такое количество пользовательских конструкторов (принимающих параметры), сколько вам необходимо. Давайте определим наш класс для сотрудника на С#:

// Исходное определение класса

class Employee

{

// Внутренние закрытые данные класса

private string fullNameж

private int empID;

private float currPay;

// Конструкторы

public Employee( ) { }

public Employee(string fullName, int empID, float currPay)

{

this.fullName = fullName;

this.empID = empID;

this.currPay = currPay;

}

// Метод для увеличения зарплаты сотрудника

public void CiveBonus(float amount)

{ currPay += amount; }

// Метод для вывода сведений о текущем состоянии объекта

public virtual void DisplayStats( )

{

Console.WriteLine(“Name:{0}, fullName);

Console.WriteLine("Pay: {0}", currPay);

Console.WriteLine(“ID: {0}", empID);

Console.WriteLine(“SSN:{0}", ssn);

}

}

Обратите внимание на то, что определение для конструктора по умолчанию не содержит каких-либо действий, которые должен выполнять конструктор:

class Employee

{

// Всем переменным-членам значения по умолчанию

// будут присвоены автоматически

public Employee( ) {}

. . . .

} '

Необходимо обязательно помнить о следующем обстоятельстве: в С# (как и в C++), если вы определили хотя бы один пользовательский конструктор (примающий параметры), конструктор по умолчанию автоматически создаваться уже будет и его придется определять явно. В противном случае при попытке выполнения такого, к примеру, кода:

// Вызываем конструктор по умолчанию

Employee е = new Employee( );

Вы получите сообщение об ошибке компилятора.

Использование пользовательского конструктора очевидно:

// Вызываем пользовательский конструктор (двумя способами)

public static void Main( )

{

Employee e = new Employee("Joe", 80, 30000);

e.GiveBonus(200);

Employee e2;

e2 = new Employee("Beth”, 81, 50000);

e2.GiveBonus(1000);

e2.DisplayStats( );

}

Ссылки на самого себя

В определениях пользовательских конструкторов часто используется зарезервированное слово this:

// В С#, как и в C++, и в Java, предусмотрено использование зарезервированного

// слова this

public Employee(string fullName, int empID, float currPay)

{

this.full Name = fullName;

this.empID = empID;

this.currPay = currPay;

}

В С# слово this используется для ссылки на текущий экземпляр объекта. В Visual Basic схожие функции выполняет зарезервированное слово Me, а программисты, использующиеС++ и Java, вообще должны почувствовать себя каг дома, поскольку в этих языках также есть слово this, выполняющее те же функции.

Главная причина, по которой в конструкторах используется слово this, — желание избежать конфликтов между именами принимаемых параметров и именам внутренних переменных-членов класса. Конечно же, такого конфликта можно избежать и более простым способом — определить для принимаемых переменных имена, отличные от имен переменных-членов класса. Кроме того, необходимо помнить,_что через указатель this невозможно обратиться к статическим функциям-членам. Причина очевидна: такие функции принадлежат не отдельным объектам класса, а всему классу в целом.

Определение открытого интерфейса по умолчанию

Открытый интерфейс по умолчанию для класса (default public interface) — это набор открытых членов данного класса. С точки зрения обращения к членам класса из остальной части программы открытый интерфейс по умолчанию — это набор тех переменных, свойств и методов — членов, к которым можно обратиться в формате «имя_объекта.член_класса». С точки зрения определения класса — это набор тех членов класса, которые объявлены с использованием ключевого слова publiс. В С# открытый интерфейс по умолчанию для класса может состоять из следующих членов этого класса:

  •  методов — наборов действий, определенных как единое целое, с помощью которых реализуется поведение данного класса;
  •  свойств — «переодетых» функций для получения данных и их изменения;
  •  полей: открытых переменных-членов (как правило, использование таких переменных — не самый лучший вариант, но в С# они поддерживаются).

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

Указание области видимости на уровне типа: открытые и внутренние типы

Перед тем как углубиться в новые варианты нашего примера с сотрудником, нам необходимо разобраться с областями видимости для типов. В предыдущих главах мы много раз встречались с определениями классов вида:

class HelloClass

{

// Любое количество методов с любым количеством параметров

// Конструктор по умолчанию и пользовательские конструкторы

// Если в этом классе определена и точка входа для всей программы.

// то еще и статический метод Main( )

}

Как мы помним, для каждого члена класса необходимо указать (или принять значение по умолчанию,) область видимости с помощью одного из следующих ключевых слов: public, private, protected и internal. Однако область видимостизадает-ся не только для членов класса, но и для самих классов! Разница между областью видимости для членов классов и областью видимости для самих классов заключается в том, что в первом случае мы определяем те члены класса, которые будут доступны через экземпляры объектов, а во втором — те области программы, из которых будет возможно обращение к данным классам.

Для класса в С# используется только два ключевых слова для определения области видимости: publiс и internal. Объекты классов, определенных как public (открытых), могут быть созданы как из своего собственного двоичного файла, так и из других двоичных файлов С# (то есть из другой сборки). Если переопределять HelloClass как public, то это можно сделать следующим образом:

// Теперь этот класс можно создавать из-за пределов сборки, в которой он определен

public class HelloClass

{

// Любое количество методов с любым количеством параметров

// Конструктор по умолчанию и пользовательские конструкторы

// Если в этом классе определена и точка входа для всей программы,

// то еще и статический метод Main( )

}

Если вы не указали явно область видимости для класса, то по умолчанию этот класс будет объявлен как internal (внутренний). Объекты таких классов могут создаваться только объектами из той же сборки, в которой они были определены. Внутренние классы часто рассматриваются как вспомогательные (helper classes), поскольку они используются типами данной сборки для помощи в выполнении каких-либо действий.

// Внутренние классы могут использоваться только внутри той сборки,

//в которой они определены

internal class HelloClassHelper

{

. . . . .

}

Классы — это не единственные пользовательские типы данных, для которых существует атрибут области видимости. Как мы помним, тип — это общий термин, который используется в отношении классов, структур, перечислений, интерфейсов и делегатов. Атрибут видимости (public или internal) может быть установлен для любого типа С#, например:

Все рассмотренные выше типы можно для наглядности собрать в единую схему (рис. 3.2):

Рис. 3.2. Внутренние и открытые типы

Вопросы, связанные с созданием сборок .NET, рассматриваются в главе 6. Сейчас же нам достаточно помнить, что все типы данных в С# могут объявляться как открытые (public) — доступные из-за пределов собственной сборки, и как закрытые (private) — доступные только из данной сборки.

Столпы объектно-ориентированного программирования

С# можно считать новым членом сообщества объектно-ориентированных языков программирования, к самым распространенным из которых относятся Java, C++, Object Pascal и (с некоторыми допущениями) Visual Basic 6.0. В любом объектно-ориентированном языке программирования обязательно реализованы три важнейших принципа — «столпа» объектно-ориентированного программирования:

  •  инкапсуляция: как объекты прячут свое внутреннее устройство;
  •  наследование: как в этом языке поддерживается повторное использование кода;
  •  полиморфизм: как в этом языке реализована поддержка выполнения нужного действия в зависимости от типа передаваемого объекта?

Конечно же, все три этих принципа реализованы и в С#. Однако перед тем как начать рассматривать синтаксические особенности реализации этих принципов, мы постараемся убедиться, что в них не осталось абсолютно никаких неясностей.

Инкапсуляция

Первый «столп» объектно-ориентированного программирования — это инкапсуляция. Так называется способность прятать детали реализации объектов от пользователей этих объектов. Например, предположим, что вы создали класс с именем DBReader (для работы с базой данных), в котором определено два главных метода: Open( ) и Close( ).

// Класс DBReader скрывает за счет инкапсуляции подробности открытия

// и закрытия баз данных

DBReader f = new DBReader( );

f.Open(@"C:\foo.mdf");

// Выполняем с базой данных нужные нам действия

f.Close( );

Наш вымышленный класс DBReader инкапсулирует внутренние подробности того, как именно он обнаруживает, загружает, выполняет операции и в конце концов закрывает файл данных. За счет инкапсуляции программирование становится проще: вам нет необходимости беспокоиться об огромном количестве строк кода, которые выполняют свою задачу скрыто от вас. Все, что от вас требуется — создать экземпляр нужного класса и передать ему необходимые сообщения (типа «открыть файл с именем foo.mdf»).

С философией инкапсуляции тесно связан еще один принцип — сокрытия всех внутренних данных (то есть переменных-членов) класса. Как мы уже говорили, в идеале все внутренние переменные члены класса должны быть определены как private. В результате обращение к ним из внешнего мира будет возможно только при помощи открытых функций — членов. Такое решение, помимо всего прочего, защищает вас от возможных ошибок, поскольку открытые данные очень просто повредить.

Наследование: отношения «быть» и «иметь»

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

Рис. 3.3. Отношение «быть»

Как мы помним, на вершине любой иерархии в .NET всегда находится базовый класс Object. В нашей ситуации возможности этого класса вначале дополняются возможностями, привнесенными классом Shape. Речь идет о свойствах, полях, методах и событиях, которые являются общими для всех геометрических фигур (shapes). Класс Hexagon (шестиугольник), производный от Shape, дополняет возможности предыдущих двух базовых классов своими собственными уникальными свойствами.

Диаграмму, представленную на рис. 3.3, можно прочесть следующим образом-«Шестиугольник есть геометрическая фигура, которая есть объект». Когда ваши •лассы Связываются друг с другом отношениями наследования, это означает, что зы устанавливаете между ними отношения типа «быть» (is-a). Такой тип отношений называется также классическим наследованием.

В мире объектно-ориентированного программирования используется еще одна эорма повторного использования кода. Эта форма называется включением-деле-"ированием (или отношением «иметь» — has-a). При ее использовании один класс включает в свой состав другой и открывает внешнему миру часть возможностей этого внутреннего класса.

Например, если вы создаете программную модель автомобиля, у вас может появиться идея включить внутрь объекта «автомобиль» объект «радио» с помощью отношения «иметь». Это вполне разумный подход, поскольку вряд ли возможно произвести как радио от автомобиля, так и автомобиль от радио, используя отношения наследования. Вместо наследования вы создаете два независимых класса, работающих совместно, где внешний (контейнерный) класс создает внутренний класс и открывает внешнему миру его возможности (рис. 3.4).

Рис. 3.4. Отношения между средой выполнения и библиотекой базовых классов .NET

Внешний объект Саг (автомобиль) ответственен за создание внутреннего объекта Radio (радио). Если вы хотите, чтобы через объект Саг можно было бы обращаться ко внутреннему объекту Radio, в классе Саг необходимо предусмотреть специальный открытый интерфейс для этой цели. Обратите внимание, что пользователь объекта Саг и не узнает, что тот передает обращения какому-то внутреннему объекту:

// Внутренний класс Radio инкапсулирован внешним классом Саг

Саг viper = new Car( );

Viper.TurnOnRadio(false);  // Вызов будет передан внутреннему объекту Radio

Полиморфизм: классический и для конкретного случая

Последний, третий столп объектно-ориентированного программирования — это полиморфизм. Можно сказать, что этот термин определяет возможности, заложенные в языке, по интерпретации связанных объектов одинаковым образом. Существует две основные разновидности полиморфизма: классический полиморфизм и полиморфизм «для конкретного случая» (ad hoc). Классический полиморфизм встречается только в тех языках, которые поддерживают классическое наследование (в том числе, конечно, и в С#). При классическом полиморфизме вы можете определить в базовом классе набор членов, которые могут быть замещены в производном классе. При замещении в производных классах членов базового класса эти производные классы будут по-разному реагировать на одни и те же обращения.

Для примера мы вновь обратимся к нашей иерархии геометрических фигур. Предположим, что в классе Shape (геометрическая фигура) определена функция Draw( ) — рисование, которая не принимает параметров и ничего не возвращает Поскольку геометрические фигуры бывают разными и каждый тип фигуры потребуется изображать своим собственным уникальным способом, скорее всего, нам потребуется в производных классах (таких как Hexagon — шестиугольник и Circle — окружность) создать свой собственный метод Draw( ), заместив им метод Draw( ) базового класса (рис. 3.5).

Рис. 3.5. Классический полиморфизм

Классический полиморфизм позволяет определять возможности всех производных классов при создании базового класса. Например, в нашем примере вы можете быть уверены, что метод Draw( ) в том или ином варианте присутствует в любом классе, производном от Shape. К достоинствам классического полиморфизма можно отнести также и то, что во многих ситуациях вы сможете избежать создания повторяющихся методов для выполнения схожих операций (типа DrawCircle( ), DrawRectangleO, DrawHexagon( )и т. д.).

Вторая разновидность полиморфизма — полиморфизм для конкретного случая (ad hoc polymorphism). Этот тип полиморфизма позволяет обращаться схожим образом к объектам, не связанным классическим наследованием. Достигается это очень просто: в каждом из таких объектов должен быть метод с одинаковой сигнатурой (то есть одинаковым именем метода, принимаемыми параметрами и типом возвращаемого значения. В языках, поддерживающих полиморфизм этого типа, применяется технология «позднего связывания» (late binding), когда тип объекта, к которому происходит обращение, становится ясен только в процессе выполнения программы. В зависимости от того, к какому типу мы обращаемся, вызывается нужный метод. В качестве примера рассмотрим схему на рис. 3.6.

Обратите внимание, что общего предка — базового класса для ССircle, СНехаgon и CRectangle не существует. Однако в каждом классе предусмотрен метод Draw( )  с одинаковой сигнатурой. Для того чтобы продемонстрировать применение полиморфизма этого типа в реальном коде, мы воспользуемся примером на Visual Basic 6.0. До изобретения VB.NET Visual Basic не поддерживал классический полиморфизм (так же, как и классическое наследование), заставляя разработчиков сосредоточивать свои усилия на полиморфизме ad hoc.

Рис. 3.6. Полиморфизм для конкретного случая

‘ Это - код на Visual Basic 6.0!

‘ Вначале создадим массив элементов типа Object и установим для каждого элемента ссылку на объект

Dim objArr(3) as Object

Set objArr(0) = New Ccircle

Set objArr(1) = New Chexagon

Set objArr(2) = New Ccircle

Set objArr(3) = New Crectangle

' Теперь с помощью цикла заставим каждый элемент нарисовать самого себя

Dim i as Integer

For i = 0 to 3

 objArr(i). Draw ()   'Позднее связывание

Next i

В этом коде мы вначале создали массив элементов типа Object (это встроенный тип данных Visual Basic 6.0 для хранения ссылок на любые объекты, не имеющий ничего общего с классом System.Object в .NET). Затем мы связали каждый элемент массива с объектом соответствующего типа, а потом при помощи цикла воспользовались методом Draw( ) для каждого из этих объектов. Обратите внимание, что у геометрических фигур — элементов массива — нет общего базового класса с реализацией метода Draw( ) по умолчанию.

Теоретический обзор главных принципов полиморфизма — инкапсуляции, наследования и полиморфизма на этом закончен. Конечно же, в С# реализованы все эти принципы, при этом С# поддерживает и отношения «быть» и отношения «иметь» для повторного использования кода, и обе разновидности полиморфизма. Теперь наша задача — узнать, как реализуется каждый из этих принципов средствами синтаксиса С#.

Средства инкапсуляции в С#

Принцип инкапсуляции предполагает, что ко внутренним данным объекта (переменным-членам) нельзя обратиться напрямую через экземпляр этого объекта. Вместо этого для получения информации о внутреннем состоянии объекта и внесения изменений необходимо использовать специальные методы. В С# инкапсуляция реализуется на уровне синтаксиса при помощи ключевых слов public, private и protected. В качестве примера мы рассмотрим следующее определение класса:

// Класс с единственным полем

public class Book

{

public int numberOfPages;

 . . . . .

}

Термин «поле» (field) используется для открытых данных класса — переменных, объявленных с ключевым словом public. При использовании полей в приложении возникает проблема: полю можно присвоить любое значение, а организовать проверку этого значения бизнес-логике вашего приложения достаточно сложно. Например, для нашей открытой переменной numberOfPages используется тип данных int. Максимальное значение для этого типа данных — это достаточно большое число (2 147 483 647). Если в программе будет существовать такой код, проблем со стороны компилятора не возникнет:

// Задумаемся...

public static void Main( )

{

Book miniNovel = new Book();

miniNovel.numberOfPages = 30000000;

 

}

Тип данных i nt вполне позволяет указать для книги небольших размеров количество страниц, равное 30 000 000. Однако понятно, что книг такой величины не бывает, и во избежание дальнейших проблем желательно использовать какой-нибудь механизм проверки, который отсеивал бы явно нереальные значения (например, он пропускал бы только значения между 1 и 2000). Применение поля — открытой переменной не дает нам возможности простым способом реализовать подобный механизм. Поэтому поля в реальных рабочих приложениях используются нечасто.

Следование принципу инкапсуляции позволяет защитить внутренние данные класса от неумышленного повреждения. Для этого достаточно все внутренние данные сделать закрытыми (объявив внутренние переменные с использованием ключевых слов private или protected). Для обращения к внутренним данным можно использовать один из двух способов:

  •  создать традиционную пару методов — один для получения информации (accessor), второй — для внесения изменений (mutator);
  •  определить именованное свойство.

Еще один метод защиты данных, предлагаемый С#, — использовать ключевое слово readonly. Однако какой бы способ вы ни выбрали, общий принцип остается тем же самым — инкапсулированный класс должен прятать детали своей реализации от внешнего мира. Такой подход часто называется «программированием по методу черного ящика». Еще одно преимущество такого подхода заключается в том, что вы можете как угодно совершенствовать внутреннюю реализацию своего класса, полностью изменяя его содержимое. Единственное, о чем вам придется позаботиться, — чтобы в новой реализации остались методы с той же сигнатурой и функциональностью, что и в предыдущих версиях. В этом случае вам не придется менять ни строчки существующего кода за пределами данного класса.

Реализация инкапсуляции при помощи традиционных методов доступа и изменения

Давайте вновь обратимся к нашему классу Employee. Если мы хотим, чтобы внешний мир смог работать с внутренними данными этого класса (пусть это будет только одна переменная fullName, для которой используется тип данных string), то традиционный подход рекомендует создание метода доступа (accessor, или get method) и метода изменения (mutator, или set method). Набор таких методов может выглядеть следующим образом:

// Определение традицонных методов доступа и изменения для закрытой переменной public class Employee

{

private string fullName:

. . .  .

// Метод доступа

public string GetFullName( )  {return fullName; }

  // Метод изменения

public void SetFullName(string n)

{

// Логика для удаления неположенных символов (!. @. #. $. % и прочих

// Логика для проверки максимальной длины и прочего

fullName = n;

}

}

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

// Применение методов доступа и изменения

public static int Main(string[ ] args)

{

Employee p = new Employee();

p.SetFullName(“Fred");

Console.WriteLine(“Employee is named: " + p.GetFullName());

}

Второй способ инкапсуляции: применение свойств класса

Помимо традиционных методов доступа и изменения для обращения к закрытым членам класса можно также использовать свойства (properties). В Visual Basic и СОМ свойства — это привычный инструмент. Свойства позволяют имитировать доступ к внутренним данным класса: при получении информации или внесении изменений через свойство синтаксис выглядит так же, как при обращении к обычной открытой переменной. Но на самом деле любое свойство состоит из двух скрытых внутренних методов. Преимущество свойств заключается в том, что вместо того, чтобы использовать два отдельных метода, пользователь класса может использовать единственное свойство, работая с ним так же, как и с открытой переменной-членом данного класса:

// Обращ ение к имени сотрудника через свойство

public static int Main(string[ ] args)

{

Employee p = new Employee();.

// Устанавливаем значение

p.EmpID = 81;

// Получаем значение

Console.WriteLine(“Person ID is : {0}", p.EmpID);

return 0;

}

Если заглянуть внутрь определения класса, то свойства всегда отображаются в «реальные» методы доступа и изменения. А уже в определении этих методов вы можете реализовать любую логику (например, для устранения лишних символов, проверки допустимости вводимых числовых значений и прочего). Ниже представлен синтаксис класса Employee с определением свойства EmpID:

// Пользовательское свойство EmpID для доступа к переменной empIDi

public class Employee

{

private int empID;

// Свойство для empID

public int EmpID

{

get 

{

return empID;

}

set

{

// Здесь вы можете реализовать логику для проверки вводимых

// значений и выполнения других действий

empID = value;

}

}

}

Свойство С# состоит из двух блоков — блока доступа (get block) и блока изменения (set block). Ключевое слово value представляет правую часть выражения при присвоении значения посредством свойства. Как и все в С#, то, что представлено словом value — это также объект. Совместимость того, что передается свойству как value, с самим свойством, зависит от определения свойства. Например, свойство EmpID предназначено (согласно своему определению в классе) для работы с закрытым целочисленным empID, поэтому число 81 вполне его устроит:

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

Последнее, что мы отметим — использование свойств (по сравнению с традиционными методами доступа и изменения) делает применение ваших типов более простым. Например, предположим, что в классе Employee предусмотрена внутренняя закрытая переменная для хранения информации о возрасте сотрудника. Вы чотите, чтобы при наступлении дня рождения этого сотрудника значение этой пе-оеменной увеличивалось на единицу. При использовании традиционных методов доступа и изменения эта операция будет выглядеть так:

Employee joe = new Employee();

Joe.SetAge(joe.GetAge() + 1);

Используя свойство, вы можете сделать это проще:

Employee joe = new Employee()ж

joe.Age++;

Поддержка наследования в С#

Теперь, когда вы уже умеете создавать хорошо инкапсулированные классы, обратимся к созданию в С# наборов взаимосвязанных классов при помощи наследования. Как уже говорилось, наследование — это один из трех основных принципов объектно-ориентированного программирования. Главная задача наследования — обеспечить повторное использование кода. Существует два основных вида наследования: классическое наследование (отношение «быть» — is-a) и включение-делегирование (отношение «иметь» — has-a). Мы начнем с рассмотрения средств СП для реализации классического наследования.

При установлении между классами отношений классического наследования вы тем самым устанавливаете между ними зависимость. Основная идея классического наследования заключается в том, что производные классы должны получать функциональность от базового класса-предка и дополнять ее новыми возможностями. Рассмотрим это на примере. Предположим, что в дополнение к нашему классу Employee мы определили еще два производных класса: класс Salesperson (продавец) и класс Manager (администратор). В результате получится иерархия классов, которая представлена на рис. 3.9.

Рис. 3.9. Иерархия классов сотрудников

Как видно из рисунка, сотрудниками являются и продавец, и менеджер. В модели классического наследования базовые классы (в нашем случае Employee) используются для того, чтобы определить характеристики, которые будут общими для всех производных классов. Производные классы (такие как Salesperson и Manager) расширяют унаследованную функциональность за счет своих собственных, специфических членов.

В С# указатель на базовый класс выглядит как двоеточие ( ). Если переводить наши отношения наследования между категориями сотрудников на язык синтаксиса СП, то соответствующий код может выглядеть следующим образом:

// Добавляем в пространство имен Employees два новых производных класса

namespace Employees

{

public class Manager: Employee

{

// Менеджерам необходимо знать количество имеющихся у них опционов на акции

private ulong numberOfOptions;

public ulong NumbOpts

{

get { return numberOfptions; }

set { numberOfOptions = value; }

}

}

public class Salesperson: Employee

{

// Продавцам нужно знать объем своих продаж

private int numberOfSales;

public int NumbSales

{

get { return numberOfSales; }

set { numberOfSales = value; }

}

}

 

В нашей ситуации оба производных класса расширяют функциональность базового класса за счет добавления свойств, уникальных для каждого класса. Поскольку мы использовали отношение «быть», то классы Salesperson и Manager автоматически наследовали от класса Employee все открытые члены. Это несложно проверить:

// Создаем объект производного класса и проверяем его возможности

public static int Main(string[ ] args)

{

// Создаем объект «продавец»

SalesPerson stan = new SalesPerson();

// Эти члены унаследованы от базового класса Employee

stan.EmpID = 100;

stan.SetFullName("Stan the Man");

// А это - уникальный член, определенный только в классе Salesperson

stan.NumbSales = 42;

return 0;

}

Конечно же, через объект производного класса нельзя напрямую обратиться • закрытым членам, определенным в базовом классе:

// Ошибка! Через объект производного класса нельзя обращаться к закрытым членам,

// определенным в базовом классе 

Salesperson stan = newSalesPersonn();

stan.currPay;

Применение модели включения-делегирования

Как уже говорилось, в объектно-ориентированных языках программирования исполъзуются две главные разновидности наследования. Первая из них называется классическим наследованием (моделью «быть» — is-a), и эта модель была рассмотрена в предыдущем разделе. Вторая разновидность — это модель включения-делегирования (модель «иметь» — has-a), и именно ей посвящен настоящий раздел.

Для  начала нам потребуется простой класс, представляющий автомобильный радиоприемник:

// Этот класс будет внутренним, включенным в другой класс - Саг

pubic class Radio

{

public Radio( ){}

public void TurnOn(bool on)

{

if(on)

Console.WriteLine(“Jamming.    ");

else

Console.WriteLine(“Quiet time . ");

}

}

Теперь предположим, что вам потребовалось разработать программную модель автомобиля. Класс Саг, который мы будет использовать для этой цели, должен содержать сведения об автомобиле (как мы его именуем, его скорость в настоящий момент и максимально допустимую скорость). Все эти данные можно задавать при помощи пользовательского конструктора. Определение класса Саг может выглядеть следующим образом:

//Этот класс будет выступать в роли внешнего класса, класса-контейнера для Radio

public class Car

{

private int currSpeed;

private int maxSpeed;

private string petName;

bool dead;                                         // Жива ли машина или уже нет

public Саг()

{

maxSpeed = 100;

dead = false;

}

public Car(string name, int max, int curr)

{

currSpeed = curr;

maxSpeed = max;

petName = name;

dead = false;

}

public void SpeedUp (int delta)

{ ……   }

Сейчас в нашем распоряжении есть два независимых класса — Radio для авто мобильного радиоприемника и Саг для самого автомобиля Понятно, что эти два класса должны взаимодействовать друг с другом и эти отношения желательно как то зафиксировать Однако вряд ли нам удаст ся применить в этом случае класси ческое наследование трудно производить машину от радиоприемника ичи радио приемник от машины В этой ситуации, конечно больше подойдет отношение включения-делегирования пусть машина включает в себя радио и передает этому классу необходимые команды В терминологии объектно ориентированного про граммирования контейнерный класс (в нашем случае Саг) называется родитель ским (parent), а внутренний класс, который помещен внутрь контейнерного (это конечно Radio), называется дочерним (child).

Помещение радиоприемника внутрь автомобиля влечет за собой внесение в опре деление класса Саг следующих изменений:

// Автомобиль «имеет» (has а) радио

public class Car

{

. . . . .  .

// Внутреннее радио

private Radio theMusicBox;

}

Обратите внимание, что внутренний класс Radi о был объявлен как private С точки зрения инкапсуляции мы делаем все правильно Однако при этом неизбежно возникает вопрос: а как нам включить радио? Переводя на язык программировая — а как внешний мир будет взаимодействовать с внутренним классом? Понятнo, что ответственность за создание объекта внутреннего класса несет внешний контейнерный класс. В принципе код для создания объектов внутреннего класса можно помещать куда угодно, но обычно он помещается среди конструкторов контейнерного класса:

// За создание объектов внутренних классов ответственны контейнерные классы

public class Car

{

. . .

// Встроенное радио

private Radio theMusicBox;

 

public Car( )

{

maxSpeed = 100;

dead = false;

// Объект внешнего класса создаст необходимые объекты

//внутреннего класса  при собственном создании

    theMusicBox = new Radio( );  // Если мы этого не сделаем theMusicBox

        // начнет свою жизнь с нулевой ссылки

}

public Car(string name, int max, int curr)

{

currSpeed = curr;

maxSpeed = max;

petName = name;

dead = fales;

theMusicBox = new RadioO;

}

. . . .

}

Произвести инициализацию средствами С# можно и так:

// Автомобиль «имеет» (has-a) радио 

public class Car

{

. . . .

// Встроенное радио

private Radio theMusicBox = new Radio( );

 . . . .

}

Таким образом, радиоприемник внутри автомобиля у нас теперь создается есте с автомобилем. Однако вопрос о том, как именно можно включить этот раприемник, остался нерешенным. Ответ на него выглядит так:- для того чтобы воспользоваться возможностями внутреннего класса, необходимо делегирование (delegation). Делегирование заключается в простом добавлении во внешний контейнерный класс методов для обращения ко внутреннему классу, шример:

// Во внешний класс добавляются дополнительные открытые методы и другие члены

// которые обеспечивают доступ к внутреннему классу

public class Car

{

. . . .

public void CrankTunes(bool state)

{

// Передаем (делегируем) запрос внутреннему объекту

theMusicBox.TurnOn(state);

}

}

В приведенном ниже коде обратите внимание на то, что пользователь косвенно обращается к скрытому внутреннему объекту, даже не подозревая о том, что в недрах объекта Саг существует закрытый (определенный как private) объект Radio:

// Выводим автомобиль на пробную поездку

public class CarApp

{

public static int Main(string[ ] args)

{

// Создаем автомобиль (который, в свою очередь, создаст радио)

Саг c1;

c1 = new Car( “SlugBug”,  100, 10);

// Включаем радио (запрос будет передан внутреннему объекту)

c1.CrankTunes(true);

// Ускоряемся

for(int i=0; i < 10; i++)

c1.SpeedUp(20);

// Выключаем радио (запрос будет вновь передан внутреннему объекту

c1.CrankTunes(false);

return 0;

}

}

Поддержка полиморфизма в С#

Предположим, что в базовом классе Employee определен метод GiveBonus( ) — поощрить:

// В классе Employee определен новый метод для поощрения сотрудников

public class Employee

{

public void GiveBonus(float amount)

{

currPay += amount; 

}

. . . .

}

Поскольку этот метод определен в базовом классе как publiс, вы теперь можете поощрять продавцов и менеджеров:

// Поощрения объектам производных классов

Manager chucky = new Manager(“Chucky", 92, 100000, "333-23-2322", 9000);

Chucky.GiveBonus(300);

Chucky.DisplayStats( );

Salesperson fran = new SalesPerson("Fran", 93, 3000, "932-32-3232", 31);

Fran.GiveBonus(200);

Fran.DisplayStats( );

.

Проблема заключается в том, что унаследованный метод GiveBonus( ) пока работает абсолютно одинаково в отношении объектов обоих производных клас-— и объектов Salesperson, и объектов Manager. Однако, конечно, было бы лучше, чтобы для объекта каждого класса использовался свой уникальный вариант метода. Например, при поощрении продавцов можно учитывать их объем продаж. Менеджерам помимо денежного поощрения можно выдавать дополнительные опционы на акции. Поэтому задачу можно сформулировать так: «Как заставить один и тот же метод по-разному реагировать на объекты разных классов?»

Для решения этой задачи в С# предусмотрено понятие полиморфизма. Полиморфизм позволяет переопределять реакцию объекта производного класса на метод, определенный в базовом классе. Для реализации полиморфизма в нашем приложении мы воспользуемся ключевыми словами С#: virtual и override. Если базовый класс определяет метод, который должен быть замещен в производном классе, этот метод должен быть объявлен как виртуальный (конечно, при помощи ключевого слова virtual):

public class Employee

{

// Для метода GiveBonus( ) предусмотрена реализация по умолчанию.

// однако он может быть замещен в производных классах

public virtual void GiveBonus(float amount)

{

currPay += amount;

}

. . . .

}

Если вы хотите переопределить виртуальный метод, необходимо заново определить метод в производном классе, использовав ключевое слово override:

public class Salesperson :  Employee

{

// На размер поощрения продавцу будет влиять объем его продаж

public override void GiveBonus(float amount)

{

int salesBonus = 0;

if(numberOfSales >= 0 && numberOfSales <=100)

salesBonus = 10;

else if(numberOfSales >= 101 && numberOfSales <= 200)

salesBonus = 15;

 else

salesBonus = 20;    // Для объема продаж больше 200

base.GiveBonus(amount * salesBonus);

}

. . . .

}

public class Manager  Employee

{

private Random r = new Random( );

// Помимо денег менеджеры также получают некоторое количество опционов

// на акции

public override void GiveBonus(float amount)

{

// Деньги: увеличиваем зарплату

base.GiveBonus(amount);

// Опционы на акции: увеличиваем их количество

numberOfOptions += (ulong)r.Next(500);

}

. . . .

}

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

Метод Employee.DisplayStats( ) у нас также определен как virtual, и он замещен в производных классах таким образом, чтобы показывать текущий объем продаж, если речь идет о продавце, или количество опционов,-имеющееся в настоящее время в распоряжении менеджера. Теперь, когда для каждого из производных классов определены собственные варианты этих двух методов, объекты разных классов ведут себя по-разному:

// Улучшенная система поощрений!

Manager chucky = new Manager("Chucky", 92, 100000, "333-23-2322", 9000);

chucky.GiveBonus(300);

chucky.DisplayStats( );

Salesperson fran = new SalesPerson(“Fran", 93, 3000, "932-32-3232", 31);

fran.GiveBonus(200);

fran.DisplayStats( );

Мы с вами научились использовать полиморфизм для переопределения поведения производных классов. Однако, как, наверное, вы догадываетесь, возможности полиморфизма этим далеко не исчерпываются.

Абстрактные классы

В настоящее время базовый класс Employee выполняет в нашей программе понятные и логичные функции: он обеспечивает производные классы набором защищенных переменных, а также предоставляет реализации по умолчанию двух виртуальных методов— GiveBonus( ) и DisplayStats( ), замещенных в производных классах. Однако в нашей программе до сих пор существует потенциальный источник ошибок: мы можем создавать объекты базового класса Employee:

// А это кто такой?

Employee X = new Employee( );

«Просто сотрудников» у нас быть не должно — каждой из возможных категорий сотрудников у нас соответствует производный класс. Поэтому вполне логичным будет просто запретить создание объектов класса Employee. В С# для этого достаточно объявить класс абстрактным:

// Объявляем класс Employee абстрактным, запрещая создание объектов этого класса abstract public class Employee

{

// Открытые интерфейсы и внутренние данные класса

}

Теперь при попытке создания объекта класса Employee компилятор будет выдавать сообщение об ошибке:

// Ошибка! Нельзя создавать экземпляры абстрактного класса

Employee X = new Employee( );

Принудительный полиморфизм: абстрактные методы

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

Рис. 3.14. Иерархия классов геометрических фигур

Скорее всего, вы спросите: «А зачем это нужно?» Для ответа на этот вопрос мы вернемся к иерархии геометрических фигур, с которой мы уже имели дело в этой главе (рис. 3.14).

Как и в случае с классом Employee, желательно явным образом запретить создание объектов класса Shape. Это можно сделать следующим образом:

 namespace Shapes 

{

public abstract class Shape 

{

// Пусть каждый объект-геометрическая фигура получит у нас дружеское прозвище:

protected string petName;

// Конструкторы

public Shape( )  {petName = "NoName";}

public Shape(string s) {petName = s; }

// Метод Draw( ) объявлен как виртуальный и может быть замещен

public virtual void Draw( )

{

Console.WriteLine("Shape.Draw()”);

}

public string PetName

{

get {return petName; }

set {petName = value;}

}

}

// В классе Circle метод Draw( ) HE ЗАМЕЩЕН 

public class Circle : Shape

{

public Circle( ) {}

public Circle(string name): base(name) {}

}

// В классе Hexagon метод Draw( ) ЗАМЕЩЕН

public class Hexagon : Shape

{

public Hexagon( ) {}

public Hexagon(string name) : base(name) {}

public overrnde void Draw( )

{

Console.WriteLine("Drawing {0} the Hexagon",   PetName);

}

}

Обратите внимание, что в классе Shape определен виртуальный метод Draw( ). Как мы видим, виртуальные методы можно замещать в производных классах при помощи ключевого слова override (как это сделано в определении класса Hexagon). Однако в С# виртуальные методы можно и не замещать в производных классах (например, в определении класса Circle виртуальный метод Draw( ) базового класса остался незамещенным). При этом в случае вызова метода Draw( ) для объекта класса Hexagon будет вызван уникальный вариант этого метода для класса Hexagon, а если мы вызовем тот же метод для объекта класса Circle, этот метод будет выполнен в соответствии со своим определением в базовом классе:

// В объекте Circle реализация базового класса для Draw( ) не замещена

public static int Main(string[ ] args)

{

// Создаем и рисуем шестиугольник

Hexagon hex = new Hexagon('Beth");

hex.Draw( );

Circle cir = new Circle("Cindy");

// М-мм! Придется использовать реализацию Draw( ) базового класса

cir.Draw( );

  }

Если нам необходимо гарантировать, что каждый производный класс обязательно заместит метод Draw( ), то мы должны объявить метод Draw( ) в базовом классе Shape абстрактным. Абстрактные методы в С# работают так же, как чистые виртуальные функции в C++ — для них даже не надо указывать реализацию по умолчанию:

// Каждая геометрическая фигура теперь ОБЯЗАНА самостоятельно определять

// метод Draw( )

public abstract class Shape

{

. . . .

// Метод Draw( ) теперь определен как абстрактный (обратите внимание

// на точку с запятой)

public abstract void Draw( );

public string PetName

{

get {return petName;}

set {petName = value;}

}

}

Теперь мы обязаны определить метод Draw( ) в классе Сirclе:

// Если мы не заместим в классе Circle абстрактный метод Draw( ). класс Circle будет

// также считаться абстрактным и мы не сможем создавать объекты этого класса!

public class Circle : Shape

{

public Circle( ) {}

public Circle(string name): base(name) {}

// Теперь метод Draw( ) придется замещать в любом производном непосредственно

//от Shape классе

public override void Draw( )

{

Console.WriteLine(“Drawing {0} the Cricle", PetName);

}

}

Теперь мы можем воспользоваться полной мощью полиморфизма С#, когда конкретная реализация метода будет выбираться автоматически в зависимости от того, для объекта какого класса был вызван этот метод:

// Создаем массив объектов разных геометрических фигур

public static int Main(string[ ] args)

{

// Массив фигур

Shaped s  = {new Hexagon( ), new Hexagon("Freda"), new Circle( ), new Circle(“JoJo")};

// Проходим с помощью цикла по всем элементам массива и просим нарисовать

// каждый объект

for(int i = 0; i < s.Length; i++)

s[i].Draw( );

. . . .

}

Этот пример очень хорошо иллюстрирует суть полиморфизма. Как мы помним, объекты абстрактных классов создавать невозможно. Однако вы вполне можете хранить ссылки на любой производный класс, используя для этого переменную абстрактного класса. В нашем примере при прохождении по элементам массива геометрических фигур нужный вариант метода выбирается непосредственно в момент выполнения программы в зависимости от класса, к которому принадлежит объект.

Приведение типов в С#

К настоящему моменту мы создали уже не одну развитую иерархию типов. При этом С# позволяет приводить (cast) один тип к другому, то есть осуществлять преобразование объектов одного типа в объекты другого типа. Рассмотрению правил приведения типов в С# и посвящен этот раздел.

Рассмотрим приведение типов на простом примере. Вспомним нашу иерархию классов сотрудников. Конечно же, на самой вершине этой иерархии стоит класс System Object — в С# все типы производятся от этого класса. Можно сказать, используя терминологию классического наследования, что все типы являются is-a-объектами. Кроме того, в нашей иерархии существуют и другие отношения классического наследования. Например, PTSalesPerson (продавец на неполный рабочий день) является is-a-продавцом Salesperson и т. д.

Первый закон приведения типов звучит так: если один класс является производным от другого, всегда безопасно ссылаться на объект производного класса через объект базового класса. В результате мы можем использовать в С# весьма мощные программные конструкции. Например, если у нас определен метод для увольнения сотрудника:

public class TheMachine 

{

public static void FireThisPerson(Employee e)

{

// Удаляем сотрудника из базы данных

// Отбираем у него ключ и точилку для карандашей

}

}

В соответствии с правилами приведения типов мы можем передавать методу FireThisPerson( ) объект как самого типа Employee, так и любого производного от Employ ее типа:

// Производим сокращение персонала

TheMachine.FireThisPerson(e);

TheMachine.FireThisPerson(sp);

Этот код будет выполнен без ошибок, поскольку здесь производится неявное приведение от базового класса (Employee) к производному. Однако что, если вы также хотите уволить объект класса Manager (который в настоящее время хранится через ссылку на объект базового класса)? Если вы попробуете передать ссылку на объект (типа System.Object) нашему методу FireThisPerson( ), то вы получите сообщение об ошибке компилятора:

// Класс Manager - производный от System Object поэтому мы имеем право провести

// следующую операцию приведения

object о = new Manager("Frank Zappa", 9, 40000,  “111-11-1111", 5);

TheMachine.FireThisPerson(o);                      // Ошибка компилятора!

Причина ошибки кроется в определении метода FireThisPerson( ), который принимает объект типа Employee. Чтобы этой ошибки не возникало, нам необходимо явно привести объект базового класса System.Object к производному типу Employee (учитывая происхождение нашего объекта о, это вполне возможно):

// Здесь будет ошибка - вначале нужно провести явное приведение типов:

// FireThisPersonO

// А вот так проблем не возникнет:

FireThisPerson((Employee)o);

Приведение числовых типов

Приведение числовых типов подчиняется примерно тем же правилам, что и приведение классов. Если вы пытаетесь привести «больший» числовой тип к «меньшему» (например, int — в byte), необходимо провести явное преобразование:

int х = 30000;

byte b -= (byte)x;                  // Возможна потеря данных

Нас можно поздравить: теперь мы уже умеем создавать сложные иерархии пользовательских типов в С#. Но прежде чем перейти к ним, мы должны рассмотреть еще два аспекта, тесно связанных с проектированием классов: обработку ошибок и управление памятью.

Обработка исключений

В течение множества лет обработка ошибок превращалась разработчиками, использующими Windows, в сложную смесь различных приемов. Множество программистов реализовывало свою собственную логику обработки ошибок в рамках одно-го-единственного конкретного приложения. Например, команда разработчиков могла определить свой набор констант для представления известных условий возникновения ошибок и использовать их как значения, возвращаемые методами. Помимо этого в Windows API было определено большое количество кодов ошибок, которые должны были обрабатываться при помощи #define, HRESULTS и множества прочих средств. Многие СОМ-разработчики использовали набор стандартных СОМ-интерфейсов для возвращения значимой информации об ошибках клиентам СОМ.

Очевидная проблема всех этих приемов обработки ошибок заключается в том, что все они — разные. Каждый прием привязан к конкретной технологии, конкретному языку или конкретному проекту. Чтобы наконец навести порядок во всем этом, .NET предлагает единую технику для обнаружения ошибок времени выполнения и передачи сообщений о них: это — структурированная обработка исключений (Structured Exception Handling, SEH).

Преимущества этого метода заключаются в том, что в распоряжение всех разработчиков предоставляется единый и хорошо продуманный подход к обработке ошибок, который к тому же является общим для всех языков .NET. Таким образом, разработчик, использующий С#, будет реализовывать обработку ошибок точно так же, как программист, использующий VB.NET или МС++, и все остальные разработчики, использующие платформу .NET. Разработчики также получают дополнительную возможность генерировать и перехватывать исключения между двоичными файлами, AppDomains (о них будет сказано — в главе 6) и компьютерами в независимом от языка стиле.

Для того чтобы понять, как применять исключения в С#, в первую очередь необходимо осознать, что исключения в С# —^это объектьь_Все системные и пользовательские исключения в С# производятся от класса System Exception (который, в свою очередь, производится от класса System Object). В табл. 3.1 представлен перечень наиболее интересных свойств класса Exception.

Жизненный цикл объектов

В С# общий принцип управления памятью формулируется очень просто: для создания объекта в области «управляемой кучи» (managed heap) используется ключевое слово new. Среда выполнения .NET автоматически удалит объект тогда, когда он больше не будет нужен. Правило в целом вполне понятно, однако возникает один дополнительный вопрос: а как среда выполнения определяет, что объект больше не нужен? Короткий (то есть неполный) ответ гласит, что среда выполнения удаляет объект из памяти, когда в текущей области видимости больше не остается активных ссылок на этот объект. Например:

// Создаем локальный объект класса Саг 

public static int Main(string[ ] args)

{

// Помещаем объект класса Саг в управляемую кучу

Саг сЗ = new Car(“Viper", 200, 100);

. . . .

return 0;

} // Если сЗ - единственная ссылка на этот объект, то начиная с этого момента

//он может быть удален

Предположим, что в вашем приложении создано (размещено в оперативной памяти) три объекта класса Саг. Если в управляемой куче достаточно места, мы получим три активные ссылки — по одной на каждый объект в оперативной памяти. Каждая такая активная ссылка на объект в памяти называется также корнем (root). To, ч го у нас получилось, схематически представлено на рис. 3.21.

Рис. 3.21. Ссылки указывают на местонахождение объектов в управляемой куче

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

// Создаем объекты Саг таким образом, чтобы отреагировать на возможную нехватку места //в управляемой куче

public static int Main(string[ ] args)

{

. . . .

Car yetAnotherCar;

try

{

yetAnotherCar = new Car( );

}

catcht(OutOfMemoryException e)

{

Console.WriteLine(e.Message);

Console.WriteLine(“Managed heap is FULL1 Running GC ..");

}

return 0;

}

Вне зависимости от того, насколько осторожно вы будете создавать объекты, как только место в управляемой куче заканчивается, автоматически запускается сборщик мусора (garbage collector, GC). Сборщик мусора оценивает все объекты, размещенные в настоящий момент в управляемой куче, с точки зрения того, есть ли в области видимости приложения активные ссылки на них. Если активных ссылок на какой-либо объект больше нет или объект установлен в null, этот объект помечается для удаления, и в скором времени память, занимаемая подобными обьектами, высвобождается.

Завершение ссылки на объект

Та схема управления памятью, которая была рассмотрена выше, обладает одной важной особенностью, которая имеет как положительные, так и отрицательные стороны: она работает полностью автоматически. С одной стороны, это упрощает процесс программирования. С другой — нас может не устраивать то, что процесс удаления объектов (закрытия соединения с базой данных, окна Windows и т. п.) будет происходить в соответствии с неизвестным нам алгоритмом. Например, если тип Саг устанавливает в процессе выполнения соединение с удаленным компьютером, скорее всего, мы захотим, чтобы эго соединение разрывалось в соответствии с установленными нами правилами.

Если вам нужно обеспечить возможность удаления объектов из оперативной памяти в соответствии с определенными вами правилами, первое, о чем вам необходимо позаботиться — о реализации в вашем классе метода System.Object.Finаlize( ). Заметим между прочим, что реализация этого метода по умолчанию (в базовом классе) ничего не дает. Однако, как это ни странно, в С# запрещено напрямую замещать метод System.Object.Finаlize( ). Более того, вы даже не сможете вызвать в вашем приложении этот метод напрямую! Если вы хотите, чтобы ваш пользовательский класс поддерживал метод Finalize( ), вы должны использовать в определении этого класса метод, очень похожий на деструктор C++:

// Что-то очень знакомое

public class Car : Object

{

// Деструктор С#?

~Саг( )

{

// Закрывайте все открытые объектом ресурсы!

// Далее в С# будет автоматически вызван метод Base FinalizeO

}

}

Если у вас есть опыт работы с C++, то подобный синтаксис вам покажется знакомым. В C++ деструктор класса — это специальный метод класса, имя которого выглядит как имя класса, перед которым стоит символ тильды (~). В C++ гарантируется, что этот метод будет вызван, когда ссылка на объект выходит за пределы области видимости (для типов, размещенных в стеке) или к объекту применяется оператор delete (для объектов, размещенных в области динамической памяти).

При размещении объекта С# в управляемой куче при помощи оператора new среда выполнения автоматически определяет, поддерживает ли ваш объект метод Finalize( ) (представленный в С# с помощью «деструктороподобного» синтаксиса). Если этот метод поддерживается объектом, ссылка на этот объект помечается как «завершаемая» (finalizable). При этом в специальной внутренней очереди «завершения» (finalization queue) помещается указатель на данный объект. Когда сборщик мусора приходит к выводу, что наступило время удалять данный объект из оперативной памяти, он обращается к этому указателю и запускает деструктор С#, определенный для этого класса, прежде чем будет произведено физическое удаление объекта из памяти.

Интерфейс IDisposable

Чтобы обеспечить единообразие методов, освобождающих ресурсы в разных клас-ах, библиотеки классов .NET определяют интерфейс IDisposable, который содержит единственный член — метод Dispose( ):

public interface IDisposable

{

public void Dispose();

}

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

// Реализуем IDisposable

public Car : IDisposable

{

// Это - единственный метод, который пользователь объекта должен вызвать

// вручную

public void Dispose( )

{

// ...Закрываем открытые внутренние ресурсы

}

}

Используя этот подход, мы предоставляем пользователю возможность в любой момент высвобождать наиболее ценные ресурсы, не загружая дополнительно оче-эедь завершения. Кроме того, мы вполне можем сочетать применение интерфейса .Disposable (с методом Dispose( )) и пользовательского деструктора С#.

Взаимодействие со сборщиком мусора

Как и все в мире .NET, сборщик мусора — это объект, и мы можем обращаться к нему через ссылку на объект. Для работы со сборщиком мусора в С# предназначен :пециальный класс — System.GC (от garbage collector — сборщик мусора). Этот класс эпределен как sealed, то есть производить от него другие классы при помощи наследования невозможно. В System.GC определен небольшой набор статических членов, при помощи которых и осуществляется взаимодействие со сборщиком мусо-эа. Самые важные из этих членов представлены в табл. 3.2.

Обратите внимание, что этот вариант класса Саг поддерживает как деструктор С#, так и интерфейс IDisposable. Метод Dispose( ) определен таким образом, чтс при его выполнении происходит вызов метода GC.SupressFinalize( ). Таким образом, мы сообщаем системе, что деструктор для данного объекта вызывать уже не нужно — все ресурсы будут освобождены при помощи метода Dispose( ).


 

А также другие работы, которые могут Вас заинтересовать

72965. Организация менеджмента в аудиторской деятельности 263.49 KB
  Сегодня в Российской Федерации действует большое количество аудиторских фирм, и перед экономическими субъектами стоит проблема выбора. Самый простой способ выбрать аудиторскую фирму — довериться рекламе.
72966. План рахунків бухгалтерського обліку 57.5 KB
  Завданням керівництва є контроль за інформацією про склад засобів підприємства, джерел їх формування, господарських процесів і фінансових результатів діяльності. Для цього необхідно використовувати в бухгалтерському обліку багато різних рахунків.
72967. МЕТОДИ АДМІНІСТРАТИВНОГО МЕНЕДЖМЕНТУ 55.5 KB
  Методи адміністративного менеджмента - це сукупність прийомів та засобів застосування яких дозволяє забезпечити ефективне функціонування керуючої та керованої підсистем системи управління.
72968. АНАЛІТИЧНЕ ТА КОНСАЛТИНГОВЕ ЗАБЕЗПЕЧЕННЯ АДМІНІСТРАТИВНОГО МЕНЕДЖМЕНТУ 57.5 KB
  Вона потребує максимального концентрування на певному завданні досконалого знання предметної області оперативного вивчення ситуації розуміння конкретної проблеми і процесів її розвитку застосування різноманітних методів й прийомів аналізу певного часу тощо.
72969. Функции. Правила организации функций 85.5 KB
  При вызове функции ей при помощи аргументов (формальных параметров) могут быть переданы некоторые значения (фактические параметры), используемые во время выполнения функции. Функция может возвращать некоторое (одно!) значение.
72970. Ежелгi Римдегі құл иеленуші мемлекет және құқығы 30.94 KB
  2 кезең - рим құқығының гүлдену кезеңі. Бұл кезде цивтльдік құқықпен қатар преторлық құқық пайда болады, оның ішінен жалпы халықтық немесе халықтар құқығы бөлініп шықтығ, бұл құқық римдік азаматтармен шетелдік азаматтар арасындағы, сонымен қатар Рим мемлекетінің аумағында...
72971. Пожежна безпека виходу горючих речовин із нормально працюючого технологічного обладнання 315 KB
  Горючі гази пари і рідини виходять з апаратів і трубопроводів у виробниче приміщення або на відкриті площадки не тільки при ушкодженнях та аваріях але і при нормальній експлуатації апаратів що мають свої конструктивні особливості.
72972. Inflammation 47.5 KB
  Inflammation is defined as the local response of living mammalian tissues to injury due to any agent. It is body defense reaction in order to eliminate or limit the spread of injurious agent. The agents causing inflammation may be as under: Physical agents like heat, cold, radiation, mechanical trauma.