Управляемый C++ в .NET Framework

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

Объект системы: System::Object


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

Общедоступные методы экземпляров класса Object (Объект)

Есть четыре общедоступных метода экземпляров класса Object (Объект), три из которых являются виртуальными и часто подменяются в управляемых классах.
Метод Equals (Равняется)
public: virtual bool Equals(Object*);
// виртуальный логический (булев) Равняется (Объект *);
Этот метод сравнивает объект, указатель на который передается методу в качестве аргумента, с текущим объектом. В случае равенства объектов метод возвращает true (истина). В классе Object (Объект) данный метод сравнивает только указатели на объекты. В классе ValueType этот метод подменен и производит сравнение содержимого объектов. Многие классы, в свою очередь, подменяют этот метод для того, чтобы приспособить условия сравнения объектов к своим нуждам. Существует также и статическая версия метода Equals (Равняется), сравнивающая два объекта, указатели на которые передаются ему в качестве аргументов.
Метод ToString
public: virtual String* ToString(); // виртуальный
Этот метод возвращает строку, содержащую удобочитаемое для человека описание объекта. Принятая по умолчанию версия, реализованная в объекте Object (Объект), возвращает просто полное имя типа. Производные классы часто перегружают данный метод, чтобы возвращаемое описание объекта было более содержательным.
Метод GetHashCode
public: virtual int GetHashCode(); // виртуальный
Этот метод возвращает значение хеш-функции объекта, который может использоваться в алгоритмах хэширования и хэш-таблицах. Классы, подменяющие данный метод, должны также подменять метод Equals (Равняется) для того, чтобы равные объекты возвращали одинаковые значения хеш-функции. Если этого не сделать, класс Hashtable (Хэш-таблица) не сможет корректно работать с объектами используемого класса.
Метод CetType
public: Type* GetType();
Этот метод возвращает информацию о типе объекта. Такая информация может быть использована для получения связанных метаданных посредством отражения (reflection), которое мы рассмотрим в главе 8 "Классы каркаса .NET Framework". Это метод не виртуальный, поэтому подменять его обычно не приходится.

Защищенные методы экземпляров класса object (Объект)


Защищенными являются два метода класса Object (Объект). Эти методы могут использоваться только производными классами.
Метод MemberwiseClone
protected: Object* MemberwiseClone(); // защищенный
Данный метод создает поверхностную (shallow) копию объекта. Это метод не виртуальный, поэтому подменять его обычно не приходится. Для того чтобы сделать детальную (deep) копию, следует использовать интерфейс ICloneable. Разница между поверхностной и детальной копией будет рассмотрена в этой главе несколько позже.
Метод Finalize (Завершить)
-Object();
Этот метод позволяет освободить используемые объектом неуправляемые ресурсы и выпочнить другие операции, необходимые при сборке мусора (утилизации неиспользуемой памяти и других ресурсов). В управляемом C++ метод Finalize (Завершить) имеет такой же синтаксис, как и деструктор в обычном C++. Но при этом семантика данного метода качественно отличается от семантики деструктора в обычном C++. В обычном C++ деструктор вызывается детерминированно и синхронно. В управляемом C++ для сборщика мусора создается независимый поток.

Родовые интерфейсы и обычное поведение


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

Использование методов класса object (Объект) в классе Customer (Клиент)


Для иллюстрации описанных методов рассмотрим класс Customer (Клиент) до и после перегрузки методов Equals (Равняется), ToString и GetHashCode.
Стандартные методы класса object (Объект)
Если не произвести подмены виртуальных методов объекта Object (Объект), наш класс будет наследовать стандартное поведение. Это поведение продемонстрировано в CustomsrObjectXStepl.
//Customer.срр
#using <mscorlib.dll>
using namespace System;
// использование пространства имен Система;
#include "Customer.h"
//Customer.h
_gc class Customer // сборщик мусора - класс Клиент
{
public:
int nCustomerld;
String *pFirstName;
String *pLastName;
String *pEmailAddress;
Customer(int id. String *pFirst, // Клиент (int идентификатор,
// Строка *pFirst, String *pLast, String *eMail)
{
nCustomerld = id; // идентификатор
pFirstName = pFirst;
pLastName = pLast;
pEmailAddress = eMail; // электронная почта
}
};
//TestCustomer.срр
ftusing <mscorlib.dll>
using namespace System;
// использование пространства имен Система;
#include "Customer.h"
#include "TestCustomer.h"
void main(void)
{
TestCustomer::Main(); // Главный }
//TestCustomer.h
_gc class TestCustomer
// класс сборщика мусора TestCustomer
{
public:
static void Main() // статическая Главная
{
Customer *pCustl, *pCust2; // Клиент
pCustl = new Customer ( // новый Клиент
99, "John", "Doe", "john@rocky.com"); // "Джон", "Доу"
pCust2 = new Customer( // новый Клиент
99, "John", "Doe", "john@rocky.com"); // "Джон", "Доу"
ShowCustomerObject("pCustl", pCustl);
ShowCustomerObject("pCust2", pCust2);
CompareCustomerObjects(pCustl, pCust2);
}
private: // частный
static void ShowCustomerObject( String *pLabel, Customer *pCust)
{
Console::WriteLine("——— {0} ——", pLabel);
Console::WriteLine(
"ToStringO = {0}", pCust->ToString());
Console::WriteLine(
"GetHashCodeO = {0}", _box(pCust->GetHashCode() ) ) ;
Console::WriteLine(
"GetTypeO = {0}", pCust->GetType () ) ;
}
static void CompareCustomerObjects( Customer *pCustl, Customer *pCust2) // Клиенты
{
Console::WriteLine(
"Equals() = {0}", // Равняется
_box(pCustl->Equals( // Равняется
dynamic_cast<0b]ect _gc *>(pCust2)))); // сборщик
// мусора
}
};
Запустите испытательную программу, и вы получите следующий результат:
——— custl ——— // ———custl ———
ToString () = Customer // ToString () = Клиент
GetHashCode () = 4 // GetHashCode () = 4
GetType () = Customer // GetType () = Клиент
——— cust2 ——— // ———cust2 ———
ToString () = Customer // ToString () = Клиент
GetHashCode () = 6 // GetHashCode () = 6
GetType () = Customer // GetType () = Клиент
Equals() = False // Равняется () = Ложь
Понятно, что реализация, принятая по умолчанию, — это совсем не то, что желательно иметь для объекта Customer (Клиент). Метод ToString возвращает имя класса, а не информацию о конкретном клиенте. Метод Equals (Равняется) сравнивает только указатели на объекты, а не сами объекты. В приведенном примере два разных указателя указывают на одинаковые (по содержащейся информации) объекты класса Customer (Клиент), но метод Equals (Равняется) при этом возвращает значение false (ложь).
Подмена методов класса object (Объект)
Версия проекта, содержащаяся в папке CustomerOb;ject\Step2, демонстрирует подмену виртуальных методов Ob] ect (Объект)
//Customer.h
_gc class Customer
// сборщик мусора - класс Клиент
{
public:
int nCustomerld;
String *pFirstName,
String *pLastName;
String *pEmailAddress;
Customer(int id, String *pFirst, // Клиент int идентификатор,
// Строка *pFirst, String *pLast, String *eMail)
{
nCustomerId = id; // идентификатор
pFirstName = pFirst;
pLastName = pLast;
pEmailAddress = eMail; // электронная почта
}
public:
bool Equals(Object *pobj)
// логический (булев) Равняется (Объект *pobj)
{
Customer *pCust = // Клиент
dynamic_cast<Customer *>(pob]);
return (pCust->nCustomerId == nCustomerld),
}
int GetHashCode()
{
return nCustomerld,
}
String *ToStnng()
{
return String::Format( // Формат
"{0} {l}", pFirstName, pLastName);
}
};
Остальная часть программы не изменена Теперь результат выполнения программы будет таким:
--- custl --- // ---custl---
ToStnng () = John Doe // ToString () = Джон Доу
GetHashCode () = 99 // GetHashCode () = 99
GetTypeO = Customer // GetType () = Клиент
--- cust2 --- // --- cust2 ---
ToString () = John Doe // ToString () = Джон Доу
GetHashCode () = 99 // GetHashCode () = 99
GetType () = Customer // GetType () = Клиент
Equals () = True // Равняется () = Истина

Коллекции


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

Пример класса ArrayList (Список массивов)


Для начала приведем простой пример использования класса ArrayList (Список массивов) Как понятно из названия (Array List — Список массивов), ArrayList — это список объектов, хранимый подобно массиву Размер списка массивов может динамически изменяться, и может расти при добавлении новых элементов
Классы коллекций содержат экземпляры класса ОЬц ect (Объект) Мы создадим и будем иметь дело с коллекцией объектов Customer (Клиент) Использовать любой другой встроенный или определяемый пользователем управляемый тип ничуть не сложнее При использовании простого типа, такого, как int, экземпляр данного типа для сохранения в коллекции должен быть упакован (boxed), а перед его использованием — распакован обратно в int
Взятая для примера программа называется CustomerCollection В ней инициализируется список клиентов, после чего пользователь может просмотреть данный список, зарегистрировать нового клиента, отменить регистрацию клиента или изменить адрес его электронной почты Вызов простого метода help (помощь) отображает список доступных команд
Enter command, quit to exit
H> help
The following commands are available:
register register a customer
unregister unregister a customer
email change email address
show show customers
quit exit the program
Вот перевод:
Введите команду, quit для выхода из программы
Н> help
Доступны следующие команды:
register (регистрировать) регистрирует клиента
unregister (отменить регистрацию) отменяет регистрацию клиента
email (электронная почта) изменяет адрес электронной почты
show (показать) показывает клиентов
quit выход из программы
До того, как ознакомиться с исходным кодом, было бы неплохо запустить программу, зарегистрировать нового клиента, просмотреть список клиентов, изменить адрес электронной почты клиента, отменить регистрацию клиента и снова просмотреть список клиентов. Приведем пример выдачи программы:
Н> show // показать
id (-1 for all): -1 // идентификатор (-1 для всех):-1
1 Rocket Squirrel rocky@frosbitefalls.com
2 Bullwinkle Moose moose@wossamotta.edu
H> register // регистрировать
first name: Bob // имя: Боб
last name: Oberg // фамилия: Оберг
email address: oberg@objectinnovations.com // адрес электронной
// почты:
id = 3 // идентификатор = 3
H> email // электронная почта
customer id: 1 // идентификатор клиента
email address: rocky@objectinnovations.com // адрес электронной
// почты
Н> unregister
id: 2 // идентификатор: 2
Н> show // показать
id (-1 for all): -1 // идентификатор (-1 для всех)
1 Rocket Squirrel rocky@objectinnovations.com
3 Bob Oberg oberg@objectinnovations.com
Класс Customer (Клиент)
Все файлы с исходными кодами программы-примера находятся в папке Customer-Collection. В файле customer. h находится реализация классов Customer (Клиент) и Customers (Клиенты). Исходный код для класса Customer (Клиент) почти такой же, как приведенный ранее. Единственное добавление — специальный конструктор, инициализирующий объект Customer (Клиент) заданным идентификатором. Этот конструктор используется классом Customers (Клиенты) при удалении элемента (UnregisterCustomer) и при проверке того, присутствует ли в коллекции некоторый элемент (Checkld).
_gc class Customer
// сборщик мусора - класс Клиент
{
pmblic:
Customer(int id) // Клиент (int-идентификатор)
{
nCustomerld = id; // идентификатор
pFirstName = "";
pLastName = "";
pEmailAddress = "";
}
};
Класс Customers (Клиенты) содержит список клиентов, хранимый в ArrayList
{Список массивов).
_gc class Customers // сборщик мусора - класс Клиенты
{
private: // частный
ArrayList *pCustomers;
public:
Customers() // Клиенты
{
pCustomers = new ArrayList;
RegisterCustomer(
"Rocket", // Ракета "Squirrel",
// Белка "rocky@frosbitefalls.com");
RegisterCustomer( "Bullwinkle",
"Moose", // Американский лось "moose@wossamotta.edu");
}
int RegisterCustomer( String *pFirstName, String *pLastName, String *pEmailAddress)
{
Customer *pCust = new Customer( // Клиент
*pCust = новый Клиент
{
pFirstName, pLastName, pEmailAddress); pCustomers->Add(pCust); // Добавить return pCust->nCustomer!d; }
void UnregisterCustomer(int id) // идентификатор {
Customer *pCust = new Customer(id);
// Клиент *pCust = новый Клиент (идентификатор);
pCustomers->Remove(pCust);
}
void ChangeEmailAddress(int id, String *pEmailAddress)
// (int идентификатор, Строка *pEmailAddress)
{
lEnumerator *pEnum =
pCustomers-XSetEntimerator () ;
while (pEnum->MoveNext())
{
Customer *pCust = // Клиент
dynamic_cast<Customer *>(pEnum->Current); // Клиент
if (pCust->nCustomer!d == id)
// если (pCust-> nCustomerld == идентификатор)
{
pCust->pEmailAddress = pEmailAddress;
return;
}
}
String *pStr = String::Format( // Формат
"id {0} {!}", _box(id), S"not found");
// "идентификатор {0} {!}", (идентификатор),
// " не найден");
throw new Exception(pStr); // новое Исключение
}
void ShowCustomers(int id) // идентификатор
{
if ('Checkld(id) && id '= -1)
// если (! Checkld (идентификатор) && идентификатор! =-1)
return; lEnumerator *pEnum =
pCustomers-XSetEnumerator();
while (pEnum->MoveNext())
{
Customer *pCust = // Клиент
dynamic_cast<Customer *>(pEnum->Current); // Клиент
if (id == -1 || id == pCust->nCustomer!d)
// если (идентификатор == -1 ||
// идентификатор == pCust-> nCustomerld)
{
String *pSid =
pCust->nCustomerId.ToStnng() ->PadLeft (4) ;
String *pFirst =
pCust->pFirstName->PadRight(12) ;
String *pLast =
pCust->pLastName->PadRight(12);
String *pEmail =
pCust->pEmailAddress->PadRight(20) ;
Console::Write("{0} ", pSid); // Пульт:: Записать
Console::Write("{0} ", pFirst); // Пульт:: Записать
Console::Write("{0} ", pLast) ; // Пульт:: Записать
Console::WriteLine("{0}", pEmail); // Пульт:: Записать
}
}
}
bool Checkld(int id)
// логический{булев} Checkld (int идентификатор)
{
Customer *pCust = new Customer(id);
// Клиент *pCust = новый Клиент (идентификатор);
return pCustomers->Contains(pCust); // Содержит ?
}
};
Жирным шрифтом в листинге выделены строки, в которых используются особенности класса коллекции. Для перемещения по коллекции используется интерфейс lEnumerator. Он может быть использован в данном случае, так как ArrayList (Список массивов) поддерживает интерфейс lEnumerable. Этот интерфейс обеспечивает поддержку особой семантики С# — семантики foreach. Ключевое слово foreach не поддерживается в C++. Однако из приведенного примера видно, что в управляемом C++ для перебора элементов ArrayList (Список массивов) можно применять интерфейс lEnumerable. Интерфейсы, предназначенные для работы с коллекциями, включая lenumerable, будут рассмотрены в этой главе позже.
Методы Add (Добавить) и Remove (Удалить), как и следует предположить по их названиям, используются для добавления и удаления элементов коллекции, соответственно. Метод Remove (Удалить) просматривает коллекцию в поисках элемента, равного элементу, переданному методу в качестве аргумента, и удаляет найденный элемент. Равенство элементов устанавливается вызовом метода Equals (Равняется). Методу Remove (Удалить) в качестве аргумента передается элемент, создаваемый реализованным нами специальным конструктором, причем для создания элемента используется идентификатор. Поскольку мы подменили метод Equals (Равняется) так, что равенство элементов устанавливается только по атрибуту CustomerlD, этот конструктор имеет единственный аргумент — идентификатор клиента.
Метод Contains (Содержит), применяемый нами во вспомогательном методе Checkld, использует подмененный метод Equals (Равняется) аналогичным образом.
Благодаря использованию коллекций облегчается добавление и удаление элементов. При использовании с той же целью массивов вместо коллекций требуется написать немало кода, необходимого для вставки и перемещения элементов массива, а также для заполнения пустого места, остающегося после удаления элемента. Кроме того, коллекции не имеют определенного размера, и при необходимости могут быть динамически увеличены.

Интерфейсы


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

Основные сведения об интерфейсах


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

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


В Visual C++ .NET для определения интерфейсов используется ключевое слово _interface (интерфейс). Само же определение подобно определению класса. Так же, как и классы, интерфейсы являются ссылочными типами, а для определения их как управляемых используется ключевое слово _дс (сборщик мусора). Наибольшее отличие интерфейсов от классов (как управляемых, так и неуправляемых) — отсутствие конкретной реализации для интерфейсов; они представляют собой чистые спецификации. Тем не менее, подобно классам, интерфейсы могут иметь свойства (property), индексаторы (indexer) и, конечно же, методы.
На примере интерфейса ICustomer продемонстрируем определение методов, используемых клиентами системы Бюро путешествий Acme (Acme Travel Agency).
_gc _interface Icustomer
// сборщик мусора - ICustomer
{
public:
int RegisterCustomer;
String *pFirstName,
String *pLastName,
String *pEmailAddress) ;
void UnregisterCustomer(int id); // идентификатор
ArrayList *GetCustomer(int id); // идентификатор
void ChangeEmailAddress(int id, String *pEmailAddress) ;
// (int идентификатор, Строка *pEmailAddress);
};
Описания методов RegisterCustomer, UnregisterCustomer и ChangeEmailAd-dress полностью совпадают с сигнатурами одноименных методов, реализованных нами в классе Customer (Клиент). Метод GetCustomer является новым. Ранее мы использовали метод ShowCustomer, выводящий на экран список клиентов. Этот метод использовался временно. Однако лучше возвращать по запросу сами данные и предоставить пользователю самому решать, что с ними делать. Метод GetCustomer возвращает информацию об одном или нескольких клиентах в списке массивов. Когда в качестве идентификатора клиента задается значение -1, возвращается информация обо всех зарегистрированных клиентах. В противном случае возвращаемый список содержит информацию о клиенте с заданным идентификатором. Если среди клиентов нет клиента с таким идентификатором, возвращаемый список будет пустым.
Наследование для интерфейсов
Интерфейс может быть потомком другого интерфейса (однако управляемый интерфейс не может быть потомком неуправляемого, и наоборот). В отличие от классов, для которых допустимо лишь единичное наследование, допускается множественное наследование интерфейсов, т.е. интерфейс может иметь несколько непосредственных родителей. Например, интерфейс ICustomer может быть определен как производный от двух более мелких интерфейсов IBasicCustomer и ICustomerlnfo. Заметим, что в описании указанных трех интерфейсов не задается спецификатор открытого доступа. Это потому, что интерфейсы общедоступны по умолчанию.
_gc _interface IBasicCustomer
// сборщик мусора - IBasicCustomer
{
int RegisterCustomer(
String *pFirstName,
String *pLastName,
String *pEmailAddress) ;
void UnregisterCustomer(int id); // идентификатор
void ChangeEmailAddress(int id, String *pEmailAddress) ;
// (int идентификатор, Строка *pEmailAddress) ;
};
_gc _interface ICustomerlnfo
// сборщик мусора - ICustomerlnfo
{
ArrayList *GetCustomer(int id); // идентификатор
};
_gc _interface ICustomer : IBasicCustomer, ICustomerlnfo
// сборщик мусора - ICustomer: IBasicCustomer, ICustomerlnfo
{
};
При таком объявлении интерфейса можно также определить новые методы, как это сделано ниже для интерфейса ICustomer2.
_gc _interface ICustomer2 : IBasicCustomer, ICustomerlnfo
// сборщик мусора - ICustomer2: IBasicCustomer, ICustomerlnfo
{
void NewMethod();
};

Программирование с использованием интерфейсов


Использование интерфейсов облегчает программирование на управляемом C++. Интерфейсы реализуются через классы, и для получения указателя на интерфейс можно выполнить приведение указателя на класс. Методы интерфейсов можно вызывать, используя и указатели на класс, и указатели на интерфейс; однако для того, чтобы полностью воспользоваться достоинствами полиморфизма, предпочтительно везде, где только возможно, использовать указатели на интерфейсы.
Реализация интерфейсов
В C++ указание того, что класс реализует интерфейс, осуществляется с помощью двоеточия, используемого также для указания наследования класса. Управляемый класс может наследовать от одного управляемого класса и, кроме этого, от одного или нескольких управляемых интерфейсов. В этом случае базовый класс должен указываться в списке первым, сразу после двоеточия. Заметим, что, в отличие от управляемых интерфейсов, наследование управляемых классов может быть только общедоступным.
_gc class HotelBroker : public Broker, public IHotellnfo,
// класс сборщика мусора - HotelBroker: общедоступный Брокер,
public IHotelAdmin, public IHotelReservation
{
...
};
В этом примере класс HotelBroker является производным от класса Broker (Брокер) и реализует интерфейсы IHotellnfo, IHotelAdmin и IHotelReservation. В HotelBroker должны быть реализованы все методы этих интерфейсов, либо непосредственно, либо используя реализацию, унаследованную от базового класса Broker (Брокер).
Подробно пример использования интерфейсов будет рассмотрен в этой главе несколько позже, когда мы возьмемся за реализацию второго шага создаваемой системы.
А сейчас в качестве небольшого примера вышеизложенного, рассмотрим программу Smalllnterface. Класс Account (Счет) реализует интерфейс IBasicAccount. В описании этого интерфейса демонстрируется синтаксис объявления свойства интерфейса.
//Account.h
_gc _interface IBasicAccount
// сборщик мусора - IBasicAccount
{
void Deposit(Decimal amount); // Депозит (Десятичное
// количество);
void Withdraw(Decimal amount); // Снять (Десятичное
// количество);
_property Decimal get_Balance(); // Десятичное число };
_gc class Account : public IBasicAccount
// сборщик мусора - класс Счет: IBasicAccount
{
private: // частный
Decimal balance; // Десятичный баланс public:
Account(Decimal balance) // Счет (Десятичный баланс)
{
this->balance = balance; // баланс
}
void Deposit(Decimal amount) // Депозит (Десятичное количество)
{
balance = balance + amount; // баланс = баланс + количество
}
void Withdraw(Decimal amount) // Снять (Десятичное количество)
{
balance = balance - amount; // баланс = баланс - количество
}
_property Decimal get_Balance() // Десятичное число
{
return balance; // баланс
}
};
Использование интерфейсов
Когда известно, что некоторый класс поддерживает определенный интерфейс, его методы можно вызывать с помощью указателя на экземпляр класса. Если же неизвестно, реализован ли интерфейс классом, можно попытаться выполнить приведение указателя на класс к указателю на интерфейс. И если класс не поддерживает данный интерфейс, при такой попытке возникнет исключение. В следующем примере, взятом из файла Smalllnterf асе. h, демонстрируется именно такой способ проверки.
try // попытка
{
IBasicAccount *pifc2 =
dynamic_cast<IBasicAccount *>(pacc2);
pifc2->Deposit(25); // Депозит
Console::WriteLine(
"balance = {0}", _box(pifc2->Balance)); // Баланс
}
catch (NullReferenceException *pe)
{
Console::WriteLine(
"IBasicAccount is not supported"); // IBasicAccount
// не поддерживается
Console::WriteLine(pe->Message); // Сообщение
}
}
В программе Small Inter face используются два почти одинаковых класса. Класс Account (Счет) поддерживает интерфейс IBasicAccount, а второй класс, NoAccount его не поддерживает. Оба класса имеют идентичные наборы методов и свойств. Приведем полностью содержимое файлов Smalllnterf асе. срр и Smalllnterf асе. h. Заметим, что в этой программе делаются попытки привести указатели на экземпляры классов Account (Счет) и NoAccount к указателю на интерфейс IBasicAccount.
//Smalllnterfасе.срр
fusing <mscorlib.dll>
using namespace System;
// использование пространства имен Система;
#include "Account.h"
#include "NoAccount.h"
#include "Smalllnterface.h"
void main() // главный
{
Smalllnterface::Main(); // Главный
}
//Smalllnterface.h
_gc class Smalllnterface
// класс сборщика мусора Smalllnterface
{
public:
static void Main() // Главный
{
Account *pacc - new Account(100); // новый Счет
// Использовать ссылку на класс
Console::WriteLine(
"balance = {0}", _box(pacc->Balance)); // Баланс
pacc->Deposit(25); // Депозит
Console::WriteLine(
"balance = {0}", _box(pacc->Balance)); // Баланс
// Использовать ссылку на интерфейс
IBasicAccount *pifc =
dynamic_cast<IBasicAccount *>(pacc); pifc->Deposit(25); // Депозит
Console::WriteLine(
"balance = {0}", _box(pifc->Balance)); // Баланс
// Теперь попробовать с классом,
// не реализующим
IBasicAccount NoAccount *pacc2 = new NoAccount(500);
// Использовать ссылку на класс
Console::WriteLine(
"balance = {0}", _box(pacc2->Balance)); // Баланс
pacc2->Deposit(25); // Депозит
Console::WriteLine(
"balance = {0}", _box(pacc2->Balance)); // Баланс
// Испробовать указатель на интерфейс try
// попытка
{
IBasicAccount *piba=
dynamic_cast<IBasicAccount *>(pacc2);
piba->Deposit(25); // Депозит
Console::WriteLine(
"balance = {0}", _box(piba->Balance)); // Баланс
}
catch (NullReferenceException *pe)
{
Console::WriteLine(
"IBasicAccount is not supported"); // IBasicAccount
//не поддерживается
Console::WriteLine(pe->Message); // Сообщение
}
}
};
В приведенном примере сначала мы имеем дело с классом Account (Счет), который поддерживает интерфейс IBasicAccount. В этом случае попытки вызвать методы интерфейса как с помощью указателя на класс, так и указателя на интерфейс, полученного в результате приведения, заканчиваются успешно. Далее мы имеем дело с классом NoAccount. Несмотря на то, что набор методов этого класса идентичен набору методов класса Account (Счет), в его описании не указано, что он реализует интерфейс IBasicAccount.
//NoAccount.h
_gc class NoAccount
// класс сборщика мусора NoAccount
{
При запуске этой программы возникает исключение NullReferenceException. Это происходит при попытке использовать указатель на интерфейс IBasicAccount, полученный в результате динамического приведения указателя на класс NoAccount. (Иными словами, исключение возникает при попытке приведения типа NoAccount * к данным типа указателя на интерфейс IBasicAccount *.) Если бы мы использовали обычное приведение типа в стиле С, то при подобной попытке возникло бы исключение InvalidCastException. Однако уже при компиляции такой программы было бы выдано предупреждение, что использование приведения типов в стиле С не рекомендуется.
balance = 100
balance = 125
balance = 150
balance = 500
balance = 525
IBasicAccount is not supported
Value null was found where an instance of an object was
required.
Вот перевод выдачи:
баланс = 100
баланс =125
баланс = 150
баланс = 500
баланс = 525
IBasicAccount не поддерживается
Пустой указатель там, где требуется указатель на объект.

Динамическое использование интерфейсов


Полезной особенностью интерфейсов является возможность их использования в динамических сценариях, что позволяет по ходу выполнения программы проверять, поддерживается ли интерфейс определенным классом. Если интерфейс поддерживается, мы можем воспользоваться предоставляемыми им возможностями; в противном же случае программа может проигнорировать интерфейс. Фактически такое динамическое поведение реализуется с помощью обработки возникающих исключений, как это уже было продемонстрировано выше. Хотя подобный подход вполне работоспособен, однако он отличается некоторой неуклюжестью и приводит к появлению трудночитаемых программ. C++ поддерживает использование операторов dynamic_cast и typeid, а в .NET Framework есть класс Туре (Тип), облегчающий динамическое использование интерфейсов.
В качестве примера рассмотрим интерфейс ICustomer2, имеющий, по сравнению с интерфейсом ICustomer 1, дополнительный метод ShowCustomer.
_gc _interface ICustomer2 : ICustomerl
// сборщик мусора - ICustomer2: ICustomerl
{
public:
void ShowCustomers(int id); // идентификатор
};
Предположим, что класс Customerl поддерживает интерфейс ICustomerl, a класс Customer2 — интерфейс !Customer2. Для консольной программы-клиента удобнее использовать исходный метод ShowCustomer, а не метод Get-Customer, так как последний создает список массивов и копирует данные в него. Поэтому программа-клиент предпочтет работать с интерфейсом ICus-tomer2, если это возможно. В папке TestlnterfaceBeforeCast содержится программа, рассмотренная в следующем разделе.
Проверка поддержки интерфейса перед приведением типов
Проверку поддержки интерфейса можно производить, выполняя динамическое приведение типа указателя и обрабатывая исключение, которое может при этом возникнуть. Однако более изящным решением будет выполнять проверку до приведения типа, избегая при этом возникновения исключений. Если объект поддерживает необходимый интерфейс, можно выполнять приведение типа для получения доступа к интерфейсу. С# поддерживает использование удобного оператора is для проверки того, поддерживает ли объект определенный интерфейс. К сожалению, в C++ с этой целью приходится использовать метод отражения, реализуемый посредством методов GetType и Getlnterf асе. В связи с тем, что это приводит к появлению несколько громоздкого выражения, в следующем примере с помощью директивы #define определяется макрос IS (THIS, THAT_INTERFACE), используемый далее в двух условных операторах if.
//TestlnterfaceBeforeCast.срр
//MACRO: pObj->GetType()->GetInterface("Somelnterface")!=0
// МАКРОС
#define IS(THIS, THAT_INTERFACE) (THIS->GetType()->GetInterface(
THAT_INTERFACE)!=0)
#using <mscorlib.dll>
using namespace System;
// использование пространства имен Система;
_gc _interface ICustomer1 {};
// сборщик мусора - ICustomer1;
_gc _interface ICustomer2 : ICustomer1
// сборщик мусора - ICustomer2: ICustomer1
{
public:
void ShowCustomers(int id); // идентификатор
};
_gc class Customer! : public ICustomer1 {};
// класс сборщика мусора Customerl: ICustomerl {};
_gc class Customer2 : public ICustomer2
// класс сборщика мусора Customer2: ICustomer2
{
public:
void ShowCustomers(int id) // идентификатор
{
Console::WriteLine("Customer2::ShowCustomers:
succeeded");
}
};
void main(void) // главный
{
Customerl *pCustl = new Customerl; // не к ICustomer2
Console::WriteLine(pCustl->GetType());
// проверить, относится ли к типу ICustomer2 перед приведением
if (IS(pCustl, "ICustomer2"))
{
ICustomer2 *plcust2 =
dynamic_cast<ICustomer2 *>(pCustl);
plcust2->ShowCustomers(-1);
}
else
Console::WriteLine
("pCustl does not support ICustomer2 interface");
// ("pCustl не поддерживает интерфейс ICustomer2");
Customer2 *pCust2 = new Customer2; // да, на ICustomer2
Console::WriteLine(pCust2->GetType());
// проверить, относится ли к типу ICustomer2 перед приведением
if (IS(pCust2, "ICustomer2"))
{
ICustomer2 *plcust2 =
dynamic_cast<ICustomer2 *>(pCust2);
pIcust2->ShowCustomers(-1);
}
else
Console::WriteLine
("pCust2 does not support ICustomer2 interface");
// ("pCust2 не поддерживает интерфейс ICustomer2");
}
В этом примере продемонстрировано далеко не оптимальное решение, так как проверка типа производится дважды. Первый раз — при использовании отражения для проверки поддержки интерфейса в макросе IS. А еще раз проверка производится автоматически — при выполнении динамического приведения типа, в этом случае, если интерфейс не поддерживается, возникает исключение. Результат работы программы приведен ниже. Обратите внимание, что при выполнении программы действительно не возникает исключения.
Customer1
pCustl does not support ICustomer2 interface
Customer2
Customer2::ShowCustomers: succeeded
А вот и перевод:
Customer1
pCustl не поддерживает интерфейс ICustomer2
Customer2
Customer2:: ShowCustomers: успешно
Оператор dynamic_cast
Результатом выполнения оператора dynamic_cast является непосредственно указатель на интерфейс. В случае, если интерфейс не поддерживается, значение указателя устанавливается равным нулю. Используем этот факт для создания программы, в которой проверка поддержки интерфейса производится один раз. Приведенный ниже фрагмент взят из примера CastThenTestForNull, отличающегося от предыдущего, Testlnter-f aceBef oreCast, тем, что в нем производится проверка равенства нулю результата динамического приведения типа.
void main(void) // главный
{
Customerl *pCustl = new Customer!; // нет ICustomer2
Console::WriteLine(pCustl->GetType() ) ;
// Использовать оператор С ++ dynamic_cast, чтобы проверить
// наличие ICustomer2
ICustomer2 *plcust2 =
dynamic_cast<ICustomer2 *>(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<ICustomer2 *>(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)


Попытаемся применить полученные знания об интерфейсах для небольшой переделки программы Бюро путешествий 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


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

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