NET программирование
Главная Новости
Framework .NET
Visual Studio .NET Самоучитель C# Учебники О сайте
Что такое Microsoft .NET?
Основы технологии .NET
Программирование на управляемом C++
Объектно-ориентированное программирование на управляемом C++
Управляемый C++ в .NET Framework
Создание графических пользовательских интерфейсов
Сборки и развертывание
Классы каркаса .NET Framework
Программирование в ADO.NET
ASP.NET и Web-формы
Web-службы
Web-узлы и Web-службы,работающие на основе ATL Server
Защита
Трассировка и отладка в .NET
Смешивание управляемого и неуправляемого кода
Приложение. Visual Studio.NET
Управляемый C++ в .NET Framework

Язык C++ — мощный инструмент разработки программ, оказавший огромное влияние на развитие вычислительной науки. Управляемые (managed) расширения от Microsoft добавили в язык C++ целый новый мир — мир .NET. Для того чтобы полностью использовать возможности Visual C++ .NET, необходимо понимать, как он работает с .NET Framework. Мы начнем рассмотрение с базового класса Object (Объект) из пространства имен System. Затем рассмотрим коллекции, а также методы класса Object (Объект), которые следует перегрузить для использования возможностей, предоставляемых .NET Framework. Далее познакомимся с интерфейсами, позволяющими строго определить свойства реализуемых классов. В управляемом C++ класс может реализовывать несколько интерфейсов, даже при том, что он может быть потомком только одного суперкласса. Интерфейсы позволяют применять динамическое программирование; во время выполнения программы можно послать классу запрос для того, чтобы узнать, поддерживает ли он определенный интерфейс.

Будут подробно рассмотрены интерфейсы, поддерживающие использование коллекций. Потом остановимся на видах копирования. Вместо применения конструкторов копирования, как это делается в обычном C++, в управляемом C++ для реализации копирования используется интерфейс ICloneable. Мы рассмотрим родовые интерфейсы в методологии программирования .NET Framework и сравним использование компонентов .NET и СОМ. Более полно использование родовых интерфейсов иллюстрируется на примерах различных сортировок с помощью интерфейса IComparable. Этот пример позволяет также почувствовать удобство работы с каркасом приложений, определяющим архитектуру программ, а не являющимся просто библиотекой классов, в которой имеются некие полезные функции. При использовании каркаса приложений программа может вызывать методы каркаса, а те могут вызывать методы программы. Поэтому создаваемый код можно уподобить сандвичу. Этот пример помогает понять, для чего необходима платформа .NET. Функции обратного вызова применяются в программировании уже много лет. Управляемый C++ использует эту концепцию в работе с делегатами и событиями. Здесь представлены два простых и понятных примера: моделирование фондовой биржи и диалоговая комната для дискуссий (чат-программа).

Как мы уже знаем, каждый управляемый (managed) тип (объявленный с префиксом _дс (сборщик мусора)) в управляемом C++ в конце концов является потомком корневого класса System::Object (Система::Объект). Даже такие традиционные примитивные типы, как System:: Int32, System::SByte и System:: Double являются потомками System::ValueType, а тот, в свою очередь— потомок System: :Object (Система-Объект). Кроме упомянутых, в .NET Framework имеется свыше 2500 классов, и все они — потомки Ob j ect (Объект).

Есть четыре общедоступных метода экземпляров класса Object (Объект), три из которых являются виртуальными и часто подменяются в управляемых классах.

Метод Equals (Равняется)

public: virtual bool Equals(Object*); 
// виртуальный логический (булев) Равняется (Объект *); 

Этот метод сравнивает объект, указатель на который передается методу в качестве аргумента, с текущим объектом. В случае равенства объектов метод возвращает true (истина). В классе Object (Объект) данный метод сравнивает только указатели на объекты. В классе ValueType этот метод подменен и производит сравнение содержимого объектов. Многие классы, в свою очередь, подменяют этот метод для того, чтобы приспособить условия сравнения объектов к своим нуждам. Существует также и статическая версия метода Equals (Равняется), сравнивающая два объекта, указатели на которые передаются ему в качестве аргументов.

Метод ToString

public: virtual String* ToString(); // виртуальный 

Этот метод возвращает строку, содержащую удобочитаемое для человека описание объекта. Принятая по умолчанию версия, реализованная в объекте Object (Объект), возвращает просто полное имя типа. Производные классы часто перегружают данный метод, чтобы возвращаемое описание объекта было более содержательным.

Метод GetHashCode

public: virtual int GetHashCode(); // виртуальный 

Этот метод возвращает значение хеш-функции объекта, который может использоваться в алгоритмах хэширования и хэш-таблицах. Классы, подменяющие данный метод, должны также подменять метод Equals (Равняется) для того, чтобы равные объекты возвращали одинаковые значения хеш-функции. Если этого не сделать, класс Hashtable (Хэш-таблица) не сможет корректно работать с объектами используемого класса.

Метод CetType

public: Type* GetType(); 

Этот метод возвращает информацию о типе объекта. Такая информация может быть использована для получения связанных метаданных посредством отражения (reflection), которое мы рассмотрим в главе 8 "Классы каркаса .NET Framework". Это метод не виртуальный, поэтому подменять его обычно не приходится.

Защищенными являются два метода класса Object (Объект). Эти методы могут использоваться только производными классами.

Метод MemberwiseClone

protected: Object* MemberwiseClone(); // защищенный 

Данный метод создает поверхностную (shallow) копию объекта. Это метод не виртуальный, поэтому подменять его обычно не приходится. Для того чтобы сделать детальную (deep) копию, следует использовать интерфейс ICloneable. Разница между поверхностной и детальной копией будет рассмотрена в этой главе несколько позже.

Метод Finalize (Завершить)

-Object(); 

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

Если вы знакомы с языком Smalltalk или ему подобными, набор возможностей, реализованных непосредственно в классе Object (Объект), может показаться вам весьма ограниченным. В языке Smalltalk, в котором использована концепция иерархии классов, являющихся потомками одного базового класса, набор методов, реализованных в Object (Объект), весьма широк. Я насчитал 38 методов! Эти методы осуществляют различные действия, такие, как сравнение и копирование объектов. Библиотека классов .NET Framework содержит и подобные методы, и еще множество других. Но вместо того, чтобы вводить их в базовый класс, .NET определяет набор стандартных интерфейсов, которые при желании может реализовывать класс. Такая организация, используемая также в технологии COM (Component Object Model — модель компонентных объектов Microsoft) от Microsoft и в языке Java, очень гибка. В этой главе мы рассмотрим некоторые родовые интерфейсы .NET Framework.

Для иллюстрации описанных методов рассмотрим класс Customer (Клиент) до и после перегрузки методов Equals (Равняется), ToString и GetHashCode.

Стандартные методы класса object (Объект)

Если не произвести подмены виртуальных методов объекта Object (Объект), наш класс будет наследовать стандартное поведение. Это поведение продемонстрировано в CustomsrObjectXStepl.

//Customer.срр 
#using  
using namespace System; 
// использование пространства имен Система; 
#include "Customer.h" 
//Customer.h 
_gc class Customer // сборщик мусора - класс Клиент 
{ 
public: 
int nCustomerld; 
String *pFirstName; 
String *pLastName; 
String *pEmailAddress; 
Customer(int id. String *pFirst, // Клиент (int идентификатор, 
// Строка *pFirst, String *pLast, String *eMail) 
{ 
nCustomerld = id; // идентификатор 
pFirstName = pFirst; 
pLastName = pLast; 
pEmailAddress = eMail; // электронная почта 
} 
}; 
//TestCustomer.срр 
ftusing  
using namespace System; 
// использование пространства имен Система; 
#include "Customer.h" 
#include "TestCustomer.h" 
void main(void) 
{ 
TestCustomer::Main(); // Главный } 
//TestCustomer.h 
_gc class TestCustomer 
// класс сборщика мусора TestCustomer 
{ 
public: 
static void Main() // статическая Главная 
{ 
Customer *pCustl, *pCust2; // Клиент 
pCustl = new Customer ( // новый Клиент 
99, "John", "Doe", "john@rocky.com"); // "Джон", "Доу" 
pCust2 = new Customer( // новый Клиент 
99, "John", "Doe", "john@rocky.com"); // "Джон", "Доу" 
ShowCustomerObject("pCustl", pCustl); 
ShowCustomerObject("pCust2", pCust2); 
CompareCustomerObjects(pCustl, pCust2); 
} 
private: // частный 
static void ShowCustomerObject( String *pLabel, Customer *pCust) 
{ 
Console::WriteLine("——— {0} ——", pLabel); 
Console::WriteLine( 
"ToStringO = {0}", pCust->ToString()); 
Console::WriteLine( 
"GetHashCodeO = {0}", _box(pCust->GetHashCode() ) ) ; 
Console::WriteLine( 
"GetTypeO = {0}", pCust->GetType () ) ; 
} 
static void CompareCustomerObjects( Customer *pCustl, Customer *pCust2) // Клиенты 
{ 
Console::WriteLine( 
"Equals() = {0}", // Равняется 
_box(pCustl->Equals( // Равняется 
dynamic_cast<0b]ect _gc *>(pCust2)))); // сборщик 
// мусора 
} 
}; 

Запустите испытательную программу, и вы получите следующий результат:

——— custl ——— // ———custl ——— 
ToString () = Customer // ToString () = Клиент 
GetHashCode () = 4 // GetHashCode () = 4 
GetType () = Customer // GetType () = Клиент 
——— cust2 ——— // ———cust2 ——— 
ToString () = Customer // ToString () = Клиент 
GetHashCode () = 6 // GetHashCode () = 6 
GetType () = Customer // GetType () = Клиент 
Equals() = False // Равняется () = Ложь 

Понятно, что реализация, принятая по умолчанию, — это совсем не то, что желательно иметь для объекта Customer (Клиент). Метод ToString возвращает имя класса, а не информацию о конкретном клиенте. Метод Equals (Равняется) сравнивает только указатели на объекты, а не сами объекты. В приведенном примере два разных указателя указывают на одинаковые (по содержащейся информации) объекты класса Customer (Клиент), но метод Equals (Равняется) при этом возвращает значение false (ложь).

Подмена методов класса object (Объект)

Версия проекта, содержащаяся в папке CustomerOb;ject\Step2, демонстрирует подмену виртуальных методов Ob] ect (Объект)

//Customer.h 
_gc class Customer 
// сборщик мусора - класс Клиент 
{ 
public: 
int nCustomerld; 
String *pFirstName, 
String *pLastName; 
String *pEmailAddress; 
Customer(int id, String *pFirst, // Клиент int идентификатор, 
// Строка *pFirst, String *pLast, String *eMail) 
{ 
nCustomerId = id; // идентификатор 
pFirstName = pFirst; 
pLastName = pLast; 
pEmailAddress = eMail; // электронная почта 
} 
public: 
bool Equals(Object *pobj) 
// логический (булев) Равняется (Объект *pobj) 
{ 
Customer *pCust = // Клиент 
dynamic_cast(pob]); 
return (pCust->nCustomerId == nCustomerld), 
} 
int GetHashCode() 
{ 
return nCustomerld, 
} 
String *ToStnng() 
{ 
return String::Format( // Формат 
"{0} {l}", pFirstName, pLastName); 
} 
}; 

Остальная часть программы не изменена Теперь результат выполнения программы будет таким:

--- custl --- // ---custl--- 
ToStnng () = John Doe // ToString () = Джон Доу 
GetHashCode () = 99 // GetHashCode () = 99 
GetTypeO = Customer // GetType () = Клиент 
--- cust2 --- // --- cust2 --- 
ToString () = John Doe // ToString () = Джон Доу 
GetHashCode () = 99 // GetHashCode () = 99 
GetType () = Customer // GetType () = Клиент 
Equals () = True // Равняется () = Истина 

Библиотека классов NET Framework предлагает широкий выбор классов для работы с коллекциями объектов Все эти классы находятся в пространстве имен System: Collections (Система Коллекции) и реализуют ряд различного типа коллекций, в том числе списки, очереди, массивы, стеки и хэш-таблицы В коллекциях содержатся экземпляры класса Object (Объект) Так как все управляемые типы происходят исключительно от Object (Объект), в коллекции может храниться экземпляр любого встроенного илирпределяемого пользователем типа

В этом разделе мы рассмотрим типичный представитель данного пространства имен — класс ArrayList (Список массивов), и научимся на практике использовать списки массивов. В частности, мы используем их для подходящей реализации нашего класса, экземпляры которого предполагается хранить в коллекции Мы увидим, что метод Equals (Равняется) нашего класса должен быть подменен, так как реализация любого из классов коллекций требует реализации метода Equals (Равняется)

Для начала приведем простой пример использования класса ArrayList (Список массивов) Как понятно из названия (Array List — Список массивов), ArrayList — это список объектов, хранимый подобно массиву Размер списка массивов может динамически изменяться, и может расти при добавлении новых элементов

Классы коллекций содержат экземпляры класса ОЬц ect (Объект) Мы создадим и будем иметь дело с коллекцией объектов Customer (Клиент) Использовать любой другой встроенный или определяемый пользователем управляемый тип ничуть не сложнее При использовании простого типа, такого, как int, экземпляр данного типа для сохранения в коллекции должен быть упакован (boxed), а перед его использованием — распакован обратно в int

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

Enter command, quit to exit 
H> help 
The following commands are available: 
register register a customer 
unregister unregister a customer 
email change email address 
show show customers 
quit exit the program 

Вот перевод:

Введите команду, quit для выхода из программы 
Н> help 
Доступны следующие команды: 
register (регистрировать) регистрирует клиента 
unregister (отменить регистрацию) отменяет регистрацию клиента 
email (электронная почта) изменяет адрес электронной почты 
show (показать) показывает клиентов 
quit выход из программы 

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

Н> show // показать 
id (-1 for all): -1 // идентификатор (-1 для всех):-1 
1 Rocket Squirrel rocky@frosbitefalls.com 
2 Bullwinkle Moose moose@wossamotta.edu 
H> register // регистрировать 
first name: Bob // имя: Боб 
last name: Oberg // фамилия: Оберг 
email address: oberg@objectinnovations.com // адрес электронной 
// почты: 
id = 3 // идентификатор = 3 
H> email // электронная почта 
customer id: 1 // идентификатор клиента 
email address: rocky@objectinnovations.com // адрес электронной 
// почты 
Н> unregister 
id: 2 // идентификатор: 2 
Н> show // показать 
id (-1 for all): -1 // идентификатор (-1 для всех) 
1 Rocket Squirrel rocky@objectinnovations.com 
3 Bob Oberg oberg@objectinnovations.com 

Класс Customer (Клиент)

Все файлы с исходными кодами программы-примера находятся в папке Customer-Collection. В файле customer. h находится реализация классов Customer (Клиент) и Customers (Клиенты). Исходный код для класса Customer (Клиент) почти такой же, как приведенный ранее. Единственное добавление — специальный конструктор, инициализирующий объект Customer (Клиент) заданным идентификатором. Этот конструктор используется классом Customers (Клиенты) при удалении элемента (UnregisterCustomer) и при проверке того, присутствует ли в коллекции некоторый элемент (Checkld).

_gc class Customer 
// сборщик мусора - класс Клиент 
{ 
pmblic: 
Customer(int id) // Клиент (int-идентификатор) 
{ 
nCustomerld = id; // идентификатор 
pFirstName = ""; 
pLastName = ""; 
pEmailAddress = ""; 
} 
};
//Класс Customers (Клиенты) содержит список клиентов, хранимый в ArrayList 
{Список массивов). 
_gc class Customers // сборщик мусора - класс Клиенты 
{ 
private: // частный 
ArrayList *pCustomers; 
public: 
Customers() // Клиенты 
{ 
pCustomers = new ArrayList; 
RegisterCustomer( 
"Rocket", // Ракета "Squirrel", 
// Белка "rocky@frosbitefalls.com"); 
RegisterCustomer( "Bullwinkle", 
"Moose", // Американский лось "moose@wossamotta.edu"); 
} 
int RegisterCustomer( String *pFirstName, String *pLastName, String *pEmailAddress) 
{ 
Customer *pCust = new Customer( // Клиент 
*pCust = новый Клиент 
{ 
pFirstName, pLastName, pEmailAddress); pCustomers->Add(pCust); // Добавить return pCust->nCustomer!d; } 
void UnregisterCustomer(int id) // идентификатор { 
Customer *pCust = new Customer(id); 
// Клиент *pCust = новый Клиент (идентификатор); 
pCustomers->Remove(pCust); 
} 
void ChangeEmailAddress(int id, String *pEmailAddress) 
// (int идентификатор, Строка *pEmailAddress) 
{ 
lEnumerator *pEnum = 
pCustomers-XSetEntimerator () ; 
while (pEnum->MoveNext()) 
{ 
Customer *pCust = // Клиент 
dynamic_cast(pEnum->Current); // Клиент 
if (pCust->nCustomer!d == id) 
// если (pCust-> nCustomerld == идентификатор) 
{ 
pCust->pEmailAddress = pEmailAddress; 
return; 
} 
} 
String *pStr = String::Format( // Формат 
"id {0} {!}", _box(id), S"not found"); 
// "идентификатор {0} {!}", (идентификатор), 
// " не найден"); 
throw new Exception(pStr); // новое Исключение 
} 
void ShowCustomers(int id) // идентификатор 
{ 
if ('Checkld(id) && id '= -1) 
// если (! Checkld (идентификатор) && идентификатор! =-1) 
return; lEnumerator *pEnum = 
pCustomers-XSetEnumerator(); 
while (pEnum->MoveNext()) 
{ 
Customer *pCust = // Клиент 
dynamic_cast(pEnum->Current); // Клиент 
if (id == -1 || id == pCust->nCustomer!d) 
// если (идентификатор == -1 || 
// идентификатор == pCust-> nCustomerld) 
{ 
String *pSid = 
pCust->nCustomerId.ToStnng() ->PadLeft (4) ; 
String *pFirst = 
pCust->pFirstName->PadRight(12) ; 
String *pLast = 
pCust->pLastName->PadRight(12); 
String *pEmail = 
pCust->pEmailAddress->PadRight(20) ; 
Console::Write("{0} ", pSid); // Пульт:: Записать 
Console::Write("{0} ", pFirst); // Пульт:: Записать 
Console::Write("{0} ", pLast) ; // Пульт:: Записать 
Console::WriteLine("{0}", pEmail); // Пульт:: Записать 
} 
} 
} 
bool Checkld(int id) 
// логический{булев} Checkld (int идентификатор) 
{ 
Customer *pCust = new Customer(id); 
// Клиент *pCust = новый Клиент (идентификатор); 
return pCustomers->Contains(pCust); // Содержит ? 
} 
}; 

Жирным шрифтом в листинге выделены строки, в которых используются особенности класса коллекции. Для перемещения по коллекции используется интерфейс lEnumerator. Он может быть использован в данном случае, так как ArrayList (Список массивов) поддерживает интерфейс lEnumerable. Этот интерфейс обеспечивает поддержку особой семантики С# — семантики foreach. Ключевое слово foreach не поддерживается в C++. Однако из приведенного примера видно, что в управляемом C++ для перебора элементов ArrayList (Список массивов) можно применять интерфейс lEnumerable. Интерфейсы, предназначенные для работы с коллекциями, включая lenumerable, будут рассмотрены в этой главе позже.

Методы Add (Добавить) и Remove (Удалить), как и следует предположить по их названиям, используются для добавления и удаления элементов коллекции, соответственно. Метод Remove (Удалить) просматривает коллекцию в поисках элемента, равного элементу, переданному методу в качестве аргумента, и удаляет найденный элемент. Равенство элементов устанавливается вызовом метода Equals (Равняется). Методу Remove (Удалить) в качестве аргумента передается элемент, создаваемый реализованным нами специальным конструктором, причем для создания элемента используется идентификатор. Поскольку мы подменили метод Equals (Равняется) так, что равенство элементов устанавливается только по атрибуту CustomerlD, этот конструктор имеет единственный аргумент — идентификатор клиента.

Метод Contains (Содержит), применяемый нами во вспомогательном методе Checkld, использует подмененный метод Equals (Равняется) аналогичным образом.

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

Концепция интерфейсов — одна из основных в современном программировании. Большие системы неизбежно разделяются на части, и существенным становится вопрос о взаимодействии этих частей друг с другом. Правила такого взаимодействия должны быть строго определены и постоянны, так как их изменение может повлиять на несколько частей системы. Однако сама реализация взаимодействия может быть изменена и это не потребует изменения кода других частей системы. В Visual C++ .NET ключевое слово _interface (интерфейс) имеет четко определенное значение. Управляемый (managed) интерфейс — ссылочный тип данных, подобный абстрактному классу, задающий поведение с помощью набора методов с определенными сигнатурами. Интерфейс — это просто контракт. Когда класс реализует интерфейс, он, таким образом, должен придерживаться контракта.

Использование интерфейсов — удобный способ разделения функциональных возможностей. Сначала определяются интерфейсы, а затем разрабатываются классы, реализующие эти интерфейсы. Методы класса могут быть сгруппированы в разных интерфейсах. Хотя в управляемом C++ класс является непосредственным потомком только одного базового класса, он может реализовывать несколько интерфейсов.

Использование интерфейсов помогает в создании динамических, гибких и легко изменяемых программ. CLR и BCL (Base Class Library— библиотека базовых классов) обеспечивают удобную возможность во время выполнения программы послать классу запрос для определения, реализует ли он некоторый интерфейс. Интерфейсы в .NET концептуально очень похожи на интерфейсы в модели компонентных объектов Microsoft (СОМ), но работать с ними намного легче.

Далее мы подробно изучим преимущества и использование интерфейсов. Затем мы рассмотрим несколько важных родовых интерфейсов библиотеки .NET, что поможет нам понять, каким образом управляемый C++ и .NET могут использовать друг друга для того, чтобы способствовать разработчикам в создании мощных и полезных программ.

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

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

Интерфейсы в управляемом C++

В Visual C++ .NET для определения интерфейсов используется ключевое слово _interface (интерфейс). Само же определение подобно определению класса. Так же, как и классы, интерфейсы являются ссылочными типами, а для определения их как управляемых используется ключевое слово _дс (сборщик мусора). Наибольшее отличие интерфейсов от классов (как управляемых, так и неуправляемых) — отсутствие конкретной реализации для интерфейсов; они представляют собой чистые спецификации. Тем не менее, подобно классам, интерфейсы могут иметь свойства (property), индексаторы (indexer) и, конечно же, методы.

На примере интерфейса ICustomer продемонстрируем определение методов, используемых клиентами системы Бюро путешествий Acme (Acme Travel Agency).

_gc _interface Icustomer 
// сборщик мусора - ICustomer 
{ 
public: 
int RegisterCustomer; 
String *pFirstName, 
String *pLastName, 
String *pEmailAddress) ; 
void UnregisterCustomer(int id); // идентификатор 
ArrayList *GetCustomer(int id); // идентификатор 
void ChangeEmailAddress(int id, String *pEmailAddress) ; 
// (int идентификатор, Строка *pEmailAddress); 
}; 

Описания методов RegisterCustomer, UnregisterCustomer и ChangeEmailAd-dress полностью совпадают с сигнатурами одноименных методов, реализованных нами в классе Customer (Клиент). Метод GetCustomer является новым. Ранее мы использовали метод ShowCustomer, выводящий на экран список клиентов. Этот метод использовался временно. Однако лучше возвращать по запросу сами данные и предоставить пользователю самому решать, что с ними делать. Метод GetCustomer возвращает информацию об одном или нескольких клиентах в списке массивов. Когда в качестве идентификатора клиента задается значение -1, возвращается информация обо всех зарегистрированных клиентах. В противном случае возвращаемый список содержит информацию о клиенте с заданным идентификатором. Если среди клиентов нет клиента с таким идентификатором, возвращаемый список будет пустым.

Наследование для интерфейсов

Интерфейс может быть потомком другого интерфейса (однако управляемый интерфейс не может быть потомком неуправляемого, и наоборот). В отличие от классов, для которых допустимо лишь единичное наследование, допускается множественное наследование интерфейсов, т.е. интерфейс может иметь несколько непосредственных родителей. Например, интерфейс ICustomer может быть определен как производный от двух более мелких интерфейсов IBasicCustomer и ICustomerlnfo. Заметим, что в описании указанных трех интерфейсов не задается спецификатор открытого доступа. Это потому, что интерфейсы общедоступны по умолчанию.

_gc _interface IBasicCustomer 
// сборщик мусора - IBasicCustomer 
{ 
int RegisterCustomer( 
String *pFirstName, 
String *pLastName, 
String *pEmailAddress) ; 
void UnregisterCustomer(int id); // идентификатор 
void ChangeEmailAddress(int id, String *pEmailAddress) ; 
// (int идентификатор, Строка *pEmailAddress) ; 
}; 
_gc _interface ICustomerlnfo 
// сборщик мусора - ICustomerlnfo 
{ 
ArrayList *GetCustomer(int id); // идентификатор 
}; 
_gc _interface ICustomer : IBasicCustomer, ICustomerlnfo 
// сборщик мусора - ICustomer: IBasicCustomer, ICustomerlnfo 
{ 
}; 

При таком объявлении интерфейса можно также определить новые методы, как это сделано ниже для интерфейса ICustomer2.

_gc _interface ICustomer2 : IBasicCustomer, ICustomerlnfo 
// сборщик мусора - ICustomer2: IBasicCustomer, ICustomerlnfo 
{ 
void NewMethod(); 
}; 

Использование интерфейсов облегчает программирование на управляемом C++. Интерфейсы реализуются через классы, и для получения указателя на интерфейс можно выполнить приведение указателя на класс. Методы интерфейсов можно вызывать, используя и указатели на класс, и указатели на интерфейс; однако для того, чтобы полностью воспользоваться достоинствами полиморфизма, предпочтительно везде, где только возможно, использовать указатели на интерфейсы.

Реализация интерфейсов

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

_gc class HotelBroker : public Broker, public IHotellnfo, 
// класс сборщика мусора - HotelBroker: общедоступный Брокер, 
public IHotelAdmin, public IHotelReservation 
{ 
... 
}; 

В этом примере класс HotelBroker является производным от класса Broker (Брокер) и реализует интерфейсы IHotellnfo, IHotelAdmin и IHotelReservation. В HotelBroker должны быть реализованы все методы этих интерфейсов, либо непосредственно, либо используя реализацию, унаследованную от базового класса Broker (Брокер).

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

А сейчас в качестве небольшого примера вышеизложенного, рассмотрим программу Smalllnterface. Класс Account (Счет) реализует интерфейс IBasicAccount. В описании этого интерфейса демонстрируется синтаксис объявления свойства интерфейса.

//Account.h 
_gc _interface IBasicAccount 
// сборщик мусора - IBasicAccount 
{ 
void Deposit(Decimal amount); // Депозит (Десятичное 
// количество); 
void Withdraw(Decimal amount); // Снять (Десятичное 
// количество); 
_property Decimal get_Balance(); // Десятичное число }; 
_gc class Account : public IBasicAccount 
// сборщик мусора - класс Счет: IBasicAccount 
{ 
private: // частный 
Decimal balance; // Десятичный баланс public: 
Account(Decimal balance) // Счет (Десятичный баланс) 
{ 
this->balance = balance; // баланс 
} 
void Deposit(Decimal amount) // Депозит (Десятичное количество) 
{ 
balance = balance + amount; // баланс = баланс + количество 
} 
void Withdraw(Decimal amount) // Снять (Десятичное количество) 
{ 
balance = balance - amount; // баланс = баланс - количество 
} 
_property Decimal get_Balance() // Десятичное число 
{ 
return balance; // баланс 
} 
}; 

Использование интерфейсов

Когда известно, что некоторый класс поддерживает определенный интерфейс, его методы можно вызывать с помощью указателя на экземпляр класса. Если же неизвестно, реализован ли интерфейс классом, можно попытаться выполнить приведение указателя на класс к указателю на интерфейс. И если класс не поддерживает данный интерфейс, при такой попытке возникнет исключение. В следующем примере, взятом из файла Smalllnterf асе. h, демонстрируется именно такой способ проверки.

try // попытка 
{ 
IBasicAccount *pifc2 = 
dynamic_cast(pacc2); 
pifc2->Deposit(25); // Депозит 
Console::WriteLine( 
"balance = {0}", _box(pifc2->Balance)); // Баланс 
} 
catch (NullReferenceException *pe) 
{ 
Console::WriteLine( 
"IBasicAccount is not supported"); // IBasicAccount 
// не поддерживается 
Console::WriteLine(pe->Message); // Сообщение 
} 
} 

В программе Small Inter face используются два почти одинаковых класса. Класс Account (Счет) поддерживает интерфейс IBasicAccount, а второй класс, NoAccount его не поддерживает. Оба класса имеют идентичные наборы методов и свойств. Приведем полностью содержимое файлов Smalllnterf асе. срр и Smalllnterf асе. h. Заметим, что в этой программе делаются попытки привести указатели на экземпляры классов Account (Счет) и NoAccount к указателю на интерфейс IBasicAccount.

//Smalllnterfасе.срр 
fusing  
using namespace System; 
// использование пространства имен Система; 
#include "Account.h" 
#include "NoAccount.h" 
#include "Smalllnterface.h" 
void main() // главный 
{ 
Smalllnterface::Main(); // Главный 
} 
//Smalllnterface.h 
_gc class Smalllnterface 
// класс сборщика мусора Smalllnterface 
{ 
public: 
static void Main() // Главный 
{ 
Account *pacc - new Account(100); // новый Счет 
// Использовать ссылку на класс 
Console::WriteLine( 
"balance = {0}", _box(pacc->Balance)); // Баланс 
pacc->Deposit(25); // Депозит 
Console::WriteLine( 
"balance = {0}", _box(pacc->Balance)); // Баланс 
// Использовать ссылку на интерфейс 
IBasicAccount *pifc = 
dynamic_cast(pacc); pifc->Deposit(25); // Депозит 
Console::WriteLine( 
"balance = {0}", _box(pifc->Balance)); // Баланс 
// Теперь попробовать с классом, 
// не реализующим 
IBasicAccount NoAccount *pacc2 = new NoAccount(500); 
// Использовать ссылку на класс 
Console::WriteLine( 
"balance = {0}", _box(pacc2->Balance)); // Баланс 
pacc2->Deposit(25); // Депозит 
Console::WriteLine( 
"balance = {0}", _box(pacc2->Balance)); // Баланс 
// Испробовать указатель на интерфейс try 
// попытка 
{ 
IBasicAccount *piba= 
dynamic_cast(pacc2); 
piba->Deposit(25); // Депозит 
Console::WriteLine( 
"balance = {0}", _box(piba->Balance)); // Баланс 
} 
catch (NullReferenceException *pe) 
{ 
Console::WriteLine( 
"IBasicAccount is not supported"); // IBasicAccount 
//не поддерживается 
Console::WriteLine(pe->Message); // Сообщение 
} 
} 
}; 

В приведенном примере сначала мы имеем дело с классом Account (Счет), который поддерживает интерфейс IBasicAccount. В этом случае попытки вызвать методы интерфейса как с помощью указателя на класс, так и указателя на интерфейс, полученного в результате приведения, заканчиваются успешно. Далее мы имеем дело с классом NoAccount. Несмотря на то, что набор методов этого класса идентичен набору методов класса Account (Счет), в его описании не указано, что он реализует интерфейс IBasicAccount.

//NoAccount.h 
_gc class NoAccount 
// класс сборщика мусора NoAccount 
{ 

При запуске этой программы возникает исключение NullReferenceException. Это происходит при попытке использовать указатель на интерфейс IBasicAccount, полученный в результате динамического приведения указателя на класс NoAccount. (Иными словами, исключение возникает при попытке приведения типа NoAccount * к данным типа указателя на интерфейс IBasicAccount *.) Если бы мы использовали обычное приведение типа в стиле С, то при подобной попытке возникло бы исключение InvalidCastException. Однако уже при компиляции такой программы было бы выдано предупреждение, что использование приведения типов в стиле С не рекомендуется.

balance = 100 
balance = 125 
balance = 150 
balance = 500 
balance = 525 
IBasicAccount is not supported 
Value null was found where an instance of an object was 
required. 

Вот перевод выдачи:

баланс = 100 
баланс =125 
баланс = 150 
баланс = 500 
баланс = 525 
IBasicAccount не поддерживается 
Пустой указатель там, где требуется указатель на объект.

Полезной особенностью интерфейсов является возможность их использования в динамических сценариях, что позволяет по ходу выполнения программы проверять, поддерживается ли интерфейс определенным классом. Если интерфейс поддерживается, мы можем воспользоваться предоставляемыми им возможностями; в противном же случае программа может проигнорировать интерфейс. Фактически такое динамическое поведение реализуется с помощью обработки возникающих исключений, как это уже было продемонстрировано выше. Хотя подобный подход вполне работоспособен, однако он отличается некоторой неуклюжестью и приводит к появлению трудночитаемых программ. C++ поддерживает использование операторов dynamic_cast и typeid, а в .NET Framework есть класс Туре (Тип), облегчающий динамическое использование интерфейсов.

В качестве примера рассмотрим интерфейс ICustomer2, имеющий, по сравнению с интерфейсом ICustomer 1, дополнительный метод ShowCustomer.

_gc _interface ICustomer2 : ICustomerl 
// сборщик мусора - ICustomer2: ICustomerl 
{ 
public: 
void ShowCustomers(int id); // идентификатор 
}; 

Предположим, что класс Customerl поддерживает интерфейс ICustomerl, a класс Customer2 — интерфейс !Customer2. Для консольной программы-клиента удобнее использовать исходный метод ShowCustomer, а не метод Get-Customer, так как последний создает список массивов и копирует данные в него. Поэтому программа-клиент предпочтет работать с интерфейсом ICus-tomer2, если это возможно. В папке TestlnterfaceBeforeCast содержится программа, рассмотренная в следующем разделе.

Проверка поддержки интерфейса перед приведением типов

Проверку поддержки интерфейса можно производить, выполняя динамическое приведение типа указателя и обрабатывая исключение, которое может при этом возникнуть. Однако более изящным решением будет выполнять проверку до приведения типа, избегая при этом возникновения исключений. Если объект поддерживает необходимый интерфейс, можно выполнять приведение типа для получения доступа к интерфейсу. С# поддерживает использование удобного оператора is для проверки того, поддерживает ли объект определенный интерфейс. К сожалению, в C++ с этой целью приходится использовать метод отражения, реализуемый посредством методов GetType и Getlnterf асе. В связи с тем, что это приводит к появлению несколько громоздкого выражения, в следующем примере с помощью директивы #define определяется макрос IS (THIS, THAT_INTERFACE), используемый далее в двух условных операторах if.

//TestlnterfaceBeforeCast.срр 
//MACRO: pObj->GetType()->GetInterface("Somelnterface")!=0 
// МАКРОС 
#define IS(THIS, THAT_INTERFACE) (THIS->GetType()->GetInterface( 
THAT_INTERFACE)!=0) 
#using  
using namespace System; 
// использование пространства имен Система; 
_gc _interface ICustomer1 {}; 
// сборщик мусора - ICustomer1; 
_gc _interface ICustomer2 : ICustomer1 
// сборщик мусора - ICustomer2: ICustomer1 
{ 
public: 
void ShowCustomers(int id); // идентификатор 
}; 
_gc class Customer! : public ICustomer1 {}; 
// класс сборщика мусора Customerl: ICustomerl {}; 
_gc class Customer2 : public ICustomer2 
// класс сборщика мусора Customer2: ICustomer2 
{ 
public: 
void ShowCustomers(int id) // идентификатор 
{ 
Console::WriteLine("Customer2::ShowCustomers: 
succeeded"); 
} 
}; 
void main(void) // главный 
{ 
Customerl *pCustl = new Customerl; // не к ICustomer2 
Console::WriteLine(pCustl->GetType()); 
// проверить, относится ли к типу ICustomer2 перед приведением 
if (IS(pCustl, "ICustomer2")) 
{ 
ICustomer2 *plcust2 = 
dynamic_cast(pCustl); 
plcust2->ShowCustomers(-1); 
} 
else 
Console::WriteLine 
("pCustl does not support ICustomer2 interface"); 
// ("pCustl не поддерживает интерфейс ICustomer2"); 
Customer2 *pCust2 = new Customer2; // да, на ICustomer2 
Console::WriteLine(pCust2->GetType()); 
// проверить, относится ли к типу ICustomer2 перед приведением 
if (IS(pCust2, "ICustomer2")) 
{ 
ICustomer2 *plcust2 = 
dynamic_cast(pCust2); 
pIcust2->ShowCustomers(-1); 
} 
else 
Console::WriteLine 
("pCust2 does not support ICustomer2 interface"); 
// ("pCust2 не поддерживает интерфейс ICustomer2"); 

} 	

В этом примере продемонстрировано далеко не оптимальное решение, так как проверка типа производится дважды. Первый раз — при использовании отражения для проверки поддержки интерфейса в макросе IS. А еще раз проверка производится автоматически — при выполнении динамического приведения типа, в этом случае, если интерфейс не поддерживается, возникает исключение. Результат работы программы приведен ниже. Обратите внимание, что при выполнении программы действительно не возникает исключения.

Customer1 pCustl does not support ICustomer2 interface Customer2 Customer2::ShowCustomers: succeeded

А вот и перевод:

Customer1 pCustl не поддерживает интерфейс ICustomer2 Customer2 Customer2:: ShowCustomers: успешно

Оператор dynamic_cast

Результатом выполнения оператора dynamic_cast является непосредственно указатель на интерфейс. В случае, если интерфейс не поддерживается, значение указателя устанавливается равным нулю. Используем этот факт для создания программы, в которой проверка поддержки интерфейса производится один раз. Приведенный ниже фрагмент взят из примера CastThenTestForNull, отличающегося от предыдущего, Testlnter-f aceBef oreCast, тем, что в нем производится проверка равенства нулю результата динамического приведения типа.

void main(void) // главный 
{ 
Customerl *pCustl = new Customer!; // нет ICustomer2 
Console::WriteLine(pCustl->GetType() ) ; 
// Использовать оператор С ++ dynamic_cast, чтобы проверить 
// наличие ICustomer2 
ICustomer2 *plcust2 = 
dynamic_cast(pCustl); 
if (plcust2 != 0) 
p!cust2->ShowCustomers(-1) ; 
else 
Console::WriteLine 
("pCustl does not support ICustomer2 interface"); 
// ("pCustl не поддерживает интерфейс ICustomer2"); 
Customer2 *pCust2 = new Customer2; // да, есть ICustomer2 
Console::WriteLine(pCust2->GetType()) ; 
// Использовать оператор С ++ dynamic_cast, чтобы проверить 
// наличие ICustomer2 
plcust2 = 
dynamic_cast(pCust2); 
if (plcust2 != 0) 
p!cust2->ShowCustomers(-1) ; 
else 
Console::WriteLine 
("pCust2 does not support ICustomer2 interface"); 
// ("pCust2 не поддерживает интерфейс ICustomer2"); 
} 

Результат выполнения программы CastThenTestForNull показывает, что действительно, исключения не возникает, но проверка поддержки интерфейса при этом производится всего один раз для каждого из объектов.

Customer1 
pCustl does not support ICustomer2 interface 
Customer2 
ICustomer2::ShowCustomers: succeeded 

Вот перевод этой выдачи:

Customer1 
pCustl не поддерживает интерфейс ICustomer2 
Customer2 
ICustomer2:: ShowCustomers: успешно 

Если вы знакомы с моделью компонентных объектов Microsoft (COM), проверка поддержки классом интерфейса вам должна быть хорошо знакома.

Попытаемся применить полученные знания об интерфейсах для небольшой переделки программы Бюро путешествий Acme (Acme Travel Agency). Одним из наибольших достоинств интерфейсов является то, что благодаря им повышается уровень абстракции, — это позволяет понять и ощутить систему на уровне взаимодействия ее отдельных частей, абстрагируясь от конкретной их реализации.

Файлы с исходным кодом находятся в папке CaseStudy.

Интерфейсы в управляемом C++ и .NET

.NET и модель компонентных объектов Microsoft (COM) имеют много сходного. В обеих этих технологиях фундаментальную роль играет концепция интерфейсов. Их удобно использовать для определения контрактов. Интерфейсы обеспечивают очень динамичный стиль программирования.

В модели компонентных объектов Microsoft (COM) разработчику приходится самому заботиться о тщательном создании инфраструктуры, необходимой для реализации СОМ-компонентов. Для создания СОМ-объектов требуется реализовать фабрику класса (class factory). Для динамической проверки поддержки интерфейса разработчик (Должен реализовать метод Querylnterfасе интерфейса Unknown. Кроме того, для соответствующего управления памятью следует реализовать методы AddRef и Release (Освободить).

При использовании же языков .NET все эти действия осуществляются автоматически виртуальной машиной, реализующей общий язык времени выполнения CLR (Common Language Runtime). Для создания объекта достаточно воспользоваться " оператором new (создать). Проверку поддержки классом интерфейса и получение указателя на интерфейс несложно провести с помощью оператора dynamic_cast. Управление памятью берет на себя сборщик мусора.

Контракт

Ранее мы уже рассмотрели интерфейс ICustomer класса Customers (Клиенты). Теперь обратим внимание на класс HotelBroker. Его методы естественным образом разделяются на три группы.

  • 1. Информация о гостинице, такая, как названия городов, в которых есть гостиницы, и названия гостиниц, которые есть в определенном городе
  • 2. Управление информацией о гостиницах, в частности добавление или удаление гостиницы из базы данных либо изменение количества комнат, доступных в некоторой гостинице.
  • 3. Операции резервирования гостиниц, например, бронирование номеров, отмена заказа или просмотр списка резервирования.

В свете изложенного логично будет создать для класса HotelBroker три интерфейса. Эти интерфейсы определены в файле AcmeDef initions.h.

__gc _interface IHotellnfo 
// сборщик мусора - IHotellnfo 
{ 
ArrayList *GetCities(); 
ArrayList *GetHotels(); 
ArrayList *GetHotels(String *pCity); 
}; 
_gc _interface IHotelAdmin 
// сборщик мусора - IHotelAdmin 
{ 
String *AddHotel ( 
String *pCity, 
String *pName, 
int numberRooms, 
Decimal rate); // Десятичная цена 
String *DeleteHotel (String *pCity, String *pName); 
String *ChangeRooms( 
String *pCity, 
String *pName, 
int numberRooms, 
Decimal rate); // Десятичная цена 
}; 
_gc _interface IHotelReservation 
// сборщик мусора - IHotelReservation 
{ 
ReservationResult MakeReservation( 
int customerld, 
String *pCity, 
String *pHotel, 
DateTime checkinDate, 
int numberDays); 
void CancelReservation(int id); // идентификатор 
ArrayList *FindReservationsForCustomer( 
int nCustomerld); 
}; 

Реализация

Далее реализуем систему управления гостиницами, используя коллекции вместо массивов. При этом мы будем возвращать программе-клиенту запрошенную ею информацию в методе TestHotel: -.Main вместо того, чтобы делать это непосредственно в классе HotelBroker. Ранее в этой же главе мы рассмотрели новую реализацию класса Customers (Клиенты). Принципы, применявшиеся при тех переделках, будут использованы и для обновления класса HotelBroker.

Структуры

Прежде всего следует разобраться со структурой данных, передаваемых клиенту по его запросу. Мы используем класс ArrayList (Список массивов). А что же будет храниться в указанном списке массивов? В нашем случае это могут быть объекты Customer (Клиент) и Hotel (Гостиница). Проблема применимости такого подхода состоит в том, что кроме данных, которые клиенту могут понадобиться, оба этих класса содержат также данные, необходимые для реализации класса, но не нужные программе-клиенту вовсе. Для того чтобы решить эту проблему, определим несколько структур.

В файле Customers .h определим структуру CustomerListltem, предназначенную для возврата информации о клиенте.

_value struct CustomerListltem 
{ 
public: 
int nCustomerld; 
String *pFirstName; 
String *pLastName; 
String *pEmailAddress; 
}; 

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

_value struct HotelListltem 
{ 
public: 
String *pCity; 
String *pHotelName; 
int nNumberRooms; 
Decimal decRate; // Десятичное число 
}; 
_value struct ReservationListltem 
{ 
public: 
int nCustomerld; 
int nReservationld; 
String *pHotelName; 
String *pCity; 
DateTime dtArrivalDate; 
DateTime dtDepartureDate; 
int nNumberDays; 
}; 
_value struct ReservationResult 
{ 
public: 
int nReservationld; 
Decimal decReservationCost; // Десятичное число 
Decimal decRate; // Десятичное число 
String *pComment; 
}; 

ReservationResult возвращает значение Reservationld или -1 при возникновении проблем (в этом случае в поле pComment содержится более подробное описание возникших проблем; если же никаких проблем нет, там находится строка "ОК.").

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

При использовании интерфейсов может возникать неопределенность в случае, если в двух реализованных классом интерфейсах есть методы с одинаковыми именами и сигнатурами. Просмотрим, например, следующие версии интерфейсов lAccount и IState-ment. Обратите внимание, что в каждом из них есть метод Show (Показать).

_gc _interface lAccount 
// сборщик мусора - IAccount 
{ 
void Deposit(Decimal amount); // Депозит (Десятичное 
// количество) 
void Withdraw(Decimal amount); // Снять (Десятичное количество) 
_property Decimal get_Balance(); // Десятичное число 
void Show(); // Показать 
}; 
_gc _interface IStatement // сборщик мусора - IStatement 
{ 
_property int get_Transactions(); 
void Show(); // Показать 
}; 

Как в подобном случае указать классу нужную реализацию метода? Такая задача решается благодаря использованию имени интерфейса вместе с именем реализуемого метода, как это продемонстрировано на примере программы Ambiguous (Неоднозначная программа). Версия метода Show (Показать), относящаяся к интерфейсу lAccount, выводит на экран информацию только о состоянии счета, а метод IStatement: : Show (Показать) выводит число сделок и баланс.

//Account.h 
_gc class Account : public lAccount, public IStatement 
// класс сборщика мусора Счет 
{ 
private: // частный 
Decimal decBalance; // Десятичное число 
int nNumXact; public: 
Account(Decimal decBalance) : nNumXact(O) 
// Счет (Десятичное число decBalance) 
{ 
this->decBalance = decBalance; 
} 
void Deposit(Decimal decAmount) 
// Депозит (Десятичное число decAmount) 
{ 
decBalance = decBalance + decAmount; 
++nNumXact; } void Withdraw(Decimal decAmount) // Снять (Десятичное 
// число decAmount) 
{ 
decBalance = decBalance - decAmount; 
++nNumXact; 
} 
_property Decimal get_Balance() // Десятичное число 
{ 
return decBalance; 
} 
void lAccount::Show() // Показать 
{ 
Console::WriteLine( 
"balance = {0}", _box(decBalance)); // баланс 
} 
_property int get_Transactions() 
{ 
return nNumXact; 
} 
void IStatement::Show() // Показать 
{ 
Console::WriteLine( 
"{0} transactions, balance = {!}", // сделки, баланс 
_box(nNumXact), 
_box(decBalance)) ; 
} 
}; 

Доступ к методам lAccount::Show (Показать) и IStatement:: Show (Показать) нельзя получить с использованием указателя на экземпляр класса. Доступ к этим методам возможен только с помощью указателя на интерфейс того типа, который явно указан в объявлениях методов. Приведенная программа демонстрирует, что метод lAccountShow можно вызвать только при использовании указателя на интерфейс lAccount, но не с помощью указателя на экземпляр класса Account (Счет). Попытка вызвать такой метод с помощью указателя на экземпляр класса является ошибкой и будет пресечена компилятором. Получая указатель на интерфейс IStatement, можно вызвать метод IStatement :: Show (Показать). Результат выполнения программы будет следующим:

О transactions, balance = 100 
О transactions, balance = 100 
balance = 115 
2 transactions, balance = 115 

Вот перевод:

О сделок, баланс = 100 
О сделок, баланс = 100 
баланс = 115 
2 сделки, баланс = 115 

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

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

  • работа с коллекциями;
  • копирование объектов;
  • сравнение объектов.

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

Теперь, достаточно подробно обсудив концепцию интерфейсов, мы можем обратить более пристальный взгляд на коллекции, в частности, на класс ArrayList (Список массивов), который мы активно использовали в программе Бюро путешествий Acme (Acme Travel Agency). Присмотревшись к определению класса ArrayList (Список массивов), можно увидеть, что он реализует четыре стандартных интерфейса.

// класс сборщика мусора ArrayList 
_gс class ArrayList : public IList, ICollection, 
lEnumerable, ICloneable 

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

Рис. 5.1. Иерархия интерфейсов для списков

Четвертый интерфейс из реализованных в ArrayList (Список массивов), ICloneable, является независимым от первых трех и предназначен для осуществления детального копирования объектов. Для знакомства с интерфейсами, обеспечивающими работу с коллекциями, рассмотрим программу StringList. Просмотрите главный метод Main программы StringList, а вспомогательные методы будут подробно рассматриваться по мере нашего знакомства с разными интерфейсами, предназначенными для работы с коллекциями.

//StringList.h 
_gc class StringList 
// класс сборщика мусора StringList 
{ 
private: // частный 
static ArrayList *pList; // статический 
public: 
static void Main() // статический Главный 
{ 
// Инициализировать строки и показать начальное состояние 
pList = new ArrayList(4); 
ShowCount(); 
AddStringC'Amy") ; // Эми 
AddStringC'Bob"); // Боб 
AddString("Charlie"}; // Чарли 
ShowEnum(pList);// счетчик 
ShowCount (); 
// Добавить еще две строки и снова показать состояние 
AddString("David"); //Дэвид 
AddString("Ellen"); // Эллен 
ShowList(pList);// моделировать 
foreach ShowCount (}; 
// Удалить две строки из списка и показать состояние 
RemoveString("David"); // Дэвид RemoveAt(0); 
ShowArray(pList);// запись индекса ShowCount(); 
// Попытка удалить две строки, которых нет в списке 
RemoveString("Amy"); // Эми 
RemoveAt(3); 
} 
private: // частный 
static void ShowEnum(ArrayList *pArray) // статическая функция 
{ 
lEnumerator *plter = pArray->GetEnumerator(); 
bool more = p!ter->MoveNext(); 
while (more) 
{ 
String *pStr = 
dynamic_cast((p!ter->Current)); 
Console::WriteLine(pStr); 
more = p!ter->MoveNext(); 
} 
} 
static void ShowList(ArrayList *pArray) // статическая функция 
{ 
lEnumerator *pEnum = 
pArray->GetEnumerator() ; 
while (pEnum->MoveNext() ) 
{ 
String *pStr = 
dynamic_cast(pEnum->Current); 
Console::WriteLine(pStr}; 
} 
} 
static void ShowArray(ArrayList *pArray) // статическая функция 
{ 
for (int i = 0; i < pArray->Count; i++) f 
Console::WriteLine( 
"pArray->get_Item({0}) = {!}", _box (i) , 
pArray->get_Item(i)); } } 
static void ShowCount() // статическая функция 
{ 
Console::WriteLine( 
"pList->Count = {0}", _box(pList->Count)); 
Console::WriteLine( 
"pList->Capacity = {0}", _box(pList->Capacity)); 
// Вместимость 
} 
static void AddString (String *pStr) // статическая функция 
{ 
if (pList->Contains(pStr)) // если содержит throw new Exception! 
// новое Исключение 
String::Format("list contains {0}", pStr)); // Формат: список содержит 
pList->Add(pStr); // Добавить 
} 
static void RemoveString(String *pStr) // статическая функция 
{ 
if (pList->Contains(pStr)) // если содержит 
pList->Remove(pStr); // Удалить else 
Console::WriteLine( 
"List does not contain {0}", pStr); // Список 
//не содержит 
} 
static void RemoveAt(int nlndex) // статическая функция 
{ 
try // попытка 
{ 
pList->RemoveAt(nlndex) ; 
} 
catch (ArgumentOutOfRangeException *) 
{ 
Console::WriteLine( 
"No element at index {0}", _box(nlndex)); 
// Нет элемента с таким индексом 
} 
} 
}; 

Результат работы программы будет таким:

pList->Count = О 
pList->Capacity = 4 // Вместимость 
Amy // Эми 
Bob // Боб 
Charlie // Чарли 
pList->Count = 3 
pList->Capacity =4 // Вместимость 
Amy // Эми 
Bob // Боб 
Charlie // Чарли 
David // Дэвид 
Ellen // Эллен 
pList->Count = 5 
pList->Capacity =8 // Вместимость 
pArray->get_Item(0) = Bob // Боб 
pArray->get_Item(1) = Charlie // Чарли 
pArray->get_Item(2) = Ellen // Эллен 
pList->Count = 3 
pList->Capacity =8 // Вместимость 
List does not contain Amy // Список не содержит Эми 
No element at index 3 // Нет элемента с индексом 3 

lEnumerableИ JEnumerator

Исходным для всех интерфейсов, предназначенных для работы с коллекциями, является интерфейс lEnumerable, имеющий один метод — GetEnumerator.

_gc _interface lEnumerable 
// сборщик мусора - lEnumerable 
{ 
lEnumerator* GetEnumerator(); 
}; 

GetEnumerator возвращает указатель на интерфейс lEnumerator, который используется для последовательного доступа к элементам коллекции. Этот интерфейс имеет свойство Current (Текущая запись) и методы MoveNext и Reset (Сброс).

_gc _interface lEnumerator 
// сборщик мусора - lEnumerator 
{ 
_property Object* get__Current () ; 
bool MoveNext(); // логический (булев) 
void Reset(); 
}; 

Сразу после инициализации нумератор указывает на позицию перед первым элементом коллекции и, следовательно, для получения доступа даже к первому ее элементу его следует продвинуть. Использование нумератора для последовательного доступа к элементам списка проиллюстрировано в методе ShowEnum.

static void ShowEnum(ArrayList *pArray) // статическая функция 
{ 
lEnumerator *plter = 
pArray->GetEnumerator(); 
bool more = piter->MoveNext(); // логическое значение while (more) 
{ 
String *pStr = 
dynamic_cast((p!ter->Current)); 
Console::WriteLine (pStr) ; 
more = p!ter->MoveNext (); 
} 
} 

Интерфейс ICollection

Интерфейс ICollection является производным от lEnumerable и в нем добавляются свойство Count (Количество) и метод СоруТо.

_gc _interface ICollection : public lEnumerable 
// сборщик мусора - ICollection: lEnumerable 
{ 
_property int get_Count(); 
_property bool get_IsSynchronized(); // логический 
_property Object* get_SyncRoot(); 
void CopyTo(Array* array, int index); // массив, индекс 
}; 

Кроме того, в данном интерфейсе появляются свойства, предназначенные для обеспечения синхронизации при использовании потоков. "Является ли безопасным использование потоков?" — вот один из часто задаваемых вопросов о любой библиотеке. Что касается среды .NET Framework, то ответ на этот вопрос короток и ясен — нет. Это не значит, что разработчики .NET Framework не задумывались о безопасности использования потоков. Наоборот, они реализовали множество механизмов, которые могут помочь вам при работе с потоками. Причина же, по которой использование потоков при работе с коллекциями не является безопасным автоматически, — в том, что обеспечение безопасности приводит к понижению производительности, а если ее обеспечить автоматически, то и при работе в однопотоковом режиме производительность окажется заниженной. Если же вам необходимо обеспечить безопасность при работе с потоками, вы можете использовать свойства интерфейса, предназначенные для синхронизации. Более подробно механизмы синхронизации потоков в .NET Framework будут рассмотрены в главе 8 "Классы каркаса .NET Framework".

Программа StringList иллюстрирует использование свойства Capacity (Объем) класса ArrayList (Список массивов), а также свойства Count (Количество), унаследованного классом ArrayList (Список массивов) от интерфейса ICollection.

static void ShowCount() // статическая функция 
{ 
Console::WriteLine( 
"pList->Count = {0}", _box(pList->Count)); // Счет 
Console::WriteLine( 
"pList->Capacity = {0}", _box(pList->Capacity)); 
// Вместимость 
} 

Интерфейс IList

Интерфейс IList является производным от интерфейса iCollection и в нем введены методы для добавления элемента в список, удаления его из списка и т.д.

_gc _interface IList : public ICollection 
// сборщик мусора - IList: ICollection 
{ 
_property bool get_IsFixedSize(); // логический 
_property bool get_IsReadOnly(); // логический 
_property Object* get_Item(int index); // индекс 
_property void set_Item(int index, Object*); // индекс, 
// Объект * 
int Add(0bject* value); // Добавить значение 
void Clear(); 
bool Contains(Object* value); // Содержит ли значение 
int IndexOf(Object* value); // значение 
void Insert(int index, Object* value); // Вставка (int индекс, 
// Object* значение); 
void Remove(Object* value); // Удалить значение 
void RemoveAt(int index); // индекс }; 

В программе stringList продемонстрировано использование индексатора get_Item и методов Contains (Содержит), Add (Добавить), Remove (Удалить) и RemoveAt.

static void ShowArray(ArrayList *pArray) 
{ 
for (int i = 0; i < pArray->Count; i++) 
{ 
Console::WriteLine( 
"pArray->get_Item({0}) = {!}", _box (i) , 
pArray->get_Item(i)); 
} 
} 
static void AddString(String *pStr) 
{ 
if (pList->Contains(pStr)) 
// если содержит throw new Exception( 
// новое Исключение 
String::Format("list contains {0}", pStr)); 
// Формат:: ("список содержит") 
pList->Add(pStr); // Добавить 
} 
i static void RemoveString(String *pStr) 
{ 
if (pList->Contains(pStr)) // если содержит 
pList->Remove(pStr); // Удалить 
else 
Console::WriteLine( 
"List does not contain {0}", pStr); // Список 
// не содержит 
} 
static void RemoveAtfint nlndex) 
{ 
try // попытка 
{ 
pList->RemoveAt(nIndex); 
} 
catch (ArgumentOutOfRangeException *) 
{ 
Console::WriteLine( 
"No element at index {0}", _box(nlndex)); // Нет элемента 
//с индексом 
} 
} 

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

Напомним, что в .NET Framework есть значащие типы данных и ссылочные типы. Значащие типы данных представляют собой непосредственно данные, а ссылочные типы всего лишь указывают местонахождение данных. Если значение одной переменной-указателя скопировать в другую переменную того же типа, обе переменные будут указывать на один и тот же объект. Если с помощью первого указателя этот объект будет изменен, то изменится и объект, на который ссылается второй указатель. Иногда требуется именно такое поведение, а иногда другое.

Интерфейс ICloneable

Интерфейс ICloneable является исходным и имеет единственный метод— Clone (Клон). Данный метод может быть реализован для выполнения как поверхностного, так и детального копирования, но указатель на Object (Объект), возвращаемый методом, должен указывать на объект того же (или совместимого) типа, для которого реализован интерфейс ICloneable. Обычно метод Clone (Клон) реализуют так, чтобы он создавал новый объект. Однако бывает, например в случае класса String (Строка), что метод Clone (Клон) просто возвращает указатель на исходный объект.

_gc _interface ICloneable 
// сборщик мусора - ICloneable 
{ 
Object* Clone(); // Клон 
}; 

Поверхностная и детальная копии

Неуправляемые структуры и классы в C++ автоматически реализуют почленное копирование содержимого, которое будет актуальным до тех пор, пока не будет произведена подмена конструктора копирования. Почленное копирование называют также поверхностным копированием. В базовом классе Object (Объект) также есть защищенный (protected) метод, MemberwiseClone, выполняющий почленное копирование управляемых структур или классов.

Если среди членов структуры или класса есть указатели, такое почленное копирование может быть не тем, что вам требуется. Результатом этого копирования будет то, что указатели разных объектов будут ссылаться на одни и те же данные, а не на их независимые копии. Для фактического копирования данных следует сделать детальную копию. Детальное копирование может быть обеспечено на уровне языка или на уровне библиотек. В обычном C++ детальное копирование осуществляется на уровне языка с использованием конструктора копирования. В управляемом C++ оно осуществляется .NET Framework с помощью интерфейса Idoneable, который можно реализовывать в создаваемых классах специально для того, чтобы эти классы могли выполнять детальное копирование.

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

Присваивание значения одного указателя другому (будь то указатели на управляемые или неуправляемые классы или структуры) приводит к простому копированию указателей. Это не поверхностное, и не детальное копирование, а простое присваивание указателей.

Преобразование одного нессылочного (неуказательного) типа в другой, причем типы эти могут быть неуправляемыми классами, неуправляемыми структурами или значащими типами данных, является поверхностным копированием, автоматически осуществляемым C++.

Присваивание новому указателю значения, возвращаемого методом Clone (Клон) (конечно, это значение должно быть также указателем) и являющегося управляемым классом или структурой с реализованным интерфейсом IClone-able, представляет собой поверхностное либо детальное копирование, в зависимости от реализации метода Clone (Клон).

Пример программы проиллюстрируем изложенный материал программой CopyDemo. Эта программа делает копию экземпляра класса Course (Курс). В классе Course (Курс) содержится название курса и список (коллекция) студентов.

//Course .h 
_gc class Course : public Idoneable 
// класс сборщика мусора Курс: Idoneable 
{ 
public: 
String *pTitle; 
ArrayList *pRoster; 
Course(String *pTitle) // Курс 
{ 
this->pTitle = pTitle; pRoster = new ArrayList; 
} 
void AddStudent(String *pName) 
{ 
pRoster->Add(pName); // Добавить 
} 
void Show(String *pCaption) // Показать 
{ 
Console::WriteLine("————{0}————", pCaption) ; 
Console::WriteLine( 
"Course : {0} with {1} students", // Курс: студенты 
pTitle, 
_box(pRoster->Count)); // Счет 
lEnumerator *pEnum = pRoster->GetEnumerator(); 
while (pEnum->MoveNext()) 
{ 
String *pName = 
dynamic_cast(pEnum->Current); 
Console::WriteLine(pName); 
} 
} 
Course *ShallowCopy() // Курс - поверхностная копия 
{ 
return dynamic_cast(MemberwiseClone()); 
} 
Object *Clone() 
{ 
Course *pCourse = new Course(pTitle); // новый Курс 
pCourse->pRoster = 
dynamic_cast(pRoster->Clone()); // Клон 
return pCourse; 
} 
}; 

Тестовая программа создает экземпляр класса Course (Курс), называющийся pCl, a затем разными способами создает его копии с именем рС2.

//CopyDemo.h 
_gc class CopyDemo 
// класс сборщика мусора CopyDemo 
{ 
private: // частный 
static Course *pCl, *pC2; // статический Курс 
public: 
static void Main() 
{ 
Console::WriteLine("Copy is done via pC2 = pCl"); 
// Копия, сделанная через рС2 = pCl 
InitializeCourse (); 
pCl->Show("original"); // Показать ("оригинал"); 
pC2 = pCl; 
pC2->Show("copy"); // Показать ("копия"); 
pC2->pTitle = ".NET Programming"; // Программирование на .NET 
pC2->AddStudent("Charlie"); // Чарли 
pC2->Show("copy with changed title and new student"); 
// Показать ("копия с измененным названием 
//и новым студентом"); 
pCl->Show("original"); // Показать ("оригинал"); 
Console::WriteLine( 
"\nCopy is done via pC2 = pCl->ShallowCopy()") ; 
InitializeCourse (); 
pC2 = pCl->ShallowCopy(); 
pC2->pTitle = ".NET Programming"; // Программирование на .NET 
pC2->AddStudent("Charlie"); // Чарли 
pC2->Show("copy with changed title and new student"}; 
// Показать ("копия с измененным названием и новым 
// студентом"); 
pCl->Show("original"); // Показать ("оригинал"); 
Console::WriteLine( 
"\nCopy is done via pC2 = pCl->Clone()"); 
InitializeCourse (); 
pC2 = dynamic_oast(pCl->Clone()); 
pC2->pTitle = ".NET Programming"; // Программирование 
//на .NET 
pC2->AddStudent("Charlie"); // Чарли 
pC2->Show("copy with changed title and new student"); 
// Показать ("копия с измененным названием 
// новым студентом"); 
pCl->Show("original"); // Показать ("оригинал"); 
} 
private: // частный 
static void InitializeCourse() 
{ 
pCl = new Course("Intro to Managed C++"); 
// новый Курс ("Введение в Управляемый С ++ "); 
pCl->AddStudent("John"); // Джон 
pCl->AddStudent("Mary"); // Мэри 
} 
}; 

Вот выдача программы:

Copy is done via pC2 = pCl 
————original----- 
Course : Intro to Managed C++ with 2 students 
John 
Mary 
————copy——— 
Course : Intro to Managed C++ with 2 students 
John 
Mary 
—----copy with changed title and new student----- 
Course : .NET Programming with 3 students 
John 
Mary 
Charlie 
——--original----- 
Course : .NET Programming with 3 students 
John 
Mary 
Charlie 
Copy is done via pC2 = pCl->ShallowCopy() 
---—copy with changed title and new student--—- 
Course : .NET Programming with 3 students 
John 
Mary 
Charlie 
-----original----- 
Course : Intro to Managed C++ with 3 students 
John 
Mary 
Charlie 
Copy is done via pC2 = pCl->Clone() 
--—-copy with changed title and new student——-- 
Course : .NET Programming with 3 students 
John 
Mary 
Charlie 
- -—original- —— - 
Course : Intro to Managed C++ with 2 students 
John 
Mary 
А вот и перевод выдачи1**: 
Копия сделана через рС2 = рС1 
-----оригинал----- 
Курс: Введение в Управляемый С ++ с 2 студентами 
Джон 
Мэри 
-----копия———— 
Курс: Введение в Управляемый С ++ с 2 студентами 
Джон 
Мэри 
-----копия с измененным названием и новым студентом----- 
Курс: Программирование на .NET с 3 студентами 
Джон 
Мэри 
Чарли 
- ——оригинал—- — 
Курс: Программирование на .NET с 3 студентами 
Джон 
Мэри 
Чарли 
Копия сделана через рС2 = рС1-> ShallowCopy () 
-----копия с измененным названием и новым студентом----- 
Курс: Программирование на .NET с 3 студентами 
Джон 
Мэри 
Чарли 
-—-оригинал---— 
Добавлен, естественно, редактором русского перевода. — Прим. ред. 
Курс: Введение в Управляемый С ++ с 3 студентами 
Джон 
Мэри 
Чарли 
Копия сделана через рС2 = рС1-> Clone () 
-——копия с измененным названием и новым студентом—--- 
Курс: Программирование на .NET с 3 студентами 
Джон 
Мэри 
Чарли 
-----оригинал----- 
Курс: Введение в Управляемый С ++ с 2 студентами 
Джон 
Мэри 

Копирование указателей с помощью присваивания

Первым способом копирования является просто присваивание рС2=рС1. В этом случае мы получаем два указателя на один и тот же объект, и изменения, произведенные с использованием первого указателя, можно будет обнаружить в данных, адресуемых вторым указателем.

_gc class CopyDemo 
// класс сборщика мусора CopyDemo 
{ 
public: 
static void Main() 
{ 
Console::WriteLine("Copy is done via pC2 = pCl"); 
// Копия сделана через рС2 = pCl 
InitializeCourse(); 
pCl->Show("original"); // Показать ("оригинал"); 
pC2 = pCl; 
pC2->Show("copy"); // Показать ("копия"); 
pC2->pTitle = ".NET Programming"; // Программирование на .NET 
pC2->AddStudent("Charlie"); // Чарли 
pC2->Show("copy with changed title and new student"}; 
// Показать ("копия с измененным названием 
//и новым студентом"); 
pCl->Show("original"); // Показать ("оригинал"); 

Экземпляр класса Course (Курс) инициализируется методом InitializeCourse, причем в качестве названия курса выбирается "Intro to Managed C++" (Введение в управляемый C++) и на курс записаны два студента. Затем выполняется присваивание рС2=рС1, и с помощью указателя рС2 изменяется заголовок и добавляется новый студент. Затем демонстрируется, что произведенные изменения видны с помощью обоих указателей. Приведем результат работы первой части программы: Copy is done via pC2 = pCl

--original----- 
Course : Intro to Managed C++ with 2 students 
John 
Mary 
-copy-——- 
Course : Intro to Managed C++ with 2 students 
John 
Mary 
-copy with changed title and new student----- 
Course : .NET Programming with 3 students 
John 
Mary 
Charlie 
-----original----- 
Course : .NET Programming with 3 students 
John 
Mary 
Charlie 

А вот и перевод выдачи: 

Копия сделана через рС2 = рС1 
-----оригинал--—— 
Курс: Введение в Управляемый С ++ с 2 студентами 
Джон 
Мэри 
-----копия----- 
Курс: Введение в Управляемый С ++ с 2 студентами 
Джон 
Мэри 
-----копия с измененным названием и новым студентом----- 
Курс: Программирование на .NET с 3 студентами 
Джон 
Мэри 
Чарли 
-----оригинал----- 
Курс: Программирование на .NET с 3 студентами 
Джон 
Мэри 
Чарли 

Почленное копирование

Теперь проиллюстрируем почленное копирование, выполненное с помощью метода MemberwiseClone класса Object (Объект). Так как этот метод защищен (имеет спецификатор доступа protected), он может быть вызван непосредственно только из экземпляра класса Course (Курс). Поэтому в классе Course (Курс) мы определили метод ShallowCopy, реализованный через метод MemberwiseClone.

_gc class Course : public ICloneable 
// класс сборщика мусора Курс: ICloneable 
{ 
Course *ShallowCopy() 
{ 
return dynamic_cast(MemberwiseClone()); 
} 
}; 

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

_gc class CopyDemo 
// класс сборщика мусора CopyDemo 
{ 
public: 
static void Main() { 
Console::WriteLine( 
"\nCopy is done via pC2 = pCl->ShallowCopy()"); 
InitializeCourse(); 
pC2 = pCl->ShallowCopy(); 
pC2->pTitle = ".NET Programming"; // Программирование на .NET 
pC2->AddStudent("Charlie"); // Чарли 
pC2->Show("copy with changed title and new student"); 
// Показать ("копия с измененным названием 
// и новым студентом"); 
pCl->Show("original"); 
// Показать ("оригинал"); 

Ниже приведен результат работы второй части программы. Видно, что поле Title (Название), существует после копирования в двух независимых экземплярах, но коллекция Roster (Список), представляющая собой список студентов, была скопирована через указатель. Поэтому она одна для обеих копий, и изменения, внесенные с использованием одного указателя, видны через другой.

Copy is done via pC2 = pCl->ShallowCopy() 
———-copy with changed title and new student—-— 
Course : .NET Programming with 3 students 
John 
Mary 
Charlie 
—---original----- 
Course : Intro to Managed C++ with 3 students 
John 
Mary 
Charlie 

А вот и перевод выдачи:

Копия сделана через рС2 = рС1-> ShallowCopy () 
—---копия с измененным названием и новым студентом----- 
— Курс: Программирование на .МЕТ с 3 студентами Джон 
Мэри 
Чарли 
-----оригинал----- 
Курс: Введение в Управляемый С ++ с 3 студентами 
Джон 
Мэри 
Чарли 

Использование ICloneable

Последний способ копирования использует тот факт, что класс Course (Курс) поддерживает интерфейс ICloneable и реализует метод Clone (Клон). Для копирования коллекции Roster (Список) используется также то, что ArrayList (Список массивов) реализует интерфейс ICloneable, как было указано в этой главе ранее. Заметим, что метод Clone (Клон) возвращает значение типа Object*, так что перед тем, как присвоить его полю pRoster, возвращаемое значение необходимо привести к типу ArrayList*.

_gc class Course : public ICloneable 
// класс сборщика мусора Курс: ICloneable 
{ 
public: 
Object *Clone() 
{ 
Course *pCourse = new Course(pTitle); // новый Курс 
pCourse->pRoster = 
dynamic_cast(pRoster->Clone()); 
return pCourse; 
} 
}; 

Приведем третью часть программы, в которой вызывается метод Clone (Клон). Здесь мы тоже с помощью второго указателя изменяем название и добавляем в список нового студента.

_gc class CopyDemo 
// класс сборщика мусора CopyDemo 
{ 
public: 
static void Main() 
{ 
Console::WriteLine( 
"\nCopy is done via pC2 = pCl->Clone()"); 
InitializeCourse (); 
pC2 = dynamic_cast(pCl->Clone()); 
pC2->pTitle = ".NET Programming"; // Программирование на .NET 
pC2->AddStudent("Charlie"); // Чарли 
pC2->Show("copy with changed title and new student"); 
// Показать ("копия с измененным названием 
//и новым студентом"); 
pCl->Shcw("01iginal"); 
// Показать ("оригинал"); 

Приведем выдачу третьей части программы. Теперь видно, что в результате копирования мы получили два независимых экземпляра класса Course (Курс), каждый из которых имеет свой заголовок и список студентов.

Copy is done via рС2 = pCl->Clone() 
—---copy with changed title and new student-—-- 
Course : .NET Programming with 3 students 
John 
Mary 
Charlie 
—---original--—— 
Course : Intro to Managed C++ with 2 students 
John 
Mary 

А вот и перевод выдачи:

Копия сделана через рС2 = рС1-> Clone О 
—---копия с измененным названием и новым студентом-—-- 
Курс: Программирование на .NET с 3 студентами 
Джон 
Мэри 
Чарли 
—----оригинал----- 
Курс: Введение в Управляемый С ++ с 2 студентами 
Джон 
Мэри 

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

Итак, мы подробно рассмотрели копирование объектов. Теперь рассмотрим сравнение объектов. Для сравнения объектов в .NET Framework используется интерфейс ICompa-rable. В этом разделе мы в качестве примера воспользуемся интерфейсом ICompara-Ые для сортировки массива.

Сортировка массива

В классе System::Array (Система::Массив) статический метод Sort (Сортировка) предназначен для сортировки массива. Программа ArrayName иллюстрирует применение этого метода к сортировке массива объектов Name (Имя), где класс Name (Имя) содержит просто строку. Приведем листинг основной программы:

//ArrayName.срр 
_gc class ArrayName 
// класс сборщика мусора ArrayName 
{ 
public: 
static void Main() { 
Name *array[] = new Name*[5]; // Имя 
array[0] = new Name("Michael"); // новое Имя ("Майкл") 
array[l] = new Name("Charlie"); // новое Имя ("Чарли") 
array[2] = new Name("Peter"); // новое Имя ("Питер") 
array[3] = new Name("Dana"); // новое Имя ("Дана") 
array[4] = new Name("Bob"); // новое Имя ("Боб") 
if (dynamic_cast(array[0]) != 0) 
Array::Sort(array); else 
Console::WriteLine( 
"Name does not implement IComparable"); 
// (" Name (Имя) не реализует IComparable"); 
lEnumerator *pEnum = array->GetEnumerator(); 
while (pEnum->MoveNext()) 
{ 
Name *pName = // Имя 
dynamic_cast(pEnum->Current); 
if (pName != 0) 
Console::WriteLine(pName); 
} 
} 
}; 

Реализация IComparable

Для того чтобы можно было произвести сортировку, необходимо определить правила сравнения сортируемых объектов. Такие правила реализуются в методе CompareTo интерфейса IComparable. Поэтому, для того, чтобы иметь возможность сортировать массивы, содержащие данные определенного вами типа, необходимо реализовать интерфейс IComparable для этого типа данных.

_gc _interface IComparable 
// сборщик мусора - IComparable 
{ 
int CompareTo(Object* obj); 
}; 

Приведем листинг реализации класса Name (Имя), включая и реализацию интерфейса

IComparable: 
_gc class Name : public IComparable 
{ 
private: // частный 
String *pText; 
public: 
Name(String *pText) // Имя 
{ 
this->pText = pText; 
} 
_property String* get_Item() 
{ 
return pText; 
} 
_property void set_Itern(String* pText) 
{ 
this->pText = pText; 
} 
int CompareTo(Object *pOb]) // Объект 
{ 
String *pSl = this->pText; 
String *pS2 = 
(dynamic_cast(pObj))->pText; 
return String::Compare(pSl, pS2); // сравниваются имена 
} 
String *ToString() 
{ 
return pText; 
} 
}; 

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

Bob // Боб 
Charlie // Чарли 
Dana // Дана 
Michael // Майкл 
Peter // Питер 

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

  • ваша программа обращается к нижнему уровню;
  • верхний уровень обращается к вашей программе.

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

Интерфейсы облегчают написание профамм в том смысле, что ее составляющие могут быть вызваны другими приложениями или системой. Такой стиль профаммирования известен давно как использование функций обратного вызова (callback function). В этом разделе мы рассмотрим использование делегатов (delegate) в управляемом C++. Делегаты можно рассматривать в качестве объектно-ориентированных функций обратного вызова, обеспечивающих типовую безопасность. Делегаты — основа более сложного протокола вызова функций обратного вызова, называемого событиями, которые мы рассмотрим в следующем разделе главы.

Функции обратного вызова — это функции, которые ваша программа определяет и некоторым образом "регистрирует", после чего они могут быть вызваны другими приложениями. В С и C++ такие функции реализуются с использованием указателей на функции.

В управляемом C++ указатель на метод можно инкапсулировать в объекте-делегате. Делегат может указывать как на статический метод, так и на экземпляр метода. Если делегат указывает на метод экземпляра класса, он хранит и сам экземпляр класса, и точку входа в метод. Таким образом, метод экземпляра класса можно вызвать, используя сам экземпляр класса. Если делегат указывает на статический метод, в нем хранится только точка входа в этот метод.

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

Делегат в действительности является управляемым классом, потомком класса System: : Delegate (Система: Делегат). Новый экземпляр делегата создается, как и для любого другого класса, с помощью оператора new (создать). Делегаты являются объектно-ориентированными и безопасными с точки зрения типов; они позволяют полностью использовать все средства безопасности, предусмотренные в среде выполнения управляемого кода.

В управляемом C++ делегат объявляется с помощью особого обозначения — ключевого слова_delegate (делегат) — и сигнатуры инкапсулированного метода. В соответствии с соглашением об именовании, имя делегата должно заканчиваться буквосочетанием "Callback". Приведем пример объявления делегата:

_delegate void NotifyCallback(Decimal balance); 
// делегировать NotifyCallback (Десятичный баланс); 

После инициализации делегата следует определить метод обратного вызова, сигнатура которого соответствует сигнатуре, описанной в объявлении делегата. Метод может быть как статическим, так и методом экземпляра класса. Приведем несколько примеров методов, которые могут использоваться с объявленным выше делегатом Not i f yCalIback:

static void NotifyCustomer(Decimal balance) // Десятичный баланс 
{ 
Console::WriteLine("Dear customer,"); // Дорогой клиент 
Console::WriteLine( 
" Account overdrawn, balance = {0}", // баланс на счете 
_box(balance)); // баланс 
} 
static void NotifyBank(Decimal balance) // Десятичный баланс 
{ 
Console::WriteLine("Dear bank,"); // Дорогой банк 
Console::WriteLine( 
" Account overdrawn, balance = {0}", // баланс на счете 
_box(balance)); 
} 
void Notifylnstance(Decimal balance) // Десятичный баланс 
{ 
Console::WriteLine("Dear instance,"); // Дорогой представитель 
Console::WriteLine( 
" Account overdrawn, balance = {0}", // баланс на счете 
_box(balance)); // баланс 
}

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

// создать делегат для статического метода NotifyCustomer 
NotifyCallback *pCustDlg = new NotifyCallback( 
0, // ноль для статического метода NotifyCustomer 
NotifyCustomer); 
// создать делегат для экземпляра метода Notifylnstance 
NotifyCallback *p!nstDlg = new NotifyCallback( 
pda, // отличный от нуля для экземпляра метода Notifylnstance 
Notifylnstance); 

Синтаксис "вызова" делегата совпадает с синтаксисом вызова метода. Делегат не является сам по себе методом, но он инкапсулирует метод. Делегат "передает" вызов инкапсулированному методу, потому и называется делегатом (от англ, delegate — поручать, уполномочивать). В приведенном ниже фрагменте кода делегат notif yDlg вызывается в случае, если при выплате со счета получается отрицательный баланс. В этом примере экземпляр notif yDlg инициализируется в методе SetDelegate.

_gc class Account 
// класс сборщика мусора Счет 
{ 
private: // частный 
Decimal balance; // Десятичный баланс 
NotifyCallback *pNotifyDlg; 
void SetDelegate(NotifyCallback *pDlg) 
{ 
pNotifyDlg = pDlg; } 
void Withdraw(Decimal amount) // Десятичное количество 
{ 
balance = balance - amount; 
// баланс = баланс - количество; 
if (balance < 0) // если баланс <0, ситуация овердрафта 
pNotifyDlg(balance); callback // баланс, обратный вызов 
} 

Несколько делегатов можно объединить в один так, чтобы результирующий делегат имел список вызываемых методов. При вызове подобного делегата будут вызваны по очереди все методы, содержащиеся в списке вызываемых методов этого делегата. Полезным свойством делегатов является возможность объединять списки вызываемых делегатом методов и удалять методы из таких списков. Для этого используются статические методы Delegate: :Combine (Делегат::Объединение) и Delegate: :Remove (Делегат::Удалить). Кроме того, для класса Delegate (Делегат) операторы += и -= перегружены так, чтобы обеспечить сокращенный синтаксис добавления и удаления методов.

// псевдокод: pCurrDlg = pCustDlg + pBankDlg 
pCurrDlg = static_cast( 
Delegate::Combine(pCustDlg, pBankDlg)); // Делегат::Объединение 
// дополнительный код: pCurrDlg - = pBankDlg 
pCurrDlg = static_cast( 
Delegate::Remove(pCurrDlg, pBankDlg)); // Делегат::Удалить 
// дополнительный код: pCurrDlg + = plnstDlg 
pCurrDlg = static_cast( 
Delegate::Combine(pCurrDlg, plnstDlg)); // Делегат: Объединение 

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

Программа DelegateAccount иллюстрирует использование делегатов на примере банковского счета. В файле DelegateAccount. cpp объявляется делегат NotifyCallback. Методы, соответствующие по сигнатурам методам, описанным в объявлении делегата, реализованы в классе DelegateAccount. Метод Мат (Главный) создает экземпляры делегатов и объединяет их различными способами. Указатель на экземпляр делегата передается экземпляру класса Account (Счет), который использует инкапсулированные делегатом методы для выдачи соответствующего ситуации сообщения об овецдрафте.

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

Приведем исходный код класса Account (Счет):

//Account.h 
_delegate void NotifyCallback(Decimal balance); // делегировать NotifyCallback (Десятичный баланс); 
_gc class Account 
// класс сборщика мусора Счет 
{ 
private: // частный 
Decimal balance; // Десятичный баланс 
NotifyCallback *pNotifyDlg; public: 
Account(Decimal bal, NotifyCallback *pDlg) // Счет 
{ 
balance = bal; // баланс 
pNotifyDlg = pDlg; 
} 
void SetDelegate(NotifyCallback *pDlg) 
{ 
pNotifyDlg = pDlg; 
} 
void Deposit(Decimal amount) // Депозит (Десятичное количество) 
{ 
balance = balance + amount; 
// баланс = баланс + количество; 
} 
void Withdraw(Decimal amount) // Десятичное количество 
{ 
balance = balance - amount; 
// баланс = баланс - количество; 
if (balance < 0) // если (баланс <0) ситуация кредита 
//по текущему счету 
pNotifyDlg(balance); // баланс - обратный вызов 
} 
_property Decimal get_Balance() // Десятичное число 
} 
return balance; // баланс 
} 
_property void set_Balance(Decimal balance) // Десятичный 
// баланс 
{ 
this->balance = balance; // баланс 
} 
}; 

Приведем исходный код объявления и тестирования делегата:

//DelegateAccount.h 
_gc class DelegateAccount 
// класс сборщика мусора DelegateAccount 
{ 
public: 
static void Main() // Главный 
{ 
// создать делегат для статического метода NotifyCustomer 
NotifyCallback *pCustDlg = new NotifyCallback( 
О, // ноль для статического метода NotifyCustomer 
NotifyCustomer); 
// создать делегат для статического метода NotifyBank 
NotifyCallback *pBankDlg = new NotifyCallback( 
О, // ноль для статического метода NotifyBank 
NotifyBank); 
// объявить, что делегат-объект используется // объектом Account NotifyCallback *pCurrDlg; 
// псевдокод: pCurrDlg = pCustDlg + pBankDlg pCurrDlg = static_cast( 
Delegate::Combine(pCustDlg, pBankDlg)); // Делегат: 
// Объединение 
// создать объект Account и установить 
// делегат для использования 
Account *рАсс = new Account(100, pCurrDlg); // новый Счет 
Console::WriteLine( 
"balance = {0}", _box(pAcc->get_Balance())); // баланс 
// вызвать делегат два раза 
pAcc->Withdraw(125); // обратный вызов через делегат! 
Console::WriteLine( 
"balance = {0}", _box(pAcc->get_Balance())); // баланс 
pAcc->Deposit(200);// Депозит pAcc->Withdraw(125); 
// кредит по текущему счету не нужен, // так что не вызывать обратно 
Console::WriteLine( 
"balance = {0}", _box(pAcc->get_Balance())); // баланс 
// альтернатива: pCurrDlg - = pBankDlg 
pCurrDlg = static_cast( 
Delegate::Remove(pCurrDlg, pBankDlg)); // Делегат:: 
// Удалить 
// установить новый делегат, который используется 
// объектом Account 
pAcc->SetDelegate(pCurrDlg); 
// вызвать делегат 
pAcc->Withdraw(125); // обратный вызов через делегат! 
// создать экземпляр, требуемый для экземпляра делегата DelegateAccount *pda = new DelegateAccount(); 
// создать делегат для экземпляра метода Notifylnstance NotifyCallback *p!nstDlg = new NotifyCallback( 
pda, // отличный от нуля для метода Notifylnstance 
Notifylnstance); 
// дополнительный код: pCurrDlg + = plnstDlg 
pCurrDlg = static_cast( 
Delegate::Combine(pCurrDlg, plnstDlg)); // Делегат: 
// Объединение 
// установить новый делегат, который используется 
// объектом Account pAcc->SetDelegate(pCurrDlg); 
pAcc->Withdraw(125); // обратный вызов через делегат! 
} 
private: // частный 
static void NotifyCustomer(Decimal balance) // Десятичный 
// баланс 
{ 
Console::WriteLine("Dear customer,"); // Дорогой клиент, 
Console::WriteLine( 
" Account overdrawn, balance = {0}", // Счет превышен, 
// баланс 
_box(balance)); // баланс 
} 
static void NotifyBank(Decimal balance) // Десятичный баланс 
{ 
Console::WriteLine("Dear bank,"); // Дорогой банк, 
Console::WriteLine( 
" Account overdrawn, balance = {0}", // Счет превышен, 
// баланс 
_box(balance)); // баланс 
} 
void Notifylnstance(Decimal balance) // Десятичный баланс 
{ 
Console::WriteLine("Dear instance,"); // Дорогой 
// представитель 
Console::WriteLine( 
" Account overdrawn, balance = {0}", // Счет превышен, 
// баланс 
_box(balance)); // баланс 
} 
}; 

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

balance = 100 
Dear customer, 
Account overdrawn, balance = -25 
Dear bank, 
Account overdrawn, balance = -25 
balance = -25 
balance = 50 
Dear customer, 
Account overdrawn, balance = -75 
Dear customer, 
Account overdrawn, balance = -200 
Dear instance, 
Account overdrawn, balance = -200 

А вот и перевод выдачи:

баланс = 100 Дорогой клиент, 
Овердрафт по счету, баланс =-25 
Дорогой банк, 
Овердрафт по счету, баланс =-25 
баланс =-25 
баланс = 50 
Дорогой клиент, 
Овердрафт по счету, баланс =-75 
Дорогой клиент, 
Овердрафт по счету, баланс =-200 
Дорогой представитель, 
Овердрафт по счету, баланс =-200 

Для дальнейшего знакомства с использованием делегатов рассмотрим пример моделирования фондовой биржи, реализованный в папке stockMarket. Модель состоит из двух модулей:

  • Модуль Admin (Управляющий модуль) предоставляет пользовательский интерфейс для конфигурирования и запуска модели. Кроме того, в нем реализованы операции, вызываемые моделирующей машиной.
  • Модуль Engine (Машинный модуль) — это и есть моделирующая машина. В данном модуле есть внутренние часы; модуль случайным образом, в соответствии с параметрами конфигурации, генерирует информацию о совершаемых сделках.

На рис. 5.2 показана общая архитектура модели.

Модель допускает выполнение следующих операций:

  • PrintTick: показывать ход часов (номер текущего шага);
  • PrintTrade: показывать все совершаемые сделки.

Модель содержит следующие параметры:

  • включить/выключить вывод информации о текущем шаге;
  • включить/выключить вывод информации о совершаемых сделках;
  • установить количество шагов моделирования.

Рис. 5.2. Архитектура эмулятора фондовой биржи

Запуск моделирования

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

Ticks are OFF 
Trades are ON 
Run count = 100 
Enter command, quit to exit 
: run 
2 ACME 23 600 
27 MSFT 63 400 
27 IBM 114 600 
38 MSFT 69 400 
53 MSFT 75 900 
62 INTC 27 800 
64 MSFT 82 200 
68 MSFT 90 300 
81 MSFT 81 600 
83 INTC 30 800 
91 MSFT 73 700 
99 IBM 119 400 

Список доступных команд выводится по команде help (помощь). Вот что выводится по этой команде:

count set run count 
ticks toggle ticks 
trades toggle trades 
config show configuration 
run run the simulation 
quit exit the program 

При выполнении программы выводится информация о шаге, акциях, цене и количестве проданных акций.

Определение делегата

В файле Engine . h объявлены два делегата.

_delegate void TickCallback(int ticks); 
_delegate void TradeCallback( 
int ticks, String *pStock, int price, int volume); 

Как мы уже знаем, делегат подобен классу, так что экземпляр делегата создается с помощью оператора new (создать).

TickCallback *pTickDlg = 
new TickCallback(0, PrintTick); //0 для статического 
TradeCallback *pTradeDlg = 
new TradeCallback(0, PrintTrade); //0 для статического 

Имя метода передается делегату в качестве аргумента его конструктора. Сигнатура метода должна совпадать с сигнатурой делегата.

static void PrintTick(int ticks) 
{ 
Console::Write("{0} ", _box(ticks)); 
if (++printcount == LINECOUNT) 
{ 
Console::WriteLine(); printcount = 0; 
} 
} 
static void PrintTrade( 
int ticks, String *pStock, int price, int volume) 
{ 
if (printcount != 0) 
{ 
Console::WriteLine(); 
} 
printcount = 0; 
Console::WriteLine("{0,4} {l,-4} {2,4} {3,4}", 
_box(ticks), pStock, 
_box(price), _box(volume)); 
}

Передача информации о делегате эмулятору

Класс Admin (Управляющий модуль) передает информацию об используемом делегате классу Engine (Машинный модуль) при вызове конструктора класса Engine (Машинный модуль).

Engine *pEngine = new Engine(pTickDlg, pTradeDlg); 

Генерация случайных чисел

Основой эмулятора является метод Run (Запуск) класса Engine (Машинный модуль). Основная работа метода Run (Запуск) состоит в присвоении данных, полученных в результате генерации случайных чисел. Для генерации случайных чисел используется класс System: :Random (Система::Случайный), рассмотренный нами в главе 3 "Программирование на управляемом C++".

double г = pRangen->NextDouble(); 
if (r < tradeProbti]) 
{ 
int delta = // дельта 
(int) (price[i] * volatility[i]); // цена * изменение 
if (pRangen->NextDouble() < .5) 
{ 
delta = -delta; // дельта = - дельта 
} 
price[i] += delta; // цена + = дельта 
int volume = pRangen->Next( 
minVolume, maxVolume) * 100; 
pTradeOp( 
tick, stocks[i], price [i], volume); // шаг, акции, 
//цена, объем 

Использование делегатов

Указатели на экземпляры делегатов объявляются в классе Engine (Машинный модуль):

TickCallback *pTickOp; 
TradeCallback *pTradeOp; 

Указатели на делегаты инициализируются в конструкторе класса Engine (Машинный модуль):

Engine(TickCallback *pTickOp, TradeCallback *pTradeOp) 
{ 
this->pTickOp = pTickOp; this->pTradeOp = pTradeOp; 
} 

Метод, связанный с экземпляром делегата, можно вызвать, используя указатель на делегат:

pTickOp(tick); 
pTradeOp( 
tick, stocks[i], price [i], volume); // шаг, акции, цена, объем 

Делегаты — основа более сложного протокола обратного вызова, называемого событиями. Согласно замыслу, сервер реализует входящие интерфейсы, которые могут быть вызваны клиентом. На диаграммах подобный интерфейс можно обозначить с помощью маленького кружка (обозначение, использующееся в модели компонентных объектов Microsoft (COM)). Иногда клиенту может понадобиться получать от сервера сообщения при возникновении некоторых "событий". Для подобных ситуаций сервером определяется исходящий интерфейс. Ключевой идеей механизма событий является то, что сервер определяет интерфейс, но реализует этот интерфейс клиент. На диаграммах такой интерфейс обозначается стрелкой (опять же, в обозначениях модели компонентных объектов Microsoft (COM)). На рис. 5.3 представлена диаграмма, изображающая сервер с одним входящим и одним исходящим интерфейсами. Для использования исходящего (для сервера) интерфейса клиент реализует входящий (для клиента) интерфейс, обратный вызов которого осуществляется сервером.

.NET Framework позволяет использовать простую реализацию идеи событий, основанную на применении делегатов. В управляемом C++ работа с событиями .NET основана на использовании ключевого слова _event (событие) и операторов, предназначенных для добавления и удаления обработчиков событий. Мы рассмотрим концепцию событий на примере чат-программы EventDemo (Диалоговая комната для дискуссий).

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

_delegate void JoinHandler( 
Object *pSender, ChatEventArg *pe); 
_delegate void QuitHandler( 
Object *pSender, ChatEventArg *pe); 

Первый параметр определяет объект, посылающий сообщение о событии. Второй параметр используется для передачи данных одновременно с сообщением о событии. Обычно для хранения таких данных используется класс, производный от EventArg.

Рис. 5.3. Сервер с входящим и исходящим интерфейсами

_gc class ChatEventArg : public EventArgs 
// класс сборщика мусора ChatEventArg: EventArgs 
{ 
public: 
String *pName; 
ChatEventArg(String *pName) 
{ 
pName = pName; 
} }; 

Указатель на экземпляр делегата объявляется с использованием ключевого слова _event (событие).

_gc class ChatServer // класс сборщика мусора ChatServer 
{ 
public: 
_event JoinHandler *pJoin; 
_event QuitHandler *pQuit; 

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

_gc class ChatServer 
// класс сборщика мусора ChatServer 
{ 
protected: // защищенный 
void OnJoin(ChatEventArg *pe) 
{ 
if (pJoin != 0) 
{ 
pJoin(this, pe); // запуск события 
} 
} 
void OnQuit(ChatEventArg *pe) 
{ 
if (pQuit != 0) 
{ 
pQuitfthis, pe); // запуск события 
} 
} 

Приведенный здесь вспомогательный метод проверяет, обрабатывается ли событие каким-либо экземпляром делегата. (Проверка проводится сравнением с 0.) Обычно такие вспомогательные методы объявляются как защищенные (protected), так что доступ к ним имеют только производные классы.

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

_gc class ChatServer 
// класс сборщика мусора ChatServer 
{ 
public: 
void JoinChat(String *pName) 
{ 
pMembers->Add(pName); // Добавить 
OnJoin(new ChatEventArg(pName)); 
} 
void QuitChat(String *pName) 
{ 
pMembers->Remove(pName); // Удалить 
OnQuitfnew ChatEventArg(pName)); 
} 

В этой части программы реализованы функции-обработчики событий.

_gc class ChatClient 
// класс сборщика мусора ChatClient 
{ 
public: 
static void OnJoinChat(Object *pSender, ChatEventArg *pe) 
{ 
Console::WriteLine( 
"sender = {0}, {1} has joined the chat", 
// "отправитель = {0}, {1} присоединился к чату ", 
pSender, 
pe->pName); 
} 
static void OnQuitChat(Object *pSender, ChatEventArg *pe) 
{ 
Console::WriteLine( 
"sender = {0}, {1} has guit the chat", 
// "отправитель = {0}, {1} оставил чат ", 
pSender, 
pe->pName); 
} 

Клиент связывает программу обработки с событиями с помощью оператора +=

static void Main() 
{ 
// создать сервер чата 
ChatServer *pChat = new CratServer("01 Chat Room"); // Комната 
// для дискуссий 01 
// зарегистрировать, чтобы получать уведомления 
// о событиях от сервера 
pChat->pJoin += new Jo_n4andler(pChat, OnJoinChat); 
pChat->pQuit += new QuitHanaler(pCnat, OnQuitChat); 

Изначально событие представлено пустым указателем (null), т е не связано с каким-либо обработчиком событий, а добавление обработчиков событий происходит в процессе выполнения программы с помощью оператора += При вызове делегата, соответствующего событию, будут вызваны все зарегистрированные таким образом обработчики событий Отменить регистрацию обработчика событий можно оператором -=.

Пример чат-программы EventOeTO иллюстрирует архитектуру как сервера, так и клиента В сервере реализованы следующие методы

  • JomChat,
  • QuitChat;
  • ShowMembers

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

sender = 01 Chat Room, Michael has joined the chat 
sender = 01 Chat Room, BOD has -oned the chat 
sender = 01 Chat Room, Sam nas ]oinea tne chat 
--- After 3 nave joined--- 
Michael 
Bob 
Sam 
sender = 01 Chat Room, BOD has qait the chat 
--- After 1 has quit--- 
Michael 
Sam 

А вот и перевод:

отправитель = Комчата для дискуссий 01, Майкл присоединился к чату 
отправитель = Комната для дискуссий 01, Боб присоединился к чату 
отправителе = Коуната для дискуссий 01, Сэм присоединился к чату 
---После того, как 3 присоединились--- 
Майкл 
Боб 
Сэм 
отправитель = Комната дгя дискуссий 01, Боб оставил чат 
---После того, как 1 покинул--- 
Майкл 
Сэм 

Исходный код клиента

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

//ChatClient.h 
_gc class ChatClient 
// класс сборщика мусора ChatClient 
{ 
public: 
static void OnJoinChat(Object *pSender, ChatEventArg *pe) 
{ 
Console::WrxteLine 
{ 
"sender = {0}, {1} has joined the chat", 
// "отправитель = {0}, {1} присоединился к чату ", 
pSender, 
pe->pName ; 
} 
static void OnQuitChat(Object *pSender, ChatEventArg *pe) 
{ 
Console::WriteLine( 
"sender = 40}, {1} has quit the chat", // "отправитель = {0}, {1} покинул чат ", 
pSender, pe->pName); 
} 
static void Main() 
{ 
// создать сервер чата 
ChatServer *pChat = new ChatServer("01 Chat Room"); 
// "Комната для дискуссий 01" 
// Регистрация обработчиков сообщений от сервера 
pChat->pJoin += new JoinHandler(pChat, OnJoinChat); 
pChat->pQuit += new QuitHandler(pChat, OnQuitChat); 
// вызвать методы сервера 
pChat->JoinChat("Michael"); // Майкл 
pChat->JoinChat/'Bob"); // Боб 
pChat->JoinChat("Sam"); // Сэм 
pChat->ShowMembers("After 3 have joined"); 
// "После того, как 3 присоединились" 
pChat->QuitChat("Bob"); // Боб 
pChat->ShowMembers("After 1 has quit"); 
// "После того, как 1 ушел" 
} 
}; 

Исходный код сервера

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

//ChatServer.h 
_gc class ChatEventArg : public EventArgs 
// класс сборщика мусора ChatEventArg: EventArgs 
{ 
public: 
String *pName; 
ChatEventArg(String *pName) 
{ 
this->pName = pName; 
} 
}; _delegate void JoinHandler( 
Object *pSender, ChatEventArg *pe); 
_delegate void QuitHandler( 
Object *pSender, ChatEventArg *pe); 
_gc class ChatServer // класс сборщика мусора ChatServer 
{ 
private: // частный 
ArrayList *pMembers; 
String *pChatName; 
public: 
_event JoinHandler *pJoin; 
_event QuitHandler *pQuit; 
ChatServer(String *pChatName) 
{ 
pMembers = new ArrayList; 
this->pChatName = pChatName; 
} 
String *ToString () 
{ 
return pChatName; 
} 
protected: // защищенный 
void OnJoin(ChatEventArg *pe) 
{ 
if (pJoin != 0) 
{ 
pJoin(this, pe); // запустить событие 
} 
} 
void OnQuit(ChatEventArg *pe) 
{ 
if (pQuit != 0) 
{ 
pQuit(this, ре); // запустить событие 
} 
} 
public: 
void JoinChat(String *pName) 
{ 
pMembers->Add(pName); // Добавить 
OnJoin(new ChatEventArg(pName)); 
} 
void QuitChat(String *pName) 
{ 
pMembers->Remove(pName); // Удалить 
OnQuit(new ChatEventArg(pName)); 
} 
void ShowMembers(String *pMsg) 
{ 
Console:rWriteLine ("—— {0} ——", pMsg); 
lEnumerator *plter = pMembers->GetEnumerator(); 
while (p!ter->MoveNext()) 
{ 
String *pMember = 
dynamic_cast((p!ter->Current)}; 
Console::WriteLine(pMember); 
} 
} 
}; 

Поначалу может показаться, что здесь немалый объем вспомогательного кода, но этот подход намного проще, чем прежний — механизм точек стыковки, — реализованный для событий в СОМ.

В этой главе рассмотрены некоторые важные связи управляемого C++ и .NET Framework, причем начали мы с базового класса Object (Объект). Мы рассмотрели использование коллекций, в частности, те методы класса Object (Объект), которые следует переопределять для работы с коллекциями. Очень подробно мы обсудили концепцию интерфейсов, позволяющую разработчику строго определять свойства, которые должны быть реализованы в классе. Хотя класс в управляемом C++ может иметь только один базовый класс, он может реализовывать несколько интерфейсов. Другим достоинством интерфейсов является то, что они значительно облегчают создание динамичных программ. Управляемый C++ обеспечивает возможность во время выполнения программы послать запрос классу для выяснения, поддерживает ли он определенный интерфейс.

Мы подробно рассмотрети интерфейсы используемые для работы с коллекциями, и виды копирования объектов В обычном C++ для копирования объектов используются специальные языковые средства — конструкторы копирования, а в управляемом C++ те же возможности обеспечиваются реализацией особого интерфейса ICloneable В итоге мы пришли к изучению ропи родовых интерфейсов в методологии программирования NET Framework и сравнению использования компонентов NET и СОМ Использование родовых интерфейсов также проиллюстрировано на примере сортировки коллекций с помощью интерфейса. Соответствующие примеры позволили полнее ощутить отличие каркаса притожении от простои библиотеки классов При использовании каркаса притожении программа может вызывать методы каркаса, а те могут вызывать методы программы Поэтому создаваемый код можно уподобить среднему слою сандвича Этот пример помошет понять для чего необходима платформа NET

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

назад главная вперед

Главная Новости Visual Studio .NET Самоучитель C# Учебники О сайте
Framework 3.0
Microsoft официально заявила о переименовании своего API WinFX в более привычный для слуха .NET Framework 3. Это, вероятно, последнее переименование. Нет уже Longhorn (Vista), канули в прошлое Avalon (WPF), Indigo (WCF). Время пришло переименовать WinFX…
DirectX 10
Новый набор интерфейсов программирования приложений (API) DirectX 10 увеличит скорость работы компьютеров при запуске ряда приложений в 4 раза. Об этом заявили разработчики DirectX компания Microsoft, сообщается на сайте The Inquirer...
Microsoft в России
Компания Microsoft открыла производство в России, пишет газета "Ведомости". Диски с Windows и Microsoft Office с марта печатаются на ярославском полиграфическом комбинате ООО "Сонопресс". Sonopress входит в концерн Bertelsmann и производит диски также для Sony BMG...
счетчик посещений
Используются технологии uCoz