Глава 10

Делегаты, события и потоки выполнения

 

В этой главе рассматриваются делегаты и события — два взаимосвязанных сред­ства языка С#, позволяющие организовать эффективное взаимодействие объек­тов. Во второй части главы приводятся начальные сведения о разработке много­поточных приложений.

 

Делегаты

 

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

 

Описание делегатов

 

Описание делегата задает сигнатуру методов, которые могут быть вызваны с его помощью:                                                                                                          

 

[ атрибуты ] [ спецификаторы ] delegate тип имя_делегата ( [ параметры ] )              

 

Спецификаторы делегата имеют тот же смысл, что и для класса, причем допуска­ются только спецификаторы new, public, protected, internal и private. Тип описывает возвращаемое значение методов, вызываемых с помощью делега­та, а необязательными параметрами делегата являются параметры этих методов. Делегат может хранить ссылки на несколько методов и вызывать их поочередно; естественно, что сигнатуры всех методов должны совпадать.

 

Пример описания делегата:

 

public delegate void D ( int i );

 

Здесь описан тип делегата, который может хранить ссылки на методы, возвра­щающие voi d и принимающие один параметр целого типа.

 

ПРИМЕЧАНИЕ----------------------------------------------------------------------------------------------

Делегат, как и всякий класс, представляет собой тип данных. Его базовым классом является класс System. Delegate, снабжающий своего «отпрыска» некоторыми полез­ными элементами, которые мы рассмотрим позже. Наследовать от делегата нельзя, да и нет смысла.

---------------------------------------------------------------------------------------------------------------------

Объявление делегата можно размещать непосредственно в пространстве имен или внутри класса.

 

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

 

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

Делегаты применяются в основном для следующих целей:

□  получения возможности определять вызываемый метод не при компиляции, а динамически во время выполнения программы;

□  обеспечения связи между объектами по типу «источник — наблюдатель»;

□  создания универсальных методов, в которые можно передавать другие методы;

□  поддержки механизма обратных вызовов.

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

(C00l и Hack).

 

 

 

Использование делегата имеет тот же синтаксис, что и вызов метода. Если деле­гат хранит ссылки на несколько методов, они вызываются последовательно в том порядке, в котором были добавлены в делегат.

Добавление метода в список выполняется либо с помощью метода Combine, унас­ледованного от класса System.Delegate, либо, что удобнее, с помощью перегружен­ной операции сложения. Вот как выглядит измененный метод Main из предыду­щего листинга, в котором одним вызовом делегата выполняется преобразование исходной строки сразу двумя методами:

 

 

При вызове последовательности методов с помощью делегата необходимо учи­тывать следующее:

□  сигнатура методов должна в точности соответствовать делегату;

□ методы могут быть как статическими, так и обычными методами класса;

□  каждому методу в списке передается один и тот же набор параметров;

□  если параметр передается по ссылке, изменения параметра в одном методе от­разятся на его значении при вызове следующего метода;

□  если параметр передается с ключевым словом out или метод возвращает зна­чение, результатом выполнения делегата является значение, сформированное последним из методов списка (в связи с этим рекомендуется формировать спи­ски только из делегатов, имеющих возвращаемое значение типа void);

□ если в процессе работы метода возникло исключение, не обработанное в том же методе, последующие методы в списке не выполняются, а происходит по­иск обработчиков в объемлющих делегат блоках;

□ попытка вызвать делегат, в списке которого нет ни одного метода, вызывает генерацию исключения System.NullReferenceException.

 

Паттерн «наблюдатель»

 

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

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

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

Программисты часто используют одну и ту же схему организации и взаимодей­ствия объектов в разных контекстах. За такими схемами закрепилось название паттерны, или шаблоны проектирования. Описанная стратегия известна под на­званием паттерн «наблюдатель».

Наблюдатель (observer) определяет между объектами зависимость типа «один ко многим», так что при изменении состоянии одного объекта все зависящие от него объекты получают извещении автоматически обновляются. Рассмотрим пример (листинг 10.2), в котором демонстрируется схема оповещения источни­ком трех наблюдателей. Гипотетическое изменение состояния объекта модели­руется сообщением «OOPS!». Один из методов в демонстрационных целях сде­лан статическим.

 

 

 

 

В источнике объявляется экземпляр делегата, в этот экземпляр заносятся мето­ды тех объектов, которые хотят получать уведомление об изменении состояния источника. Этот процесс называется регистрацией делегатов. При регистрации имя метода добавляется к списку. Обратите внимание: для статического метода указывается имя класса, а для обычного метода — имя объекта. При наступлении «часа X» все зарегистрированные методы поочередно вызываются через делегат.

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

 

 

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

Связь «источник — наблюдатель» устанавливается во время выполнения про­граммы для каждого объекта по отдельности. Если наблюдатель больше не хочет получать уведомления от источника, можно удалить соответствующий метод из списка делегата с помощью метода Remove или перегруженной операции вычита­ния, например:

 

 

Операции

 

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

ПРИМЕЧАНИЕ----------------------------------------------------------------------------------------------

Делегаты, различающиеся только именами, считаются имеющими разные типы.

-------------------------------------------------------------------------------------------------------------------

С делегатами одного типа можно выполнять операции простого и сложного при­сваивания, например:

 

 

Эти операции могут понадобиться, например, в том случае, если в разных об­стоятельствах требуется вызывать разные наборы и комбинации наборов ме­тодов.

Делегат, как и строка string, является неизменяемым типом данных, поэтому при любом изменении создается новый экземпляр, а старый впоследствии уда­ляется сборщиком мусора.

 

Передача делегатов в методы

 

Поскольку делегат является классом, его можно передавать в методы в качестве параметра. Таким образом обеспечивается функциональная параметризация: в ме­тод можно передавать не только различные данные, но и различные функции их обработки. Функциональная параметризация применяется для создания универ­сальных методов и обеспечения возможности обратного вызова.

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

Обратный вызов (callback) представляет собой вызов функции, передаваемой в другую функцию в качестве параметра. Рассмотрим рис. 10.1. Допустим, в биб­лиотеке описана функция А, параметром которой является имя другой функции. В вызывающем коде описывается функция с требуемой сигнатурой (В) и переда­ется в функцию А. Выполнение функции А приводит к вызову В, то есть управле­ние передается из библиотечной функции обратно в вызывающий код.

 

 

 

Механизм обратного вызова широко используется в программировании. Напри­мер, он реализуется во многих стандартных функциях Windows.

Пример передачи делегата в качестве параметра приведен в листинге 10.3. Про­грамма выводит таблицу значений функции на заданном интервале с шагом, равным единице.

 

 

 

 

В среде Visual Studio 2005, использующей версию 2.0 языка С#, можно при­менять упрощенный синтаксис для делегатов. Первое упрощение заключается в том, что в большинстве случаев явным образом создавать экземпляр делегата не требуется, поскольку он создается автоматически по контексту. Второе упро­щение заключается в возможности создания так называемых анонимных мето­дов — фрагментов кода, описываемых непосредственно в том месте, где исполь­зуется делегат. В листинге 10.4 использованы оба упрощения для реализации тех же действий, что и листинге 10.3.

 

 

 

 

 

В первом случае экземпляр делегата, соответствующего функции Sin, создается автоматически. Чтобы это могло произойти, список параметров и тип возвра­щаемого значения функции должны быть совместимы с делегатом. Во втором случае не требуется оформлять простой фрагмент кода в виде отдельной функции Simple, как это было сделано в предыдущем листинге, — код функции оформля­ется как анонимный метод и встраивается прямо в место передачи.

Альтернативой использованию делегатов в качестве параметров являются вирту­альные методы. Универсальный метод вывода таблицы значений функции можно реализовать с помощью абстрактного базового класса, содержащего два метода: метод вывода таблицы и абстрактный метод, задающий вид вычисляемой функции. Для вывода таблицы конкретной функции необходимо создать производный класс, переопределяющий этот абстрактный метод. Реализация метода вывода таблицы с помощью наследования и виртуальных методов приведена в листинге 10.5.

 

 

 

 

 

 

 

 

 

 

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

 

Обработка исключений при вызове делегатов

 

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

получить с помощью унаследованного метода GetlnvocationList. Этот прием иллю­стрирует листинг 10.6, представляющий собой измененный вариант листинга 10.1.

 

 

 

 

В этой программе помимо метода базового класса GetInvocationList использова­но свойство Method. Это свойство возвращает результат типа Method Info. Класс Method Info содержит множество свойств и методов, позволяющих получить пол­ную информацию о методе, например его спецификаторы доступа, имя и тип возвращаемого значения. Мы рассмотрим этот интересный класс в разделе «Реф­лексия» главы 12.

 

События

 

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

Механизм событий можно также описать с помощью модели «публикация — под­писка»: один класс, являющийся отправителем (sender) сообщения, публикует события, которые он может инициировать, а другие классы, являющиеся получа­телями (receivers) сообщения, подписываются на получение этих событий.

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

□  описание делегата, задающего сигнатуру обработчиков событий;

□  описание события;

□  описание метода (методов), инициирующих событие. Синтаксис события похож на синтаксис делегата

 

[ атрибуты ] [ спецификаторы ] event тип имя_события

 

Для событий применяются спецификаторы new, public, protected, internal, private, static, virtual, sealed, override, abstract и extern, которые изучались при рассмот­рении методов классов. Например, так же как и методы, событие может быть ста­тическим (static), тогда оно связано с классом в целом, или обычным — в этом случае оно связано с экземпляром класса.

Тип события — это тип делегата, на котором основано событие. Пример описания делегата и соответствующего ему события:

 

 

Обработка событий выполняется в классах-получателях сообщения. Для этого в них описываются методы-обработчики событий, сигнатура которых соответст­вует типу делегата. Каждый объект (не класс!), желающий получать сообщение, должен зарегистрировать в объекте-отправителе этот метод.

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

ПРИМЕЧАНИЕ-------------------------------------------------------------------------------------------

Событие — это удобная абстракция для программиста. На самом деле оно состоит из закрытого статического класса, в котором создается экземпляр делегата, и двух методов, предназначенных для добавления и удаления обработчика из списка этого делегата.

-----------------------------------------------------------------------------------------------------------------

В листинге 10.7 приведен код из листинга 10.2, переработанный с использовани­ем событий.

 

 

 

 

 

Внешний код может работать с событиями единственным образом: добавлять об­работчики в список или удалять их, поскольку вне класса могут использоваться только операции += и -=. Тип результата этих операций — void, в отличие от опе­раций сложного присваивания для арифметических типов. Иного способа досту­па к списку обработчиков нет.

Внутри класса, в котором описано событие, с ним можно обращаться, как с обыч­ным полем, имеющим тип делегата: использовать операции отношения, присваи­вания и т. д. Значение события по умолчанию — null. Например, в методе CryOops выполняется проверка на nul 1 для того, чтобы избежать генерации исключения

System.NullReferenceException.

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

□   имя делегата заканчивается суффиксом EventHandler;

□   делегат получает два параметра:

О первый параметр задает источник события и имеет тип object;

О второй параметр задает аргументы события и имеет тип EventArgs или про­изводный от него.

Если обработчикам события требуется специфическая информация о событии, то для этого создают класс, производный от стандартного класса EventArgs, и до­бавляют в него необходимую информацию. Если делегат не использует такую информацию, можно не описывать делегата и собственный тип аргументов, а обой­тись стандартным классом делегата System.EventHandler.

Имя обработчика события принято составлять из префикса On и имени события. В листинге 10.8 приведен пример из листинга 10.7, оформленный в соответствии со стандартными соглашениями .NET. Найдите восемь отличий!

 

 

 

 

 

 

 

 

Те, кто работает с С# версии 2.0, могут упростить эту программу, используя но­вую возможность неявного создания делегатов при регистрации обработчиков событий. Соответствующий вариант приведен в листинге 10.9. В демонстраци­онных целях в код добавлен новый анонимный обработчик — еще один меха­низм, появившийся в новой версии языка.

 

 

 

События включены во многие стандартные классы .NET, например, в классы пространства имен Windows.Forms, используемые для разработки Windows-прило­жений. Мы рассмотрим эти классы в главе 14.

 

Многопоточные приложения

 

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

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

Недостатки многопоточности:

□   большое количество потоков ведет к увеличению накладных расходов, свя­занных с их переключением, что снижает общую производительность системы;

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

 

Класс Thread

 

Поддержка многопоточности осуществляется в .NET в основном с помощью пространства имен System.Threading. Некоторые типы этого пространства описа­ны в табл. 10.1.

 

 

 

 

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

 

Thread t = new Thread ( new ThreadStart( имя_метода ) );

 

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

 

 

 

 

 

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

В табл. 10.2 перечислены основные элементы класса Thread.

 

 

 

Можно создать несколько потоков, которые будут совместно использовать один и тот же код. Пример приведен в листинге 10.11.

 

 

 

 

Варианты вывода могут несколько различаться, поскольку один поток прерыва­ет выполнение другого в неизвестные моменты времени.

Для того чтобы блок кода мог использоваться в каждый момент только одним потоком, применяется оператор lock. Формат оператора:

lock ( выражение ) блокоператоров

Выражение определяет объект, который требуется заблокировать. Для обычных методов в качестве выражения используется ключевое слово this, для статиче­ских — typeof (класс). Блок операторов задает критическую секцию кода, которую требуется заблокировать.

Например, блокировка операторов в приведенном ранее методе Do выглядит сле­дующим образом:

 

 

 

Асинхронные делегаты

 

Делегат можно вызвать на выполнение либо синхронно, как во всех приведенных ранее примерах, либо асинхронно с помощью методов Begin Invoke и End Invoke. При вызове делегата с помощью метода Begin Invoke среда выполнения создает для исполнения метода отдельный поток и возвращает управление оператору, следующему за вызовом. При этом в исходном потоке можно продолжать вычис­ления.

Если при вызове BeginInvoke был указан метод обратного вызова, этот метод вызы­вается после завершения потока. Метод обратного вызова также задается с помо­щью делегата, при этом используется стандартный делегат AsyncCal Iback. В методе обратного вызова для получения возвращаемого значения и выходных парамет­ров применяется метод Endlnvoke.

Если метод обратного вызова не был указан в параметрах метода Beginlnvoke, ме­тод End Invoke можно использовать в потоке, инициировавшем запрос. В листинге 10.11 приводятся два примера асинхронного вызова метода, выпол­няющего разложение числа на множители. Листинг приводится по документа­ции Visual Studio с некоторыми изменениями.

Класс Factorizer содержит метод Factorize, выполняющий разложение на множители? Этот метод асинхронно вызывается двумя способами: в методе Numl ме­тод обратного вызова задается в Beginlnvoke, в методе Num2 имеют место ожидание завершения потока и непосредственный вызов End Invoke.

 

 

 

 

 

 

 

ПРИМЕЧАНИЕ----------------------------------------------------------------------------------------------

Атрибут [OneWayAttribute()] помечает метод как не имеющий возвращаемого зна­чения и выходных параметров.

--------------------------------------------------------------------------------------------------------------------

 

Рекомендации по программированию

 

Делегаты широко применяются в библиотеке .NET как самостоятельно, так и для поддержки механизма событий, который имеет важнейшее значение при про­граммировании под Windows.

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

Начиная с версии 2.0, в С# поддерживаются возможности, упрощающие процесс программирования с применением делегатов — неявное создание делегатов при регистрации обработчиков событий и анонимные обработчики.

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

 

Глава 11

 

Работа с файлами

 

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

Ввод-вывод в С# выполняется с помощью подсистемы ввода-вывода и классов библиотеки .NET. В этой главе рассматривается обмен данными с файлами и их частным случаем — консолью. Обмен данными реализуется с помощью потоков.

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

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

Механизм буферизации позволяет более быстро и эффективно обмениваться информацией с внешними устройствами.

Для поддержки потоков библиотека .NET содержит иерархию классов, основная часть которой представлена на рис. 11.1. Эти классы определены в пространстве имен System. 10. Помимо классов там описано большое количество перечислений для задания различных свойств и режимов.

 

 

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

 

 

 

 

 

Как можно видеть из таблицы, выполнять обмен с внешними устройствами мож­но на уровне:

            □   двоичного представления данных (BinaryReader, BinaryWriter);

            □  байтов (FileStream);

            □   текста, то есть символов (StreamWriter, StreamReader).

 

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

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

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

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

 

ПРИМЕЧАНИЕ----------------------------------------------------------------------------------------------

Методы форматированного ввода, с помощью которых можно выполнять ввод с кла­виатуры или из текстового файла значений арифметических типов, в С# не под­держиваются. Для преобразования из символьного в числовое представление используются методы класса Convert или метод Parse, рассмотренные в разделе «Простейший ввод-вывод» (см. с. 59).

------------------------------------------------------------------------------------------------------------------

ПРИМЕЧАНИЕ----------------------------------------------------------------------------------------------

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

---------------------------------------------------------------------------------------------------------------------

Помимо перечисленных классов в библиотеке .NET есть классы XmlTextReader и XmlTextWriter, предназначенные для формирования и чтения кода в формате XML. Понятие об XML дается в главе 15.

Рассмотрим простейшие способы работы с файловыми потоками. Использова­ние классов файловых потоков в программе предполагает следующие операции:

1.   Создание потока и связывание его с физическим файлом.

2.   Обмен (ввод-вывод).

3.   Закрытие файла.

Каждый класс файловых потоков содержит несколько вариантов конструкторов, с помощью которых можно создавать объекты этих классов различными спосо­бами и в различных режимах.

Например, файлы можно открывать только для чтения, только для записи или для чтения и записи. Эти режимы доступа к файлу содержатся в перечислении FileAccess, определенном в пространстве имен System. 10. Константы перечисле­ния приведены в табл. 11.2.

 

Возможные режимы открытия файла определены в перечислении FileMode (табл. 11.3).

 

 

Режим FileMode.Append можно использовать только совместно с доступом типа FileAccess.Write, то есть для файлов, открываемых для записи.

Режимы совместного использования файла различными пользователями опреде­ляет перечисление FileShare (табл. 11.4).

 

 

Потоки байтов

 

Ввод-вывод в файл на уровне байтов выполняется с помощью класса FileStream, который является наследником абстрактного класса Stream, определяющего набор стандартных операций с потоками. Элементы класса Stream описаны в табл. 11.5.

 

 

Класс FileStream реализует эти элементы для работы с дисковыми файлами. Для определения режимов работы с файлом используются стандартные перечис­ления FileMode, FileAccess и FileShare. Значения этих перечислений приведены в табл. 11.2—11.4. В листинге 11.1 представлен пример работы с файлом. В при­мере демонстрируются чтение и запись одного байта и массива байтов, а также позиционирование в потоке.

 

 

 

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

100 0 1 2 3 4 5 6 7 8 9 10 9 8 7 6 0 0 0 0

4

5

Текущая позиция в потоке 7

 

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

Для установки желаемой позиции чтения используется метод Seek, имеющий два параметра: первый задает смещение в байтах относительно точки отсчета, зада­ваемой вторым. Точки отсчета задаются константами перечисления SeekOrigin: начало файла — Begin, текущая позиция — Current и конец файла — End.

В данном примере файл создавался в текущем каталоге. Можно указать и пол­ный путь к файлу, при этом удобнее использовать дословные литералы, речь о которых шла в разделе «Литералы» (см. с. 30), например:

 

FileStream f = new FileStream( @"D:\C#\test.txt",

                        FileMode.Create, FileAccess.ReadWrite );

 

В дословных литералах не требуется дублировать обратную косую черту.

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

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

□   FileNotFoundException, если файла с указанным именем в указанном каталоге не существует;

□   DirectoryNotFoundException, если не существует указанный каталог;

□  ArgumentException, если неверно задан режим открытия файла;

□  IOException, если файл не открывается из-за ошибок ввода-вывода. Возможны и другие исключительные ситуации.

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

 

 

 

 

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

 

Асинхронный ввод-вывод

 

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

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

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

Аналогично выполняется и асинхронный вывод. В листинге 11.2 приведен при­мер асинхронного чтения из файла большого объема и параллельного выполне­ния диалога с пользователем.

ПРИМЕЧАНИЕ----------------------------------------------------------------------------------------------

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

 

 

 

 

 

 

 

 

Для удобства восприятия операции чтения из файла и диалога с пользователем оформлены в отдельный класс Demo.

Метод OnCompletedRead (оператор 1) должен получать один параметр стандартно­го типа IAsyncResult, содержащий сведения о завершении операции, которые пе­редаются в метод EndRead.

Файл открывается в асинхронном режиме, об этом говорит значение true по-следнего параметра конструктора (оператор 2). В операторе 3 создается экзем­пляр стандартного делегата AsyncCal Iback, который инициализируется методом

 

OnCompletedRead.

 

С помощью этого делегата метод OnCompletedRead передается в метод BeginRead (опе­ратор 4), который создает отдельный поток, начинает асинхронный ввод и воз­вращает управление в вызвавший поток. Обратный вызов метода OnCompletedRead происходит при завершении операции ввода. При достаточно длинном файле verybigfile можно убедиться, что приглашение к вводу в методе User Input выдается раньше, чем сообщение о завершении операции ввода из метода OnCompletedRead.

 

ПРИМЕЧАНИЕ ---------------------------------------------------------------------------------------------

Пример, приведенный в листинге 11.2, максимально упрощен для демонстрации методов BeginRead и EndRead, поэтому в нем нет необходимых в любой программе проверок наличия файла, успешности считывания и т. д.

 

Потоки символов

 

Символьные потоки StreamWriter и StreamReader работают с Unicode-символами, следовательно, ими удобнее всего пользоваться для работы с файлами, предна­значенными для восприятия человеком. Эти потоки являются наследниками классов TextWriter и TextReader Соответственно, которые обеспечивают их боль­шей частью функциональности, В табл. 11.6 и 11.7 приведены наиболее важные элементы этих классов. Как видите, произвольный доступ для текстовых файлов не поддерживается.

 

 

 

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

В листинге 11.3 создается текстовый файл, в который записываются две строки. Вторая строка формируется из преобразованных численных значений пере­менных и поясняющего текста. Содержимое файла можно посмотреть в любом текстовом редакторе. Файл создается в том же каталоге, куда среда записывает исполняемый файл. По умолчанию это каталог ...\ConsoleApplication1\bin\Debug.

 

 

 

 

 

 

 

 

В этой программе весь файл считывается за один прием с помощью метода ReadToEnd. Чаще возникает необходимость считывать файл построчно, такой при­мер приведен в листинге 11.5. Каждая строка при выводе предваряется номером.

 

 

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

На содержимое файла накладываются весьма строгие ограничения: числа долж­ны быть разделены ровно одним пробелом, после последнего числа в строке про­бела быть не должно, файл не должен заканчиваться символом перевода строки. Методы разбиения строки и преобразования в целочисленное представление рассматривались ранее.

Двоичные потоки

 

Двоичные файлы хранят данные в том же виде, в котором они представлены в опе­ративной памяти, то есть во внутренней форме представления. Двоичные файлы применяются не для просмотра их человеком, а для использования в программах. Выходной поток BinaryWriter поддерживает произвольный доступ, то есть име­ется возможность выполнять запись в»произвольную позицию двоичного файла.

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

Основные методы двоичных потоков приведены в табл. 11.8 и 11.9.

 

 

 

В листинге 11.7 приведен пример формирования двоичного файла. В файл запи­сывается последовательность вещественных чисел, а затем для демонстрации произвольного доступа третье число заменяется числом 8888.

 

 

 

При создании двоичного потока в него передается объект базового потока. При установке указателя текущей позиции в файле учитывается длина каждого зна­чения типа doubi e — 8 байт.

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

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

 

 

 

 

Консольный ввод-вывод

 

Консольные приложения имеют весьма ограниченную область применения, са­мой распространенной из которых является обучение языку программирования. Для организации ввода и вывода используется известный вам класс Console, оп­ределенный в пространстве имен System. В этом классе определены три стандартных

потока: входной поток Console. In класса TextReader и выходные потоки Console.Out и Console.Error класса TextWriter.

По умолчанию входной поток связан с клавиатурой, а выходные — с экраном, однако можно перенаправить эти потоки на другие устройства с помощью мето­дов Set In и SetOut или средствами операционной системы (перенаправление с по­мощью операций <, > и »).

При обмене с консолью можно применять методы указанных потоков, но чаще используются методы класса Console Read, ReadLine, Write и WriteLine, которые просто передают управление методам нижележащих классов In, Out и Error.

Использование не одного, а двух выходных потоков полезно при желании разде­лить нормальный вывод программы и ее сообщения об ошибках. Например, нор­мальный вывод программы можно перенаправить в файл, а сообщения об ошиб­ках — на консоль или в файл журнала.

Работа с каталогами и файлами

 

В пространстве имен System. I0 есть четыре класса, предназначенные для рабо­ты с физическими файлами и структурой каталогов на диске: Directory, File, Directory Info и Fi1elnfo. С их помощью можно выполнять создание, удаление, перемещение файлов и каталогов, а также получение их свойств.

Классы Directory и File реализуют свои функции через статические методы. Directory Info и Fi I elnfo обладают схожими возможностями, но они реализу­ются путем создания объектов соответствующих классов. Классы Directorylnfo и FiIelnfo происходят от абстрактного класса FileSystemlnfo, который снабжает их базовыми свойствами, описанными в табл. 11.10.

 

 

Класс Directory Info содержит элементы, позволяющие выполнять необходимые действия с каталогами файловой системы. Эти элементы перечислены в табл. 11.11.

 

 

В листинге 11.9 приведен пример, в котором создаются два каталога, выводится информация о них и предпринимается попытка удаления каталога.

 

 

 

 

Каталог не пуст, поэтому попытка его удаления не удалась. Впрочем, если ис­пользовать перегруженный вариант метода Delete с одним параметром, задаю­щим режим удаления, можно удалить и непустой каталог:

 

dil.Delete( true );                                              // удаляет непустой каталог

 

Обратите внимание на свойство Attributes. Некоторые его возможные значения, заданные в перечислении FileAttributes, приведены в табл. 11.12.

 

 

 

Листинг 11.10 демонстрирует использование класса Filelnfo для копирования всех файлов с расширением jpg из каталога d:\foto в каталог d:\temp. Метод Exists позволяет проверить, существует ли исходный каталог.

 

 

 

 

 

Использование классов Fi 1 е и D1 rectory аналогично, за исключением того, что их методы являются статическими и, следовательно, не требуют создания объектов.

 

Сохранение объектов (сериализация)

 

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

К счастью, сериализация выполняется автоматически, достаточно просто пометить класс как сериализуемый с помощью атрибута [Serializable]. Атрибуты рассмат­риваются в главе 12, пока же достаточно знать, что атрибуты — это дополнитель­ные сведения о классе, которые сохраняются в его метаданных. Те поля, которые сохранять не требуется, помечаются атрибутом [NonSerialized], например:

 

 

 

Объекты можно сохранять в одном из двух форматов: двоичном или SOAP (в виде XML-файла). В первом случае следует подключить к программе пространство имен System.Runtime.Serialization.Formatters.Binary, во втором — пространство System.Runtime.Serialization.Formatters.Soap.

Рассмотрим сохранение объектов в двоичном формате. Для этого используется класс BinaryFormatter, в котором определены два метода:

 

Serialize( поток, объект );

Deserialize( поток );

 

Метод Serialize сохраняет заданный объект в заданном потоке, метод Deserialize восстанавливает объект из заданного потока.

В листинге 11.11 объект приведенного ранее класса Demo сохраняется в файле на диске с именем Demo.bin. Этот файл можно просмотреть, открыв его, к примеру, в Visual Studio.NET.

 

 

 

В программе не приведены неиспользуемые методы и свойства классов. Обрати­те внимание на то, что базовые классы сохраняемых объектов также должны быть помечены как сохраняемые. Результат работы программы:

 

Monster Вася        health = 100 ammo = 80

Monster Петя        health = 120 ammo = 50

2

2                                                                                              ,

 

Итак, для сохранения объекта в двоичном формате необходимо:

1.   Подключить к программе пространство имен System.Runtime.Serialization.

Formatters.Binary.

2.   Пометить сохраняемый класс и связанные с ним классы атрибутом [Serializable].

3.   Создать поток и связать его с файлом на диске или с областью оперативной памяти.

4.   Создать объект класса BinaryFormatter.

5.   Сохранить объекты в потоке.

6.   Закрыть файл.

В листинге 11.12 сохраненный объект считывается из файла.

 

 

Как видите, при сериализации сохраняется все дерево объектов. Обратите вни­мание на то, что значение поля у не было сохранено, поскольку оно было помече­но как несохраняемое.

 

ПРИМЕЧАНИЕ----------------------------------------------------------------------------------------------

Сериализация в формате SOAP выполняется аналогично с помощью класса SoapFormatter. Программист может задать собственный формат сериализации, для этого ему придется реализовать в своих классах интерфейс I Serializable и специ­альный вид конструктора класса.

-------------------------------------------------------------------------------------------------------------------

 

Рекомендации по программированию

 

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

Поток определяется как последовательность байтов и не зависит от конкретного устройства, с которым производится обмен. Классы библиотеки позволяют ра­ботать с потоками в различных режимах и на различных уровнях: на уровне дво­ичного представления данных, байтов и текста. Двоичные и байтовые потоки хранят данные во внутреннем представлении, текстовые  в кодировке Unicode. Поток можно открыть в синхронном или асинхронном режиме для чтения, записи или добавления. Доступ к файлам может быть последовательным и произволь­ным Текстовые файлы позволяют выполнять только последовательный доступ, в двоичных и байтовых потоках можно использовать оба метода. Прямой доступ в сочетании с отсутствием преобразований обеспечивает высокую скорость обмена. Методы форматированного ввода для значений арифметических типов в С# не поддерживаются. Для преобразования из символьного в числовое представление используются методы класса Convert или метод Parse. Форматированный вывод выполняется с помощью перегруженного метода ToString, результат выполнения которого передается в методы текстовых файлов.

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

Для сохранения объектов (сериализации) используется атрибут [Serializable]. Объекты можно сохранять в одном из двух форматов: двоичном или SOAP (в виде XML-файла).

 

 

Глава 12

Сборки, библиотеки, атрибуты, директивы

 

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

 

Сборки

 

В результате компиляции в среде .NET создается сборка — файл с расширением ехе или dll, который содержит код на промежуточном языке, метаданные типов, ма­нифест и ресурсы (рис. 12.1). Понятие сборки было введено в главе 1 (см. с. 9), а сейчас мы рассмотрим ее составные части более подробно.

 

 

 

Промежуточный язык (Intermediate Language, IL) не содержит инструкций, зависящих от операционной системы и типа компьютера, что обеспечивает две основные возможности:

□  выполнение приложения на любом типе компьютера, для которого существует среда выполнения CLR;

□  повторное использование кода, написанного на любом .NET-совместимом язык?.

 

IL-код можно просмотреть с помощью дизассемблера ILDasm.exe, который нахо­дится в папке ...\SDK\bin\ каталога размещения Visual Studio.NET. После за­пуска ILDasm можно открыть любой файл среды .NET с расширением ехе или dll с помощью команды File Open. В окне программы откроется список всех эле­ментов сборки, сведения о каждом можно получить двойным щелчком. При этом открывается окно, в котором для методов выводится доступный для восприятия дизассемблированный код.

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

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

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

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

На логическом уровне сборка представляет собой совокупность взаимосвязан­ных типов — классов, интерфейсов, структур, перечислений, делегатов и ресур­сов. Библиотека .NET представляет собой совокупность сборок, которую исполь­зуют приложения. Точно так же можно создавать и собственные сборки, которые можно будет задействовать либо в рамках одного приложения (частные сборки), либо совместно различными приложениями (открытые сборки). По умолчанию все сборки являются частными.                                               

Манифест сборки содержит:                                                                             

 

□   идентификатор версии;                                                                                   

□ список всех внутренних модулей сборки;                                                       

□   список внешних сборок, необходимых для нормального выполнения сборки;   

□   информацию о естественном языке, используемом в сборке (например, русском);                                                                                                           

□   «сильное» имя (strong name) — специальный вариант имени сборки, используемый для открытых сборок;                                                                       

□   необязательную информацию, связанную с безопасностью;

□   необязательную информацию, связанную с хранением ресурсов внутри сбор­ки (подробнее о форматах ресурсов .NET см. [27]).                                        

 

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

 

□   основной номер версии (major version);

□  дополнительный номер версии (minor version);                                             

□  номер сборки (build number);                                                                        

□  номер ревизии (revision number).

 

Среда выполнения применяет идентификатор версий для определения того, какие из открытых сборок совместимы с требованиями клиента. Например, если клиент запрашивает сборку 3.1.0.0, а присутствует только версия 3.4.0.0, сборка не будет опознана как подходящая, поскольку считается, что в дополнительных версиях могут произойти изменения в типах и их элементах. Разные номера ревизии допускают, но не гарантируют совместимость. Номер сборки на совместимость не влияет, так как чаще всего он изменяется при установке заплатки, или патча (patch).

Идентификатор версии формируется автоматически, но при желании можно за­дать его вручную с помощью атрибута [AssemblyVersion], который рассматривает­ся далее на с. 285.

Информация о безопасности позволяет определить, предоставить ли клиенту доступ к запрашиваемым элементам сборки. В манифесте сборки определены ог­раничения системы безопасности.

Ресурсы представляют собой, например, файлы изображений, помещаемых на форму, текстовые строки, значки приложения и т. д. Хранение ресурсов внут­ри сборки обеспечивает их защиту и упрощает развертывание приложения. Среда Visual Studio.NET предоставляет возможности автоматического внедре­ния ресурсов в сборку.

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

Открытые сборки размещаются в специальном каталоге, который называется глобальным кэшем сборок (Global Assembly Cache, GAC). Для идентификации открытой сборки используется уже упоминавшееся сильное имя (strong name), которое должно быть уникальным.

 

Создание библиотеки

 

Для создания библиотеки следует при разработке проекта в среде Visual Studio.NET выбрать шаблон Class Library (библиотека классов). В главе 8 была создана про­стая иерархия классов персонажей компьютерной игры. В этом разделе мы офор­мим ее в виде библиотеки, то есть сборки с расширением dll. Для сборки задано имя MonsterLib (рис. 12.2).

 

 

Текст модуля приведен в листинге 12.1. По сравнению с модулем из главы 8 в него добавлены спецификаторы доступа publiс для всех трех классов, входящих в биб­лиотеку.

 

 

Скомпилировав библиотеку, вы обнаружите файл MonsterLib.dll в каталогах ...\bin\ Debug и ...\obj\Debug. Открыв файл Monsterl_ib.dll с помощью программы ILDasm.exe, можно получить полную информацию о созданной библиотеке (рис. 12.3).

 

 

 

Использование библиотеки

 

Любая библиотека — это сервер, предоставляющий свои ресурсы клиентам. Соз­дадим клиентское приложение, выполняющее те же функции, что и приложение из раздела «Виртуальные методы» (см. с. 178), но с использованием библиотеки MonsterLib.dll. Для того чтобы компилятор мог ее обнаружить, необходимо после создания проекта (как обычно, это — консольное приложение) подключить ссылку на библиотеку с помощью команды Project Add Reference (Добавить ссылку). Для поиска каталога, содержащего библиотеку, следует использовать кнопку Browse.

После подключения библиотеки можно пользоваться ее открытыми элементами таким же образом, как если бы они были описаны в том же модуле. Текст прило­жения приведен в листинге 12.2.

 

Результаты работы программы совпадают с полученными в листинге 8.3. Ана­лиз каталога ...\bin\Debug показывает, что среда создала в нем копию библиотеки MonsterLib.dll, то есть поместила библиотеку в тот же каталог, что и исполняемый файл. Если скопировать эти два файла в другое место, программа не потеряет своей работоспособности — главное, чтобы оба файла находились в одном каталоге.

Допускается также, чтобы частные сборки находились в подкаталогах основного каталога приложения.

 

ПРИМЕЧАНИЕ ----------------------------------------------------------------------------------------------

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

--------------------------------------------------------------------------------------------------------------------

 

Рефлексия

 

Рефлексия — это получение информации о типах во время выполнения програм­мы. Например, можно получить список всех классов и интерфейсов сборки, спи­сок элементов каждого из классов, список параметров каждого метода и т. д. Вся информация берется из метаданных сборки. Для использования рефлексии не­обходимы класс System.Type и типы пространства имен System.Reflection

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

Наиболее важные свойства и методы класса Туре приведены в табл. 12.1.

 

 

 

 

 

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

1.   В базовом классе object описан метод Get Ту ре, которым можно воспользовать­ся для любого объекта, поскольку он наследуется. Метод возвращает объект типа Туре, например:

 

Monster X = new Monster():

Type t = X.GetType();

 

2.   В классе Type описан статический метод GetType с одним параметром строко­вого типа, на место которого требуется передать имя класса (типа), например:

 

Type t = Type.GetType( "Monster" ):

 

3.   Операция typeof возвращает объект класса Туре для типа, заданного в качест­ве параметра, например:

 

Type t = typeof( Monster ):

 

 

При использовании второго и третьего способов создавать экземпляр исследуе­мого класса нет необходимости.

Как видно из табл. 12.1, многие методы класса Туре возвращают экземпляры стан­дартных классов (например, Memberlnfo). Эти классы описаны в пространстве имен System.Reflection. Наиболее важные из этих классов перечислены в табл. 12.2.

 

 

В листинге 12.3 приведены примеры использования рассмотренных методов и классов для получения информации о классах библиотеки из листинга 12.1

 

 

 

 

 

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

 

Атрибуты

 

Атрибуты — это дополнительные сведения об элементах программы (классах, мето­дах, параметрах и т. д.). С помощью атрибутов можно добавлять информацию в ме­таданные сборки и затем извлекать ее во время выполнения программы. Атрибут яв­ляется специальным видом класса и происходит от базового класса System.Attribute.

Атрибуты делятся на стандартные и пользовательские. В библиотеке .NET преду­смотрено множество стандартных атрибутов, которые можно использовать в про­граммах. Если всего разнообразия стандартных атрибутов не хватит, чтобы удовле­творить прихотливые требования программиста, он может описать собственные классы атрибутов, после чего применять их точно так же, как стандартные.

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

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

 

[Serial izable]

class Monster

{  

             …         

 

[NonSeriaiized]

 string name;

 int health, ammo;

 }                                                             -                                                               

 

Атрибут [Serializable], означающий, что объекты этого класса можно сохранять во внешней памяти, относится ко всему классу Monster. При этом поле name поме­чено атрибутом [NonSerialized], что говорит о том, что это поле сохраняться не должно. Сохранение объектов рассматривалось в главе 10.

Обычно из контекста понятно, к какому элементу сборки относится атрибут, однако в некоторых случаях могут возникнуть неоднозначности. Для их устра­нения перед именем атрибута записывается тип элемента сборки — уточняющее ключевое слово, отделяемое от атрибута двоеточием. Ключевые слова и соответ­ствующие элементы сборки, к которым могут относиться атрибуты, перечислены в табл. 12.3.                                                                                                                                                                 

 

 

 

Пусть, например, перед методом описан гипотетический атрибут ABC:

 

[ABC]

public void Do()   { ... }

 

По умолчанию он относится к методу. Чтобы указать, что атрибут относится не к методу, а к его возвращаемому значению, следует написать:

 

[return:ABC]

public void Do() { ... }                                                      

 

Атрибут может иметь параметры. Они записываются в круглых скобках через запятую после имени атрибута и бывают позиционными и именованными. Име­нованный параметр указывается в форме имя = значение, для позиционного про­сто задается значение. Например, для использованного в следующем фрагмен­те кода атрибута CLSCompliant задан позиционный параметр true. Атрибуты,

относящиеся к сборке, должны располагаться непосредственно после директив using, например:

 

using System;

[assembly:CLSCompliant(true)]

namespace ConsoleApplicationl

{ ...

 

Атрибут[CLSCompliant] определяет, удовлетворяет программный код соглашени­ям CLS (Common Language Specification) или нет.

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

Стандартный атрибут [STAThread], старательно удаленный из всех листингов в этой книге, относится к методу, перед которым он записан. Он имеет значение только для приложений, использующих модель СОМ, и задает модель потоков в рамках модели СОМ. Пример применения еще одного стандартного атрибута, [Conditional], приведен далее в разделе «Директивы препроцессора».

Атрибуты уровня сборки хранятся в файле Assemblylnfo.cs, автоматически созда­ваемом средой для любого проекта. Для явного задания номера версии сборки можно записать атрибут [AssemblyVersion], например:

 

[assembly: AssemblyVersion ('1.0.0.0")]                                  ,

 

Создание пользовательских атрибутов выходит за рамки темы этого учебника. Интересующиеся могут обратиться к книге [27].

 

Пространства имен

 

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

□  логического группирования элементов программы, расположенных в различ­ных физических файлах;

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

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

Пространства имен могут быть вложенными, например:

 

 

Вложенные пространства имен, как вы наверняка успели заметить, широко при­меняются в библиотеке .NET.

Существует три способа использования типа, определенного в каком-либо про­странстве имен:

  1. Использовать полностью квалифицированное имя. Например, в пространстве имен System.Runtime.Serialization.Formatters.Binary описан класс BinaryFormatter. Создание объекта этого класса с помощью квалифицированного имени вы­глядит так:

            System.Runtime.Serialization.Formatters.Binary.BinaryFormatter bf = new             System.Runtime.Serialization.Formatters.Binary.BinaryFormatter( );

 

  1. Использовать директиву using, с помощью которой импортируются все имена из заданного пространства. В этом случае предыдущий пример примет вид

 

            using System.Runtime.Serialization.Formatters.Binary;     

            ….

            BinaryFormatter bf = new BinaryFormatter( );

 

ВНИМАНИЕ -----------------------------------------------------------------------------------------------

Директива using должна располагаться вне или внутри пространства имен, но до любых описаний типов.

------------------------------------------------------------------------------------------------------------------

 

      3.   Использовать псевдоним типа. Это делается с помощью второй формы дирек­тивы using:

            using BinF = System.Runtime.Serialization.Formatters.Binary.BinaryFormatter;

           

            BinF bf = new BinF( )

 

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

В версию языка С# 2.0 введена возможность применять псевдоним прострет ей имен с помощью операции ::, например:

 

 

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

Таким образом, сборки обеспечивают физическое группирование типов, а пространства имен — логическое. В мире сетевого программирования, когда программисту доступны десятки тысяч классов, пространства имен совершенно необходимы как  для классификации и поиска, так и для предотвращения конфликтов имен типов.

 

 

 

 

Директивы препроцессора

 

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

 Каждая директива располагается на отдельной строке и не заканчивается точ с запятой, в отличие от операторов языка. В одной строке с директивой мо Располагаться только комментарий вида //. Перечень и краткое описание директив приведены в табл. 12.4.

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

 

 

 

Количество директив #el i f произвольно. Исключаемые блоки кода могут со­держать как описания, так и исполняемые операторы. Константное выражение может содержать одну или несколько символьных констант, объединенных знаками операций = =, ! =, !, && и ||. Также допускаются круглые скобки. Кон­станта считается равной true, если она была ранее определена с помощью директивы #define.

Пример применения директив приведен в листинге 12.4.

 

 

В зависимости от того, определение какой символьной константы раскомменти­ровать, в компиляции будет участвовать один из трех методов F.

Директива #define применяется не только в сочетании с директивами условной компиляции. Можно применять ее вместе со стандартным атрибутом Conditional для условного управления выполнением методов. Метод будет выполняться, если константа определена. Пример приведен в листинге 12.5. Обратите внима­ние на то, что для применения атрибута необходимо подключить пространство имен System.Diagnostics.

 

 

 

 

                                                        

 

 

В методе Main записаны вызовы обоих методов, однако в данном случае будет выполнен только метод В, поскольку символьная константа VAR1 не определена.

 

Рекомендации по программированию

 

В этой главе приведено краткое введение в средства, использующиеся при про­фессиональной разработке программ: библиотеки, рефлексия типов, атрибуты, пространства имен и директивы препроцессора. Для реального использования этих возможностей в программах необходимо изучать документацию и дополни­тельную литературу, например [20], [21], [26], [27], [30].