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