Глава 24
Создание компонентов
Глава 24. Создание компонентов
671 есмотря на то что язык C# можно использовать для написания приложений практически любого типа, одной из самых значительных областей его применения является создание компонентов. Компонентно-ориентированное программирование настолько существенно для C#, что его иногда называют компонентно-ориентированным
языком (component-oriented language). Поскольку C# и среда .NET Framework разрабатывались с ориентацией на компоненты, компонентная модель программирования здесь в значительной степени упрощена по сравнению с более ранними решениями.
Например, если термин компонент наводит вас на мысли о COM-компонентах (Component
Object Model — модель компонентных объектов) и связанных с этим проблемах, не беспокойтесь. C#-ориентированные компоненты создавать намного проще.
Что представляет собой компонент
Начнем с определения термина компонент. Компонент — это независимый модуль, предназначенный для многократного использования и предоставляемый пользователю в двоичном формате. Это определение описывает четыре ключевых характеристики компонента. Рассмотрим их по очереди.
Компонент определен как независимый модуль. Это означает, что каждый компонент вполне самодостаточен. Другими словами, компонент обеспечивает полный набор функций. Его внутренняя работа закрыта для “внешнего мира”, но при этом реализация может быть изменена без последствий для кода, в котором используется этот компонент,
Компонент предназначен для многократного применения. Это означает, что компонент может использовать любая программа, которой требуются его функции.
Программа, которая использует компонент, называется клиентом (client). Таким образом, компонент может работать с любым количеством клиентов.
Компонент представляет собой отдельный модуль. Это очень важно. С точки зрения клиента компонент выполняет конкретную функцию или набор функций. Функциями компонента, может воспользоваться любое приложение, но сам компонент не является автономной программой.
Наконец, компонент должен быть представлен в двоичном формате. Это принципиально важно. Хотя использовать компонент могут многие клиенты, они не имеют доступа к его исходному коду. Функциональность компонента открыта для клиентов только посредством его public
-членов. Другими словами, именно компонент управляет тем, какие функции оставлять открытыми для клиентов, а какие — держать “под замком”.
Компонентная модель
Несмотря на то что приведенное выше определение точно описывает программный компонент, для полного его понимания (и использования) этого недостаточно.
Принципиально важное значение здесь имеет модель, которая реализует компоненты. Для того чтобы клиент мог использовать компонент, необходимо, чтобы и клиент, и компонент использовали один и тот же набор правил. Набор правил, определяющих форму и поведение компонента, и называется компонентной моделью (component model). Именно компонентная модель определяет характер взаимодействия компонента и модели.
Компонентная модель важна хотя бы потому, что предназначенный для многократного использования компонент, предоставляемый пользователю в двоичном фор-
Н
672 Часть III. Применение языка C# мате может быть реализован различными способами, причем количество этих способов может быть очень большим. Например, существует множество разных способов передачи параметров и приема значений. Можно также по-разному выделять (и освобождать) память
(и другие системные ресурсы) для объектов. Поэтому для эффективного использования компонентов клиенты и сами компоненты должны подчиняться правилам, определенным компонентной моделью. По сути компонентная модель представляет своего рода контракт между клиентом и компонентом, который обе стороны согласны выполнять.
До создания C# и среды .NET Framework большинство компонентов были COM- компонентами. Модель COM была разработана для традиционной среды Windows и языка
C++. Поэтому она в принципе не в состоянии использовать преимущества новых методов управления памятью, которые реализованы в C# и .NET Framework. Как следствие, COM- контракт был довольно трудным для реализации и ненадежным. К счастью, C# и среда
.NET Framework лишены практически всех проблем своих предшественников. Поэтому, если у вас за плечами есть опыт работы с COM-моделью, вы будете приятно удивлены простотой создания компонентов в C#.
Что представляет собой C#-компонент Благодаря особенностям работы средств языка C#, любой его класс полностью соответствует общему определению компонента. Например, будучи скомпилированным, класс (в его двоичной форме) можно использовать в различных приложениях. Но значит ли это, что любой класс является компонентом? Ответ: нет. Для того чтобы класс стал компонентом, он должен следовать компонентной модели, определенной средой .NET
Framework. К счастью, этого совсем не трудно добиться: такой класс должен реализовать интерфейс
System.ComponentModel.IComponent
. При реализации интерфейса
IComponent компонент удовлетворяет набору правил, позволяющих компоненту быть совместимым со средой .NET Framework.
Несмотря на простоту реализации интерфейса
IComponent
, во многих ситуациях лучше использовать альтернативный вариант— класс
System.ComponentModel.Component
. Класс
Component обеспечивает стандартную реализацию интерфейса
IComponent
. Он также поддерживает другие полезные средства, свойственные компонентам. Опыт показывает, что большинству создателей компонентов удобнее выводить их из класса
Component
, чем самим реализовать интерфейс
IComponent
, поскольку в первом случае нужно попросту меньше программировать.
Следовательно, в C# на создание компонента не требуется "героических усилий" со стороны программиста.
Контейнеры и узлы С C#-компонентами тесно связаны две другие конструкции: контейнеры и узлы.
Контейнер — это группа компонентов. Контейнеры упрощают программы, в которых используется множество компонентов.
Узел позволяет связывать компоненты и контейнеры. Подробнее обе эти конструкции рассматриваются ниже.
Сравнение C#- и COM-компонентов C#-компоненты гораздо проще реализовать и использовать, чем COM-компоненты.
Те, кто знаком с моделью COM, знают, что при использовании COM-компонента необходимо выполнять подсчет ссылок (механизм учета клиентов данного объекта),
чтобы гарантировать, что компонент останется в памяти. При такой организации каждый раз, когда добавляется очередная ссылка на компонент, программа
Глава 24. Создание компонентов
673 должна вызывать метод
AddRef()
. При каждом удалении ссылки программа должна вызывать метод
Release()
. Но самое печальное здесь то, что такой подход чреват ошибки. К счастью, для C#-компонентов подсчета ссылок не требуется. Поскольку C# использует систему сбора мусора, компонент автоматически остается в памяти до тех пор, пока существует хотя бы одна ссылка на него.
Поскольку интерфейс
IComponent и класс
Component составляют сердцевину программирования компонентов, следующие разделы будут посвящены именно им.
Интерфейс IComponent
Интерфейс
IComponent определяет правило, которому должны следовать все компоненты. В интерфейсе
IComponent определено только одно свойство и одно событие. Вот как объявляется свойство site
:
ISite Site { get; set; }
Свойство
Site получает или устанавливает узел компонента. Узел идентифицирует компонент. Это свойство имеет null
-значение, если компонент не хранится в контейнере.
Событие, определенное в интерфейсе
IComponent
, носит имя
Disposed и объявляется так: event EventHandler Disposed
Клиент, которому нужно получить уведомление при разрушении компонента, регистрирует обработчик событий посредством события
Disposed
Интерфейс
IComponent также наследует интерфейс
System.IDisposable
, в котором определен метод
Dispose()
: void Dispose()
Этот метод освобождает ресурсы, используемые объектом.
Класс Component
Несмотря на то что для создания компонента достаточно реализовать интерфейс
IComponent
, намного проще создать класс, производный от класса
Component
, поскольку он реализует интерфейс
IComponent по умолчанию. Именно этот вариант построения компонентов и используется в примерах этой главы. Если класс наследует класс
Component
, значит, он автоматически выполняет правила, необходимые для получения
.NET-совместимого компонента.
В классе
Component определен только конструктор по умолчанию. Обычно программисты не создают объект класса
Component напрямую, поскольку основное назначение этого класса — быть базовым для создаваемых компонентов.
В классе
Component определено два открытых свойства. Объявление первого из них, свойства
Container
, такое: public IContainer Container { get; }
Свойство
Container возвращает контейнер, который содержит вызывающий компонент. Если компонента нет в контейнере, возвращается значение null
. Следует помнить, что свойство
Container устанавливается не компонентом, а контейнером.
Второе свойство,
Site
, определено в интерфейсе
IComponent
. В классе
Component оно реализовано как виртуальное: public virtual ISite Site {get; set; }
674
Часть III. Применение языка C#
Свойство
Site возвращает или устанавливает объект типа
ISite
, связанный с компонентом. Оно возвращает значение null
, если компонента в контейнере нет. Свойство
Site устанавливается не компонентом, а контейнером.
В классе Component определено два открытых метода. Первый представляет собой переопределение метода
ToString()
. Второй, метод
Dispose()
, используется в двух формах. Первая из них такова: public void Dispose()
Метод
Dispose()
, вызванный в первой форме, освобождает любые ресурсы, используемые вызывающим компонентом. Этот метод реализует метод
Dispose()
, определенный в интерфейсе
IDisposable
. Для освобождения компонента и занимаемых им ресурсов клиент вызывает именно эту версию метода
Dispose()
Вот как выглядит вторая форма метода
Dispose()
: protected virtual public void Dispose(bool
how
)
Если параметр
how
имеет значение true
, эта версия метода освобождает как управляемые, так и неуправляемые ресурсы, используемые вызывающим объектом. Если
how
равно значению false
, освобождаются только неуправляемые ресурсы. Поскольку эта версия метода
Dispose()
защищена (
protected
), ее нельзя вызвать из кода клиента.
Поэтому клиент использует первую версию. Другими словами, вызов первой версии метода
Dispose()
генерирует обращение к методу
Dispose(bool)
В общем случае компонент, в котором больше нет необходимости, переопределяет версию
Dispose(bool)
, если он содержит ресурсы, которые нужно освободить. Если компонент не занимает никаких ресурсов, то для его освобождения достаточно стандартной реализации метода
Dispose(bool)
, определенной в классе
Component
Класс Component наследует класс
MarshalByRefObject
, который используется в том случае, когда компонент создается вне локальной среды, например в другом процессе или на другом компьютере, связанном с первым по сети. Для обмена данными
(аргументами методов и возвращаемыми значениями) должен существовать механизм, который определит способ пересылки данных. По умолчанию принимается, что информация должна передаваться по значению, но при унаследовании класса
MarshalByRefObject данные будут передаваться по ссылке. Таким образом, C#- компонент обеспечивает передачу данных по ссылке.
Простой компонент
Итак, настало время от теории перейти к практике и рассмотреть первый пример создания компонента
CipherComp
, который реализует очень простую стратегию шифрования. Ее суть состоит в том, что каждый символ шифруется путем добавления к его коду единицы. Дешифрирование заключается в вычитании единицы. Чтобы зашифровать таким способом строку, достаточно вызвать метод
Encode()
, передав незашифрованный текст в качестве аргумента. Чтобы дешифровать зашифрованную строку, вызовите метод
Decode()
, передав ему как аргумент зашифрованный текст. В обоих случаях возвращаются строки, содержащие результат (шифрования или дешифрирования).
// Простой компонент-шифратор.
// Назовите этот файл CipherLib.cs. using System.ComponentModel; namespace CipherLib { // Помещаем компонент в его же
// пространство имен.
Глава 24. Создание компонентов
675
//
Обратите внимание на то, что класс CipherComp
// наследует класс Component. public class CipherComp : Component {
//
Шифруем строку. public string Encode(string msg) { string temp = ""; for(int i=0; i < msg.Length; i++) temp += (char) (msg[i] + 1); return temp;
}
//
Дешифруем строку. public string Decode(string msg) { string temp = ""; for(int i=0; i < msg.Length; i++) temp += (char) (msg[i] - 1); return temp;
}
}
}
Итак, рассмотрим этот код подробнее. Прежде всего, как предлагается в комментарии, назовем этот файл
CipherLib.cs
. Это упростит использование рассматриваемого компонента, если вы работаете в среде разработки Visual Studio IDE.
Затем обратите внимание на включение пространства имен
System.ComponentModel
Как упоминалось выше, оно как раз и предназначено для поддержки программирования компонентов.
Класс
CipherComp создается в собственном пространстве имен
CipherLib
. Это позволяет защитить глобальное пространство имен от загромождения новыми именами.
Несмотря на то что такой подход формально необязателен, предложенный стиль программирования приветствуется.
Класс
CipherComp наследует класс
Component
. Это означает, что класс
CipherComp удовлетворяет всем требованиям для того, чтобы быть .NET-совместимым компонентом. Поскольку класс
CipherComp очень простой, ему не нужно обеспечивать выполнение специфических для компонентов функций. Его действия можно определить как тривиальные.
Обратите также внимание на то, что класс
CipherComp не распределяет системных ресурсов. Другими словами, он не хранит ссылок на другие объекты. Он просто определяет два метода
Encode()
и
Decode()
. А поскольку в классе
CipherComp ссылки не хранятся, ему не нужно реализовать метод
Dispose(bool)
. Безусловно, оба метода
Encode()
и
Decode()
возвращают ссылки на строки, но эти ссылки принадлежат вызывающему коду, а не объекту класса
CipherComp
Компиляция компонента CipherLib
Компонент должен быть скомпилирован с получением dll
-, а не exe
-файла. Если вы работаете в среде разработки Visual Studio IDE, то для построения компонента
CipherLib необходимо создать проект библиотеки классов (Class Library project). Если вы предпочитаете работать с компилятором командной строки, задайте опцию
676
Часть III. Применение языка C#
/t:library
. Например, чтобы скомпилировать компонент
CipherLib
, используйте следующую командную строку: csc /t:library CipherLib.es
В результате выполнения этой команды будет создан файл
CipherLib.dll
, содержащий компонент
CipherComp
Клиент, использующий компонент CipherComp
После создания компонент “готов к употреблению”. Например, следующая программа является клиентом компонента
CipherComp
, который она использует для шифрования и дешифрирования символьной строки.
// Клиент, который использует компонент CipherComp. using System; using CipherLib; // Импортируем пространство имен
// компонента CipherComp.. class CipherCompClient { public static void Main() {
CipherComp cc = new CipherComp(); string text = "Это простой тест"; string ciphertext = cc.Encode(text);
Console.WriteLine(ciphertext); string plaintext = cc.Decode(ciphertext);
Console.WriteLine(plaintext); cc.Dispose();
//
Освобождаем ресурсы.
}
}
Заметьте, что клиент включает пространство имен компонента
CipherLib
Благодаря этому компонент
CipherComp попадает в “поле зрения” клиента. Можно было бы полностью определять каждую ссылку на компонент
CipherComp
, но включение его пространства имен упрощает работу с компонентом. Во всем остальном компонент
CipherComp используется подобно любому другому классу.
Обратите внимание на обращение к методу
Dispose()
в конце программы. Как разъяснялось выше, посредством вызова метода
Dispose()
клиент освобождает ресурсы, которые использовал компонент. Компоненты, подобно другим объектам C#, используют один и тот же механизм сбора мусора, который выполняется спорадически. Однако вызов метода
Dispose()
заставляет компонент немедленно освободить свои ресурсы. Это очень важно в некоторых ситуациях, например, когда компонент удерживает такой ограниченный ресурс, как подключение к сети. Поскольку компонент
CipherComp не занимает ресурсы, вызов метода
Dispose()
в этом примере не актуален. Но так как метод
Dispose()
является частью контракта компонентной модели, имеет смысл всегда вызывать его при завершении работы с компонентом.
Чтобы скомпилировать программу-клиента, необходимо сообщить компилятору о том, что он должен обратиться к компоненту
CipherLib.dll
. Для этого используется опция
/r
. Например, нашу программу client можно скомпилировать с помощью следующей командной строки: csc /r:CipherLib.dll client.es
Глава 24. Создание компонентов
677 При использовании среды разработки Visual Studio IDE необходимо для программы- клиента добавить компонент
CipherLib.dll как ссылку.
При выполнении программы client получаем следующие результаты:
Юуп!рсптупк!ужту
Это простой тест
Переопределение метода Dispose() Представленная в предыдущем разделе версия простейшего компонента
CipherComp не занимает системных ресурсов, не создает и не хранит объектов. Поэтому у нас не было серьезных оснований для переопределения метода
Dispose(bool)
. Но если компонент использует системные ресурсы, метод
Dispose(bool)
нужно переопределять, чтобы эти ресурсы можно было освободить “в принудительном порядке”. К счастью, сделать это совсем нетрудно.
Но сначала попробуем разобраться в том, почему иногда необходимо заставить компонент освободить занимаемые им ресурсы, а не полагаться на системный механизм сбора мусора. Как разъяснялось выше в этой книге, процесс сбора мусора — недетерминированное действие. Оно выполняется по системной необходимости, а не потому, что какие-то объекты уже не нужны. Следовательно, если компонент удерживает некоторый ресурс, например, открытый файл, который нужно освободить, чтобы им могла воспользоваться другая программа, должен существовать способ принудительного освобождения ресурса, приводимого в действие, когда клиент завершит использование компонента. Одно лишь удаление ссылок на компонент не решает проблему, поскольку сам компонент будет удерживать ссылку на свой ресурс до тех пор, пока не наступит следующий сеанс сбора мусора. Выход из этой ситуации есть: компонент должен реализовать метод
Dispose(bool)
Для переопределения метода
Dispose(bool)
необходимо следовать следующим правилам.
1. При вызове метода
Dispose(bool)
с аргументом true переопределяемая версия метода должна освобождать все связанные с компонентом ресурсы — как управляемые, так и неуправляемые. При вызове этого метода с аргументом false переопределяемая версия метода должна освобождать только неуправляемые ресурсы, если таковые имеются.
2. Метод
Dispose(bool)
должен вызываться многократно, не создавая при этом никаких проблем.
3. Метод
Dispose(bool)
должен вызывать метод
Dispose(bool)
, реализованный в базовом классе.
4. Деструктор,
созданный для компонента, должен вызывать метод
Dispose(false)
Чтобы выполнить правило 2, компонент должен отслеживать момент своего освобождения. Обычно это реализуется поддержкой private
-поля, которое служит индикатором состояния.
Рассмотрим схематично представленный компонент, который реализует метод
Dispose(bool)
// Схематичная реализация компонента,
// который использует метод Dispose(bool). class MyComp : Component { bool isDisposed; // true, если компонент освобождается public MyComp {
678
Часть III. Применение языка C# isDisposed = false; // ...
}
MyComp()
{
Dispose(false);
} protected override void Dispose(bool dispAll) { if(! isDisposed) { if(dispAll)
{
// Освобождаем управляемые ресурсы. isDisposed
= true;
//
Устанавливаем компонент
//в состояние освобождения.
}
//
Освобождаем неуправляемые ресурсы. base.Dispose(dispAll);
}
}
}
При вызове метода
Dispose()
для конкретного экземпляра компонента автоматически вызывается метод
Dispose(bool)
, чтобы освободить любые ресурсы, которые он занимает.
Демонстрация использования метода Dispose(bool)
Чтобы проиллюстрировать использование метода
Dispose(bool)
, усовершенствуем компонент
CipherComp так, чтобы в нем велся журнал регистрации всех операций шифрования. Поэтому позаботимся о том, чтобы компонент записывал в файл результат каждого вызова метода
Encode()
или
Decode()
. Эти дополнительные действия будут незаметны для пользователя компонента
CipherComp
. На метод
Dispose(bool)
возлагается обязанность закрыть файл, когда этот компонент больше не нужен. Чтобы показать, когда и как вызывается метод
Dispose(bool)
, используются обращения к методу
WriteLine()
// Улучшенный вариант компонента шифрования, в котором
// поддерживается Файловый журнал регистрации всех операций. using System; using System.ComponentModel; using System.IO; namespace CipherLib {
//
Компонент шифрования, который поддерживает
// журнал регистрации. public class CipherComp : Component { static int useID = 0; int id; // Идентификационный номер экземпляра. bool isDisposed; // true, если компонент освобождается
FileStream log;
//
Конструктор public CipherComp() { isDisposed = false; // Компонент не освобождается. try
{
Глава 24. Создание компонентов
679 log = new FileStream("CipherLog" + useID,
FileMode.Create); id
= useID; useID++;
} catch(FileNotFoundException exc) {
Console.WriteLine(exc); log
= null;
}
}
//
Деструктор
CipherComp()
{
Console.WriteLine("Деструктор для компонента " + id);
Dispose(false);
}
//
Шифруем строку. Метод возвращает и
// сохраняет результат. public string Encode(string msg) { string temp = ""; for(int i=0; i < msg. Length; i++) temp += (char) (msg[i] + 1);
//
Сохраняем результат шифрования в файле. for(int i=0; i < temp.Length; i++) log.WriteByte((byte) temp[i]); return temp;
}
//
Дешифруем строку. Метод возвращает и
// сохраняет результат. public string Decode(string msg) { string temp = ""; for(int i=0; i < msg.Length; i++) temp += (char) (msg[i] -1);
//
Сохраняем результат дешифрирования в файле. for(int i=0; i < temp.Length; i++) log.WriteByte((byte) temp[i]); return temp;
} protected override void Dispose(bool dispAll) {
Console.WriteLine("Dispose(" + dispAll +
") для компонента " + id); if(isDisposed)
{ if(dispAll)
{
Console.WriteLine("Закрытие файла для " +
"компонента " + id); log.Close();
//
Закрываем файл.
680
Часть III. Применение языка C# isDisposed
= true;
}
// Неуправляемые ресурсы не нужно освобождать. base.Dispose(dispAll);
}
}
}
}
Рассмотрим подробнее эту версию компонента
CipherComp
. Его код начинается с таких строк: static int useID = 0; int id; // Идентификационный номер экземпляра. bool isDisposed; // true, если компонент освобождается
FileStream log;
Первое int
-поле, которое определено как статическое (static), предназначено для идентификации каждого экземпляра компонента
CipherComp
. Значение поля useID
должно включаться в имя файла регистрации, чтобы каждый экземпляр компонента
CipherComp имел собственный файловый журнал.
Поле id содержит идентификационный номер компонента, который совпадает со значением поля useID
в момент создания компонента, Поле isDisposed означает, освобожден ли компонент.
Четвертое поле (
log
) представляет собой ссылку типа
FileStream
, которая будет указывать на соответствующий файл регистрации.
Теперь рассмотрим конструктор класса
CipherComp
: public CipherComp() { isDisposed = false; // Компонент не освобождается. try
{ log = new FileStream("CipherLog" + useID,
FileMode.Create); id = useID; useID++;
} catch {FileNotFoundException exc) {
Console.WriteLine(exc); log = null;
}
}
В этом конструкторе переменная isDisposed инициализируется значением false
, значит, объект компонента
CipherComp используется клиентом. Затем открывается файл регистрации. Обратите внимание на то, что имя открываемого файла представляет собой конкатенацию имени “
CipherLog
” и строкового представления значения useID
. После этого значение useID
присваивается переменной id и инкрементируется. Таким образом, каждый экземпляр компонента
CipherComp будет обеспечен отдельным файлом регистрации, а каждый компонент — уникальным идентификационным номером (
ID
).
Здесь важно отметить, что при создании объекта типа
CipherComp открывается файл, который является системным ресурсом и который необходимо освободить после того, как соответствующий компонент перестанет быть нужным. Однако клиент не в состоянии напрямую освободить этот файл (клиент даже не “знает” о том, что файл был открыт).
Следовательно, закрыть файл должен метод
Dispose(bool)
Метод
Encode()
шифрует принятый строковый аргумент и возвращает результат, предварительно записав его в файл регистрации. Поскольку log
-файл остается открытым, при повторных обращениях к методу
Encode() в него будут добавляться очередные зашифрованные строки. Например, если использовать метод
Encode() для шифрования трех различных строк, файл регистрации будет содержать все три зашифрованные
Глава 24. Создание компонентов
681 строки. Метод
Decode()
работает подобным образом, за исключением того, что дешифрует значение своего аргумента.
Теперь рассмотрим метод
Dispose(bool)
, переопределенный компонентом
CipherComp
. Для удобства приведем его код здесь еще раз: protected override void Dispose(bool dispAll) {
Console.WriteLine("Dispose(" + dispAll +
") для компонента " + id); if(!isDisposed) { if(dispAll)
{
Console.WriteLine("Закрытие файла для " +
"компонента " + id); log.Close();
//
Закрываем файл. isDisposed = true;
}
//
Неуправляемые ресурсы не нужно освобождать. base.Dispose(dispAll);
}
}
Обратите внимание на то, что
Dispose(bool)
определен как protected
-метод.
Это означает, что его нельзя вызвать из кода клиента. Вместо него в программе клиента вызывается общедоступный метод,
Dispose()
, который реализован в классе
Component
В методе
Dispose(bool)
проверяется значение переменной isDisposed
. Если объект уже освобожден, никакие действия не выполняются. Если переменная isDisposed содержит значение false
, проверяется значение параметра dispAll
. Если оно равно значению true
, файл регистрации закрывается, и переменная isDisposed устанавливается равной значению true
. Вспомните: если параметр dispAll равен true
, все ресурсы должны быть освобождены. Если параметр dispAll равен false
, освобождению подлежат только неуправляемые ресурсы (которые в данном случае не задействованы). Наконец, вызывается метод
Dispose(bool)
, реализованный базовым классом (в данном случае им является класс
Component
). Это гарантирует освобождение любых ресурсов, используемых базовым классом. Обращения к методу
WriteLine()
здесь используются только в целях иллюстрации, а в реальных приложениях обычно обходятся без них.
Теперь рассмотрим деструктор компонента CipherComp:
CipherComp() {
Console.WriteLine("Деструктор для компонента "
+ id);
Dispose(false);
}
Деструктор просто вызывает метод
Dispose(bool)
с аргументом false
. И это вполне понятно: при выполнении деструктора для компонента предполагается, что этот компонент должен быть утилизирован системой сбора мусора. В данном случае автоматически будут освобождены все управляемые ресурсы. Деструктору остается лишь позаботиться об освобождении неуправляемых ресурсов. Обращение к методу
WriteLine()
здесь используется только в целях иллюстрации, а в реальных приложениях обычно обходятся без него.
Поскольку изменения, внесенные в код компонента
CipherComp
, не коснулись его интерфейса, мы можем использовать прежний код клиента. Но здесь для примера взята программа-клиент, в которой шифруются и дешифруются две строки:
// Еще один вариант программы-клиента, в которой
// используется компонент CipherComp.
682
Часть III. Применение языка C# using System; using CipherLib; // Импортируем пространство имен
// компонента CipherComp. class CipherCompClient { public static void Main() {
CipherComp cc = new CipherComp(); string text = "Тестирование"; string ciphertext = cc.Encode(text);
Console.WriteLine(ciphertext); string plaintext = cc.Decode(ciphertext);
Console.WriteLine(plaintext); text = "Компоненты - мощное средство языка C#."; ciphertext = cc.Encode(text);
Console.WriteLine(ciphertext); plaintext = cc.Decode(ciphertext);
Console.WriteLine(plaintext); cc.Dispose();
//
Освобождаем ресурсы.
}
}
Результаты выполнения этой программы выглядят так:
Ужтуйспгбойж Тестирование
Лпнрпожоуь!.!нпъопж!тсжетугп!?иьлб!D$/
Компоненты - мощное средство языка C#.
Dispose(True) для компонента 0
Закрытие файла для компонента 0
После выполнения этой программы файл регистрации
CipherLog0
будет иметь следующее содержимое:
УжтуйспгбойжТестированиеЛпнрпожоуь!.!нпъопж!тсжетугп!?иьлб!D$/Комп оненты - мощное средство языка C#.
Эта абракадабра — результат конкатенации двух строк (вернее, их зашифрованных и дешифрованных вариантов).
Обратите внимание на то, что в результатах выполнения программы-клиента метод
Dispose(bool)
вызван с аргументом true
. Дело в том, что программа вызывает метод
Dispose()
для объекта компонента
CipherComp
. Как разъяснялось выше, метод
Dispose()
затем вызывает метод
Dispose(bool)
с аргументом true
, что означает гарантированное освобождение всех ресурсов. В качестве эксперимента закомментируйте обращение к методу
Dispose()
в программе-клиенте, а затем скомпилируйте ее и выполните снова. Вы должны получить такие результаты:
Ужтуйспгбойж
Тестирование
Лпнрпожоуь!.!нпъопж!тсжетугп!?иьлб!D$/
Компоненты - мощное средство языка C#.
Деструктор для компонента 0
Dispose(False) для компонента 0
Dispose(False) для компонента 0
Глава 24. Создание компонентов
683
Поскольку обращение к методу
Dispose()
отсутствовало, компонент
CipherComp не был освобожден в явном виде. Но после выполнения программы он, безусловно, был разрушен. Следовательно, при разрушении был вызван его деструктор, что подтверждают результаты выполнения последнего варианта программы, который, в свою очередь, вызвал метод
Dispose(bool)
с аргументом false
. Второй вызов метода
Dispose(bool)
— это следствие вызова принадлежащей базовому классу версии метода
Dispose()
из метода
Dispose(bool)
, определенного в компоненте
CipherComp
. Поэтому метод
Dispose(bool)
был вызван во второй раз. В данном случае это излишне, но, поскольку метод
Dispose()
не может “знать”, как он был вызван, такое излишество неизбежно, но совершенно безвредно.
Аналогичный подход к реализации метода
Dispose(bool)
можно использовать при создании любого компонента.
Защита освобожденного компонента от использования
Несмотря на то что компонент
CipherComp надлежащим образом “убирает за собой”, его все же нельзя назвать безукоризненным. Дело в том, что в нем не предусмотрен способ предотвратить попытку клиента использовать уже разрушенный компонент.
Например, ничто не может помешать клиенту освободить компонент
CipherComp
, а затем попытаться вызвать для него метод
Encode()
. К счастью, эту проблему легко устранить: достаточно позаботиться о том, чтобы при использовании компонент проверял поле isDisposed
. Например, рассмотрим более удачный вариант методов
Encode()
и
Decode()
:
// Метод шифрования строки. Возвращает результат и
// сохраняет его в файле. public string Encode(string rasg) {
//
Предотвращаем использование освобожденного компонента. if(isDisposed)
{
Console.WriteLine("Ошибка: компонент уже разрушен."); return null;
} string temp = ""; for(int i=0; i < msg.Length; i++) temp += (char) (msg[i] + 1);
//
Сохраняем результат в файле. for(int i=0; i < temp.Length; i++) log.WriteByte((byte) temp[i]); return temp;
}
// Метод дешифрирования строки. Возвращает результат и
// сохраняет его в файле. public string Decode(string msg) {
//
Предотвращаем использование освобожденного компонента. if(isDisposed) {
Console.WriteLine("Ошибка: компонент уже разрушен."); return null;
}
684
Часть III. Применение языка C# string temp = ""; for(int i=>0; i < msg.Length; i++) temp += (char) (msg[i] - 1);
//
Сохраняем результат в файле. for(int i=0; i < temp.Length; i++) log.WriteByte((byte) temp[i]); return temp;
}
В обоих методах проверяется значение поля isDisposed
, и если оно равно true
, отображается сообщение об ошибке и никакие действия не выполняются. Но в реальных приложениях при попытке использовать уже разрушенный компонент обычно генерируется исключение.
Использование инструкции using
Как разъяснялось в главе 18, инструкцию using можно использовать для автоматического освобождения объекта. В этом случае метод
Dispose()
уже не нужно вызывать в явном виде. Вспомним, что инструкция using используется в следующих форматах: using(
obj
) {
//
Использование объекта
obj
} using(type
obj
= initializer) {
//
Использование объекта
obj
}
Здесь элемент
obj
представляет объект, используемый внутри блока using
. В первой форме этот объект объявляется вне using
-инструкции, а во второй — внутри. При завершении блока для объекта
obj
автоматически вызывается метод
Dispose()
(определенный в интерфейсе
System.IDisposable
). Инструкция using применяется только к объектам, которые реализуют интерфейс
System.IDisposable
(что, безусловно, относится ко всем компонентам).
Рассмотрим программу-клиент, которая использует компонент
CipherComp и для его освобождения вместо прямого обращения к методу
Dispose()
применяет инструкцию using
:
// Использование инструкции using. using System; using CipherLib; // Импортируем пространство имен
// компонента CipherComp. class CipherCompClient { public static void Main() {
//
Объект ее разрушится по завершении этого блока. using(CipherComp cc = new CipherComp()) { string text = "Инструкция using.";
Глава 24. Создание компонентов
685 string ciphertext = cc.Encode(text);
Console.WriteLine(ciphertext); string plaintext = cc.Decode(ciphertext);
Console.WriteLine(plaintext);
}
}
}
Результаты выполнения этой программы таковы:
Йотусфлчй?!vtjoh/
Инструкция using.
Dispose(True) для компонента 0
Закрытие файла для компонента 0
Как подтверждают результаты выполнения этой программы-клиента, метод
Dispose()
был вызван автоматически по завершении программного блока. Итак, решение остается за вами: использовать инструкцию using или явно вызывать метод
Dispose()
, но вы должны знать, что инструкция using упрощает код.
Контейнеры
Используя компоненты, иногда стоит сохранять их в контейнере. Как разъяснялось выше, контейнер определяет группу компонентов. Основное достоинство контейнера состоит в том, что он позволяет управлять целой группой компонентов. Например, вызвав метод
Dispose() для контейнера, можно освободить сразу все содержащиеся в нем компоненты. В общем случае контейнеры значительно упрощают работу с множеством компонентов.
Чтобы создать контейнер, необходимо создать объект класса
Container
, который определен в пространстве имен
System.ComponentModel
. В классе
Container определен такой конструктор: public Container()
Он создает пустой контейнер.
В уже созданный объект типа
Container можно добавлять компоненты с помощью метода
Add()
, который используется в двух форматах: public virtual void Add(IComponent
comp
) public virtual void Add(IComponent
comp
, string
compName
)
Первый формат позволяет добавить в контейнер компонент, заданный параметром
comp
. Второй формат не только добавляет в контейнер компонент, заданный параметром
comp
, но и присваивает ему имя, заданное параметром
compName
. Это имя должно быть уникальным с учетом того, что различия между прописными и строчными буквами здесь игнорируются. При попытке использовать имя, которое уже принадлежит компоненту в контейнере, генерируется исключение типа
ArgumentException
. Имя компонента можно получить с помощью свойства
Site
Чтобы удалить компонент из контейнера, используйте метод
Remove()
: public virtual void Remove(IComponent
comp
)
Предполагается, что этот метод успешно выполнится в любом случае: либо когда он действительно удалит компонент, заданный параметром
comp
, либо когда подлежащий удалению компонент в контейнере отсутствует. Другими словами, после вызова метода
Remove()
компонента
comp
в контейнере не будет.
686 Часть III. Применение языка C#
Класс
Container реализует метод
Dispose()
. При вызове для контейнера метода
Dispose()
для всех компонентов, хранимых в этом контейнере, будут вызваны их методы
Dispose()
. Таким образом, освободить все компоненты, содержащиеся в контейнере, можно одним-единственным вызовом метода
Dispose()
В классе
Container определено одно свойство
Components
: public virtual ComponentCollection Components { get; }
Это свойство получает коллекцию компонентов, которые хранятся в вызывающем контейнере.
Вспомните, что в классе
Component определены свойства
Container и
Site
, которые включаются во все производные компоненты. При сохранении компонента в контейнере свойства
Container и
Site
(относящиеся к объекту компонента) устанавливаются автоматически.
Свойство Container
называет контейнер, в котором содержится компонент. Свойство
Site позволяет получить такие данные о компоненте, как его имя, имя его контейнера и признак режима разработки. Свойство
Site возвращает ссылку типа
ISite
. Интерфейс
ISite определяет следующие свойства:
Свойство Описание IComponent Component { get; }
Получает ссылку на компонент
IContainer Container { get; }
Получает ссылку на контейнер bool DesignMode { get; }
Возвращает значение true
, если компонент используется в режиме разработки string Name { get; set; }
Получает или устанавливает имя компонента
Эти свойства можно использовать для получения информации о контейнере или компоненте во время выполнения программы.
Использование контейнера В следующей программе контейнер используется для хранения двух компонентов типа
CipherComp
. Первый добавляется в контейнер без задания имени, а второй — с именем “Второй компонент”. После выполнения операций над обоими компонентами с помощью свойства
Site на экране отображается имя второго компонента. Наконец, для контейнера вызывается метод
Dispose()
, который освобождает оба компонента.
(Конечно же, здесь было бы уместно применить инструкцию using, но в демонстрационных целях показан явный вызов метода
Dispose()
.)
// демонстрация использования контейнера
// для хранения компонентов. using System; using System.ComponentModel; using CipherLib; // Импортируем пространство имен
// компонента CipherComp. class UseContainer { public static void Main(string[] args) { string str = "Использование контейнеров.";
Container cont = new Container();
CipherComp cc = new CipherComp();
CipherComp cc2 = new CipherComp();
Глава 24. Создание компонентов
687 cont.Add(cc); cont.Add(cc2,
"Второй компонент");
Console.WriteLine("Первое сообщение: " + str); str = cc.Encode(str);
Console.WriteLine(
"Первое сообщение в зашифрованном виде: " + str); str = cc.Decode(str);
Console.WriteLine(
"Первое сообщение в дешифрованном виде: " + str); str = "один, два, три";
Console.WriteLine("Второе сообщение: " + str); str = cc2.Encode(str);
Console.WriteLine(
"Второе сообщение в зашифрованном виде: " + str); str = cc2.Decode(str);
Console.WriteLine(
"Второе сообщение в дешифрованном виде: " + str);
Console.WriteLine("\nИмя объекта cc2: " + cc2.Site.Name);
Console.WriteLine();
//
Освобождаем оба компонента. cont.Dispose();
}
}
Результаты выполнения этой программы клиента таковы:
Первое сообщение: Использование контейнеров.
Первое сообщение в зашифрованном виде: Йтрпмэипгбойж!лпоужкожспг/
Первое сообщение в дешифрованном виде: Использование контейнеров.
Второе сообщение: один, два, три
Второе сообщение в зашифрованном виде: пейо-!егб-!усй
Второе сообщение в дешифрованном виде: один, два, три
Имя объекта cc2: Второй компонент
Dispose(True) для компонента 1 Закрытие файла для компонента 1
Dispose(True) для компонента 0 Закрытие файла для компонента 0
Как вы убедились, при вызове метода
Dispose()
для контейнера все хранимые в нем компоненты освобождаются. В этом и состоит основное достоинство работы с несколькими компонентами или экземплярами компонентов.
688
Часть III. Применение языка C#
Компоненты - это будущее программирования
Организация приложения в виде набора компонентов — это мощное средство программирования, позволяющее программисту справляться со все более сложными задачами. Программисты в начале своей деятельности замечают, что чем больше программа, тем дольше период ее отладки. С увеличением размера программы обычно растет и ее сложность, но известно, что существует некоторый предел сложности, с которым может справиться человек. С точки зрения чистой комбинаторики, чем больше в программе отдельных строк, тем больше шансов получить побочные эффекты и нежелательные взаимосвязи.
Программные компоненты помогают справиться со сложностью программ по принципу “разделяй и властвуй”. Путем разделения программы на независимые компоненты программист может понизить видимый уровень ее сложности. При компонентно-ориентированном подходе программа организуется как набор строго определенных “строительных блоков” (компонентов), которые можно использовать, не вникая в детали их внутренней реализации. Суммарный эффект такого подхода состоит в снижении общей сложности программы. Сам собой напрашивается логический вывод: приложение может состоять только из одних компонентов, связанных между собой таким образом, что один компонент поставляет “питание” для другого. Такую организацию программ можно назвать компонентно-ориентированным программированием.
При такой мощности компонентов и простоте их создания в C# на вопрос:
“Компоненты — это будущее программирования?” многие программисты отвечают без колебаний: “Да!”