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


Мы поможем в написании ваших работ!



ЗНАЕТЕ ЛИ ВЫ?

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



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

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

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

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

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

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

public delegate void D (int i); Здесь описан тип делегата, который может хранить ссылки на методы, возвращающие void и принимающие один параметр целого типа.

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

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

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

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

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

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

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

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

static void MainO

string s = "cool hackers";

Del d = new Del (C001);

d += new Del(Hack); // добавление метода в делегат

d(ref s);

Console.WriteLineC s); // результат: C001 hAcKeRs

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

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

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

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

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

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

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

• попытка вызвать делегат, в списке которого нет ни одного метода, вызывает генерацию исключения System. Null Ref erenceExcepti on.

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

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

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

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

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

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

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

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

События

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

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

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

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

• описание метода (методов), инициирующих событие.

Синтаксис события похож на синтаксис делегата:

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

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

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

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

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

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

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

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

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

Класс Thread

Поддержка много поточности осуществляется в.NE T в основном с помощью

пространства имен System.Threading. Некоторые типы этого пространства описаны в табл. 10.1.

Т а б л и ц а 1 0. 1. Некоторые типы пространства имен System.Threading

Тип Описание

Interlocked - Класс, обеспечивающий синхронизированный доступ к переменным, которые используются в разных потоках

Monitor -Класс, обеспечивающий синхронизацию доступа к объектам

Mutex-Класс-примитив синхронизации, который используется также для синхронизации между процессами

ReaderWriterLock-Класс, определяющий блокировку, поддерживающую один доступ на запись и несколько — на чтение

Thread-Класс, который создает поток, устанавливает его приоритет, получает информацию о состоянии

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

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

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

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

Делегат можно вызвать на выполнение либо синхронно, как во всех приведенных ранее примерах, либо асинхронно с помощью методов Beginlnvoke и Endlnvoke. При вызове делегата с помощью метода Beginlnvoke среда выполнения создает для исполнения метода отдельный поток и возвращает управление оператору, следующему за вызовом. При этом в исходном потоке можно продолжать вычисления.Если при вызове Beginlnvoke был указан метод обратного вызова, этот метод вызывается после завершения потока. Метод обратного вызова также задается с помощью делегата, при этом используется стандартный делегат AsyncCal 1 back. В методе, обратного вызова для получения возвращаемого значения и выходных параметров применяется метод Endlnvoke. Класс Factorizer содержит метод Factorize, выполняющий разложение на множители. Этот метод асинхронно вызывается двум я способами: в методе Numl метод обратного вызова задается в Beginlnvoke, в методе Num2 имею т место ожидание завершения потока и непосредственный вызов End Invoke.

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

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

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

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

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

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

Помимо перечисленных классов в библиотеке.NE T есть классы XmlTextReadf и XmlTextWriter, предназначенные для формирования и чтения кода в форма XML.

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

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

Класс FileStream реализует эти элементы для работы с дисковыми файлами. Для определения режимов работы с файлом используются стандартные перечисления FileMode, FileAccess и FileShare.

Текущая позиция в потоке первоначально устанавливается на начало файла (для любого режима открытия, кроме Append) и сдвигается на одну позицию при записи каждого байта. Для установки желаемой позиции чтения используется метод Seek, имеющий два параметра: первый задает смещение в байтах относительно точки отсчета, задаваемой вторым. Точки отсчета задаются константами перечисления SeekOrigin: начало файла — Begin, текущая позиция — Current и конец файла — End. В данном примере файл создавался в текущем каталоге. Можно указать и полный путь к файлу, при этом удобнее использовать дословные литералы, речь о которых шла в разделе «Литералы» например:

FileStream f = new FileStreamC @"D:\CJ\test.txt",

FileMode.Create, FileAccess.ReadWrite):

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

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

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

• Argument Except ion, если неверно задан режим открытия файла;

• IOException, если файл не открывается из-за ошибок ввода-вывода.

Возможны и другие исключительные ситуации.

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

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

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

Для удобства восприятия операции чтения из файла и диалога с пользователем оформлены в отдельный класс Demo. Метод OnCompletedRead (оператор 1) должен получать один параметр стандартного типа IAsyncResult, содержащий сведения о завершении операции, которые передаются в метод EndRead. Файл открывается в асинхронном режиме, об этом говорит значение true последнего параметра конструктора (оператор 2). В операторе 3 создается экземпляр стандартного делегата AsyncCallback, который инициализируется методом OnCompletedRead. С помощью этого делегата метод OnCompletedRead передается в метод'BeginRead (оператор 4), который создает отдельный поток, начинает асинхронный ввод и возвращает управление в вызвавший поток. Обратный вызов метода OnCompletedRead происходит при завершении операции ввода. При достаточно длинном файле verybigfile можно убедиться, что приглашение к вводу в методе Userlnput выдается раньше, чем сообщение о завершении операции ввода из метода OnCompletedRead.

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

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

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

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

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

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

Попытка просмотра сформированного программой файла в текстовом редакторе весьма медитативная, но не информативная.

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

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

Консольные приложения имеют весьма ограниченную область применения, самой распространенной из которых является обучение языку программирования. Для организации ввода и вывода используется известный вам класс Console, определенный в пространстве имен System. В этом классе определены три стандартных потока: входной поток Console. In класса TextReader и выходные потоки Consol е. Out и Console.Error класса TextWriter. По умолчанию входной поток связан с клавиатурой, а выходные — с экраном, однако можно перенаправить эти потоки на другие устройства с помощью методов Set In и SetOut или средствами операционной системы (перенаправление с помощью операций <, > и»).При обмене с консолью можно применять методы указанных потоков, но чаще используются методы класса Console — Read, ReadLine, Write и WriteLine, которые просто передают управление методам нижележащих классов In, Out и Error. Использование не одного, а двух выходных потоков полезно при желании разделить нормальный вывод программы и её сообщения об ошибках. Например, нормальный вывод программы можно перенаправить в файл, а сообщения об ошибках — на консоль или в файл журнала.

 

 



Поделиться:


Последнее изменение этой страницы: 2016-08-15; просмотров: 677; Нарушение авторского права страницы; Мы поможем в написании вашей работы!

infopedia.su Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. Обратная связь - 3.145.206.169 (0.04 с.)