Копирование объектов и интерфейс icioneable

Иногда бывает необходимо сделать копию объекта. Если при этом копируются объекты, которые содержат другие объекты или указатели на них, то для корректной реализации этого процесса необходимо понимать его особенности. Ниже мы сравним копирование указателя, поверхностное почленное копирование и детальное копирование. Мы увидим, что в зависимости от реализации интерфейса 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<String *>(pEnum->Current);
Console::WriteLine(pName);
}
}
Course *ShallowCopy() // Курс - поверхностная копия
{
return dynamic_cast<Course *>(MemberwiseClone());
}
Object *Clone()
{
Course *pCourse = new Course(pTitle); // новый Курс
pCourse->pRoster =
dynamic_cast<ArrayList *>(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<Course *>(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<Course *>(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<ArrayList *>(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<Course *>(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<IComparable *>(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<Name *>(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<Name *>(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<NotifyCallback *>(
Delegate::Combine(pCustDlg, pBankDlg)); // Делегат::Объединение
// дополнительный код: pCurrDlg - = pBankDlg
pCurrDlg = static_cast<NotifyCallback *>(
Delegate::Remove(pCurrDlg, pBankDlg)); // Делегат::Удалить
// дополнительный код: pCurrDlg + = plnstDlg
pCurrDlg = static_cast<NotifyCallback *>(
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<NotifyCallback *>(
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<NotifyCallback *>(
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<NotifyCallback *>(
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: показывать все совершаемые сделки.
Модель содержит следующие параметры:
• включить/выключить вывод информации о текущем шаге;
• включить/выключить вывод информации о совершаемых сделках;
• установить количество шагов моделирования.
Запуск моделирования
Скомпонуйте и запустите программу 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 представлена диаграмма, изображающая сервер с одним входящим и одним исходящим интерфейсами. Для использования исходящего (для сервера) интерфейса клиент реализует входящий (для клиента) интерфейс, обратный вызов которого осуществляется сервером.

События в управляемом C++ и .NET


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

Описание сервера


Начнем рассмотрение с реализации сервера, находящейся в файле ChatServer. h. В архитектуре событий .NET используются делегаты с особой сигнатурой:
_delegate void JoinHandler(
Object *pSender, ChatEventArg *pe);
_delegate void QuitHandler(
Object *pSender, ChatEventArg *pe);
Первый параметр определяет объект, посылающий сообщение о событии. Второй параметр используется для передачи данных одновременно с сообщением о событии. Обычно для хранения таких данных используется класс, производный от EventArg.
_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<String *>((p!ter->Current)};
Console::WriteLine(pMember);
}
}
};
Поначалу может показаться, что здесь немалый объем вспомогательного кода, но этот подход намного проще, чем прежний — механизм точек стыковки, — реализованный для событий в СОМ.

Резюме


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

 
На главную | Содержание | < Назад....Вперёд >
С вопросами и предложениями можно обращаться по nicivas@bk.ru. 2013 г. Яндекс.Метрика