Презентацию к данной лекции Вы можете скачать здесь.
Делегат — это вид класса, предназначенный для хранения ссылок на методы. Делегат, как любой класс, можно передать в качестве параметра, а затем вызвать инкапсулированный в нем метод. Делегаты используются для поддержки событий, а также как самостоятельная конструкция языка (в том числе в многопоточных приложениях).
Описание делегата задает сигнатуру методов, которые могут быть вызваны с его помощью:
[ атрибуты ] [ спецификаторы ] delegate тип имя_делегата ( [ параметры ] )
Допускаются спецификаторы new, public, protected, internal и private. Тип описывает возвращаемое значение методов, вызываемых с помощью делегата, а необязательными параметрами делегата являются параметры этих методов. Делегат может хранить ссылки на несколько методов и вызывать их поочередно; естественно, что сигнатуры всех методов должны совпадать.
Пример описания делегата:
public delegate void D ( int i );
Здесь описан тип делегата, который может хранить ссылки на методы, возвращающие void и принимающие один параметр целого типа. Объявление делегата можно размещать непосредственно в пространстве имен или внутри класса.
Чтобы воспользоваться делегатом, необходимо создать его экземпляр и задать имена методов, на которые он будет ссылаться. При вызове
Делегаты применяются в основном для следующих целей:
Рассмотрим сначала пример реализации первой из этих целей. В листинге 10.1 объявляется делегат, с помощью которого один и тот же оператор используется для вызова двух разных методов ( C00l и ).
using System;
namespace ConsoleApplication1
{
delegate void Del ( ref string s ); // объявление делегата
class Class1
{
public static void C00l ( ref string s ) // метод 1
{
string temp = "";
for ( int i = 0; i < s.Length; ++i )
{
if ( s[i] == 'o' || s[i] == 'O') temp += '0';
else if ( s[i] == 'l' ) temp += '1';
else temp += s[i];
}
s = temp;
}
public static void Hack ( ref string s ) // метод 2
{
string temp = "";
for ( int i = 0; i < s.Length; ++i )
if ( i / 2 * 2 == i ) temp += char.ToUpper( s[i] );
else temp += s[i];
s = temp;
}
static void Main()
{
string s = "cool hackers";
Del d; // экземпляр делегата
for ( int i = 0; i < 2; ++i )
{
d = new Del( C00l ); // инициализация методом 1
if ( i == 1 ) d = new Del(Hack); // инициализация методом 2
d( ref s ); // использование делегата для вызова методов
Console.WriteLine( s );
}
}
}
}
Результат работы программы:
c001 hackers C001 hAcKeRs
Использование делегата имеет тот же синтаксис, что и вызов метода. Если делегат хранит ссылки на несколько методов, они вызываются последовательно в том порядке, в котором были добавлены в делегат.
Добавление метода в список выполняется либо с помощью метода Combine, унаследованного от класса System.Delegate, либо, что удобнее, с помощью перегруженной операции сложения. Вот как выглядит измененный метод Main из предыдущего листинга, в котором одним вызовом делегата выполняется преобразование исходной строки сразу двумя методами.
static void Main()
{
string s = "cool hackers";
Del d = new Del( C00l );
d += new Del( Hack ); // добавление метода в делегат
d( ref s );
Console.WriteLine( s ); // результат: C001 hAcKeRs
}
При вызове последовательности методов с помощью делегата необходимо учитывать следующее:
out или метод возвращает значение, результатом выполнения делегата является значение, сформированное последним из методов списка (в связи с этим рекомендуется формировать списки только из делегатов, имеющих возвращаемое значение типа void );System.NullReferenceException.Рассмотрим применение делегатов для обеспечения связи между объектами по типу "источник — наблюдатель". В результате разбиения системы на множество совместно работающих классов появляется необходимость поддерживать согласованное состояние взаимосвязанных объектов. При этом желательно избежать жесткой связанности классов, так как это часто негативно сказывается на возможности многократного использования кода.
Для обеспечения связи между объектами во время выполнения программы применяется следующая стратегия. Объект, называемый источником, при изменении своего состояния, которое может представлять интерес для других объектов, посылает им уведомления. Эти объекты называются наблюдателями. Получив уведомление, наблюдатель опрашивает источник, чтобы синхронизировать с ним свое состояние. Примером такой стратегии может служить связь электронной таблицы с созданными на ее основе диаграммами.
Программисты часто используют одну и ту же схему организации и взаимодействия объектов в разных контекстах. За такими схемами закрепилось название паттерны, или шаблоны проектирования. Описанная стратегия известна под названием паттерн "наблюдатель".
Наблюдатель (observer) определяет между объектами зависимость типа "один ко многим", так что при изменении состояния одного объекта все зависящие от него объекты получают извещение и автоматически обновляются. Рассмотрим пример (листинг 10.2), в котором демонстрируется схема оповещения источником трех наблюдателей. Гипотетическое изменение состояния объекта моделируется сообщением "ОЙ!". Один из методов в демонстрационных целях сделан статическим.
using System;
namespace ConsoleApplication1
{
public delegate void Del( object o ); // объявление делегата
class Subj // класс-источник
{
Del dels; // объявление экземпляра делегата
public void Register( Del d ) // регистрация делегата
{
dels += d;
}
public void OOPS() // что-то произошло
{
Console.WriteLine( "ОЙ!" );
if ( dels != null ) dels( this ); // оповещение наблюдателей
}
}
class ObsA // класс-наблюдатель
{
public void Do( object o ) // реакция на событие источника
{
Console.WriteLine( "Бедняжка!" );
}
}
class ObsB // класс-наблюдатель
{
public static void See( object o ) // реакция на событие источника
{
Console.WriteLine( "Да ну, ерунда!" );
}
}
class Class1
{
static void Main()
{
Subj s = new Subj(); // объект класса-источника
ObsA o1 = new ObsA(); // объекты
ObsA o2 = new ObsA(); // класса-наблюдателя
s.Register( new Del( o1.Do ) ); // регистрация методов
s.Register( new Del( o2.Do ) ); // наблюдателей в источнике
s.Register( new Del( ObsB.See ) ); // ( экземпляры делегата )
s.OOPS(); // инициирование события
}
}
}
В источнике объявляется
Результат работы программы:
ОЙ! Бедняжка! Бедняжка! Да ну, ерунда!
Для обеспечения обратной связи между наблюдателем и источником делегат объявлен с параметром типа object, через который в вызываемый метод передается ссылка на вызывающий объект. Следовательно, в вызываемом методе можно получать информацию о состоянии вызывающего объекта и посылать ему сообщения.
Связь "источник — наблюдатель" устанавливается во время выполнения программы для каждого объекта по отдельности. Если наблюдатель больше не хочет получать уведомления от источника, можно удалить соответствующий метод из списка делегата с помощью метода Remove или перегруженной операции вычитания, например:
public void UnRegister( Del d ) // удаление делегата
{
dels -= d;
}
Делегаты можно сравнивать на равенство и неравенство. Два делегата равны, если они оба не содержат ссылок на методы или если они содержат ссылки на одни и те же методы в одном и том же порядке. Сравнивать можно даже делегаты различных типов при условии, что они имеют один и тот же тип возвращаемого значения и одинаковые списки параметров.
С делегатами одного типа можно выполнять операции простого и сложного присваивания, например:
Del d1 = new Del( o1.Do ); // o1.Do Del d2 = new Del( o2.Do ); // o2.Do Del d3 = d1 + d2; // o1.Do и o2.Do d3 += d1; // o1.Do, o2.Do и o1.Do d3 -= d2; // o1.Do и o1.Do
Эти операции могут понадобиться, например, в том случае, если в разных обстоятельствах требуется вызывать разные наборы и комбинации наборов методов.
Делегат, как и строка string, является неизменяемым типом данных, поэтому при любом изменении создается новый экземпляр, а старый впоследствии удаляется сборщиком мусора.
Поскольку делегат является классом, его можно передавать в методы в качестве параметра. Таким образом обеспечивается функциональная параметризация: в метод можно передавать не только различные данные, но и различные функции их обработки. Функциональная параметризация применяется для создания универсальных методов и обеспечения возможности обратного вызова.
В качестве простейшего примера универсального метода можно привести метод вывода таблицы значений функции, в который передается диапазон значений аргумента, шаг его изменения и вид вычисляемой функции. Пример приведен в листинге 10.3.
using System;
namespace ConsoleApplication1
{
public delegate double Fun( double x ); // объявление делегата
class Class1
{
public static void Table( Fun F, double x, double b )
{
Console.WriteLine( " ----- X ----- Y -----" );
while (x <= b)
{
Console.WriteLine( "| {0,8:0.000} | {1,8:0.000} |", x, F(x));
x += 1;
}
Console.WriteLine( " ---------------------" );
}
public static double Simple( double x )
{
return 1;
}
static void Main()
{
Console.WriteLine( " Таблица функции Sin " );
Table( new Fun( Math.Sin ), -2, 2 );
Console.WriteLine( " Таблица функции Simple " );
Table( new Fun( Simple ), 0, 3 );
}
}
}
Результат работы программы:
Таблица функции Sin ----- X ----- Y ----- | -2,000 | -0,909 | | -1,000 | -0,841 | | 0,000 | 0,000 | | 1,000 | 0,841 | | 2,000 | 0,909 | --------------------- Таблица функции Simple ----- X ----- Y ----- | 0,000 | 1,000 | | 1,000 | 1,000 | | 2,000 | 1,000 | | 3,000 | 1,000 | ---------------------
Обратный вызов (callback) представляет собой вызов функции, передаваемой в другую функцию в качестве параметра. Рассмотрим рисунок 10.1. Допустим, в библиотеке описана функция А, параметром которой является имя другой функции. В вызывающем коде описывается функция с требуемой сигнатурой ( В ) и передается в функцию А. Выполнение функции А приводит к вызову В, то есть управление передается из библиотечной функции обратно в вызывающий код.
(рис 10.1) Механизм обратного вызоваМеханизм обратного вызова широко используется в программировании. Например, он реализуется во многих стандартных функциях Windows.
Начиная с Visual Studio 2005, использующей версию 2.0 языка C#, можно применять упрощенный синтаксис для делегатов. Первое упрощение заключается в том, что в большинстве случаев явным образом создавать
static void Main()
{
Console.WriteLine( " Таблица функции Sin " );
Table( Math.Sin, -2, 2 ); // упрощение 1
Console.WriteLine( " Таблица функции Simple " );
Table( delegate (double x ){ return 1; }, 0, 3 ); // упрощение 2
}
}
}
В первом случае Sin, создается автоматически. Чтобы это могло произойти, список параметров и тип возвращаемого значения функции должны быть совместимы с делегатом. Во втором случае не требуется оформлять простой фрагмент кода в виде отдельной функции Simple, как это было сделано в предыдущем листинге, — код функции оформляется как анонимный метод и встраивается прямо в место передачи.
Событие — это элемент класса, позволяющий ему посылать другим объектам уведомления об изменении своего состояния. При этом для объектов, являющихся наблюдателями события, активизируются методы-обработчики этого события. Обработчики должны быть зарегистрированы в объекте-источнике события. Таким образом, механизм событий формализует на языковом уровне паттерн "наблюдатель", который рассматривался в предыдущем разделе.
Механизм событий можно также описать с помощью модели "публикация — подписка": один класс, являющийся отправителем (sender) сообщения, публикует события, которые он может инициировать, а другие классы, являющиеся получателями (receivers) сообщения, подписываются на получение этих событий.
События построены на основе делегатов: с помощью делегатов вызываются методы-обработчики событий. Поэтому создание события в классе состоит из следующих частей:
Синтаксис события похож на синтаксис делегата:
[ атрибуты ] [ спецификаторы ] event тип имя_события
Для событий применяются спецификаторы new, public, protected, internal, private, static, virtual, sealed, override, abstract и extern. Например, так же как и методы, событие может быть статическим ( static ), тогда оно связано с классом в целом, или обычным — в этом случае оно связано с экземпляром класса. Тип события — это тип делегата, на котором основано событие.
Пример описания делегата и соответствующего ему события:
public delegate void Del( object o ); // объявление делегата
class A
{
public event Del Oops; // объявление события
...
}
Обработка событий выполняется в классах-получателях сообщения. Для этого в них описываются методы-обработчики событий, сигнатура которых соответствует типу делегата. Каждый объект (не класс!), желающий получать сообщение, должен зарегистрировать в объекте-отправителе этот метод.
Как видите, это в точности тот же самый механизм, который рассматривался в предыдущем разделе. Единственное отличие состоит в том, что при использовании событий не требуется описывать метод, регистрирующий обработчики, поскольку события поддерживают операции += и -=, добавляющие обработчик в список и удаляющие его из списка.
В листинге 10.4 приведен код из листинга 10.2, переработанный с использованием событий, а рисунок 10.2 поясняет организацию обработки событий.

(рис 10.4) Выполнение программы с двумя нулевыми коэффициентами(рис 10.2) Оповещение наблюдателей с помощью событийusing System; namespace ConsoleApplication1 { public delegate void Del(); // объявление делегата class Subj // класс-источник { public event Del Oops; // объявление события public void CryOops() // метод, инициирующий событие { Console.WriteLine( "ОЙ!" ); if ( Oops != null ) Oops(); } } class ObsA // класс-наблюдатель { public void Do(); // реакция на событие источника { Console.WriteLine( "Бедняжка!" ); } } class ObsB // класс-наблюдатель { public static void See() // реакция на событие источника { Console.WriteLine( "Да ну, ерунда!" ); } } class Class1 { static void Main() { Subj s = new Subj(); // объект класса-источника ObsA o1 = new ObsA(); // объекты ObsA o2 = new ObsA(); // класса-наблюдателя s.Oops += new Del( o1.Do ); // добавление s.Oops += new Del( o2.Do ); // обработчиков s.Oops += new Del( ObsB.See ); // к событию s.CryOops(); // инициирование события } } }
Внешний код может работать с событиями единственным образом: добавлять обработчики в список или удалять их, поскольку вне класса могут использоваться только операции += и -=. Тип результата этих операций — void, в отличие от операций сложного присваивания для арифметических типов.
Внутри класса, в котором описано событие, с ним можно обращаться, как с обычным полем, имеющим тип делегата: использовать операции отношения, присваивания и т. д. Значение события по умолчанию — null.
В библиотеке .NET описано огромное количество стандартных делегатов, предназначенных для реализации механизма обработки событий. Большинство этих классов оформлено по одним и тем же правилам:
EventHandler ;object ;EventArgs или производный от него.Если обработчикам события требуется специфическая информация о событии, то для этого создают класс, производный от стандартного класса EventArgs, и добавляют в него необходимую информацию. Если делегат не использует такую информацию, можно обойтись стандартным классом делегата System.EventHandler.
Имя обработчика события принято составлять из префикса On и имени события. В листинге 10.5 приведен пример из листинга 10.4, оформленный в соответствии со стандартными соглашениями .NET.
using System;
namespace ConsoleApplication1
{
class Subj
{
public event EventHandler Oops;
public void CryOops()
{
Console.WriteLine( "ОЙ!" );
if ( Oops != null ) Oops( this, null );
}
}
class ObsA
{
public void OnOops( object sender, EventArgs e )
{
Console.WriteLine( "Бедняжка!" );
}
}
class ObsB
{
public static void OnOops( object sender, EventArgs e )
{
Console.WriteLine( "Да ну, ерунда!" );
}
}
class Class1
{ static void Main()
{
Subj s = new Subj();
ObsA o1 = new ObsA();
ObsA o2 = new ObsA();
s.Oops += new EventHandler( o1.OnOops );
s.Oops += new EventHandler( o2.OnOops );
s.Oops += new EventHandler( ObsB.OnOops );
s.CryOops();
}
}
}
Те, кто работает с C# версии начиная с 2.0, могут упростить эту программу, используя возможность неявного создания делегатов при регистрации обработчиков событий. Об этом можно прочитать в учебнике [4].
Приложение .NET состоит из одного или нескольких процессов. Процессу принадлежат выделенная для него область оперативной памяти и ресурсы. Каждый процесс может состоять из нескольких доменов (частей) приложения, ресурсы которых изолированы друг от друга. В рамках домена может быть запущено несколько потоков выполнения. Поток (thread) представляет собой часть исполняемого кода программы. Иногда термин "thread" переводится буквально — "нить", чтобы отличить его от потоков ввода-вывода. Поэтому в литературе можно встретить и термин "многонитевые приложения".
В каждом процессе есть первичный поток, исполняющий роль точки входа в приложение. Для консольных приложений это метод Main.
Многопоточные приложения создают как для многопроцессорных, так и для однопроцессорных систем. Основной целью при этом являются повышение общей производительности и сокращение времени реакции приложения. Управление потоками осуществляет операционная система. Каждый поток получает некоторое количество квантов времени, по истечении которого управление передается другому потоку. Это создает у пользователя однопроцессорной машины впечатление одновременной работы нескольких потоков и позволяет, к примеру, выполнять ввод текста одновременно с длительной операцией по передаче данных.
Недостатки многопоточности:
В .NET многопоточность поддерживается в основном с помощью пространства имен System.Threading. Некоторые типы этого пространства описаны в таблице 10.1.
| Тип | Описание |
|---|---|
Monitor |
Класс, обеспечивающий синхронизацию доступа к объектам |
|
Класс-примитив синхронизации, который используется также для синхронизации между процессами |
Thread |
Класс, который создает поток, устанавливает его приоритет, получает информацию о состоянии |
ThreadPool |
Класс, используемый для управления набором взаимосвязанных потоков — пулом потоков |
Timer |
Класс, определяющий механизм вызова заданного метода в заданные интервалы времени для пула потоков |
WaitHandle |
Класс, инкапсулирующий объекты синхронизации, которые ожидают доступа к разделяемым ресурсам |
ThreadStart |
Делегат, представляющий метод, который должен быть выполнен при запуске потока |
TimerCallback |
Делегат, представляющий метод, обрабатывающий вызовы от класса Timer |
WaitCallback |
Делегат, представляющий метод для элементов класса ThreadPool |
ThreadPriority |
Перечисление, описывающее |
ThreadState |
Перечисление, описывающее состояние потока |
Первичный поток создается автоматически. Для запуска вторичных потоков используется класс Thread. При создании объекта-потока ему передается делегат, определяющий метод, выполнение которого выделяется в отдельный поток:
Thread t = new Thread ( new ThreadStart( имя_метода ) );
После создания потока заданный метод начинает в нем свою работу, а первичный поток продолжает выполняться. В листинге 10.6 приведен пример одновременной работы двух потоков.
using System;
using System.Threading;
namespace ConsoleApplication1
{ class Program
{
static public void Hedgehog() // метод для вторичного потока
{
for ( int i = 0; i < 6; ++i )
{
Console.Write(" " + i ); Thread.Sleep( 1000 );
}
}
static void Main()
{
Console.WriteLine( "Первичный поток " +
Thread.CurrentThread.GetHashCode() );
Thread ta = new Thread( new ThreadStart(Hedgehog) );
Console.WriteLine( "Вторичный поток " + ta.GetHashCode() );
ta.Start();
for ( int i = 0; i > -6; --i )
{
Console.Write( " " + i ); Thread.Sleep( 400 );
}
}
}
}
Результат работы программы:
Первичный поток 1 Вторичный поток 2 0 0 -1 -2 1 -3 -4 2 -5 3 4 5
В листинге используется метод Sleep, останавливающий функционирование потока на заданное количество миллисекунд. Как видите, оба потока работают одновременно. Если бы они работали с одним и тем же файлом, он был бы испорчен так же, как и приведенный вывод на консоль, поэтому такой способ распараллеливания вычислений имеет смысл только для работы с различными ресурсами.
В таблице 10.2 перечислены основные элементы класса Thread.
| Элемент | Вид | Описание |
|---|---|---|
CurrentThread |
Статическое свойство | Возвращает ссылку на выполняющийся поток (только для чтения) |
Name |
Свойство | Установка текстового имени потока |
Priority |
Свойство | Получить/установить ThreadPriority ) |
ThreadState |
Свойство | Возвращает состояние потока (используются значения перечисления ThreadState ) |
Abort |
Метод | Генерирует исключение ThreadAbortException. Вызов этого метода обычно завершает работу потока |
Sleep |
Статический метод | Приостанавливает выполнение текущего потока на заданное количество миллисекунд |
Interrupt |
Метод | Прерывает работу текущего потока |
Join |
Метод | Блокирует вызывающий поток до завершения другого потока или указанного промежутка времени и завершает поток |
Resume |
Метод | Возобновляет работу после приостановки потока |
Start |
Метод | Начинает выполнение потока, определенного делегатом ThreadStart |
Suspend |
Метод | Приостанавливает выполнение потока. Если выполнение потока уже приостановлено, то игнорируется |
Можно создать несколько потоков, которые будут совместно использовать один и тот же код. Пример приведен в листинге 10.7.
using System;
using System.Threading;
namespace ConsoleApplication1
{
class Class1
{ public void Do()
{
for ( int i = 0; i < 4; ++i )
{ Console.Write( " " + i ); Thread.Sleep( 3 ); }
}
}
class Program
{ static void Main()
{
Class1 a = new Class1();
Thread t1 = new Thread( new ThreadStart( a.Do ) );
t1.Name = "Second";
Console.WriteLine( "Поток " + t1.Name );
t1.Start();
Thread t2 = new Thread( new ThreadStart( a.Do ) );
t2.Name = "Third";
Console.WriteLine( "Поток " + t2.Name );
t2.Start();
}
}
}
Результат работы программы:
Поток Second Поток Third 0 0 1 1 2 2 3 3
Варианты вывода могут несколько различаться, поскольку один поток прерывает выполнение другого в неизвестные моменты времени.
Для того чтобы блок кода мог использоваться в каждый момент только одним потоком, применяется оператор lock. Формат оператора:
lock ( выражение ) блок_операторов
Выражение определяет объект, который требуется заблокировать. Для обычных методов в качестве выражения используется ключевое слово this, для статических — typeof( класс ). Блок операторов задает критическую секцию кода, которую требуется заблокировать.
Например, блокировка операторов в приведенном ранее методе Do выглядит следующим образом:
public void Do()
{
lock( this )
{
for ( int i = 0; i < 4; ++i )
{ Console.Write( " " + i ); Thread.Sleep( 30 ); }
}
}
Для такого варианта метода результат работы программы изменится:
Поток Second Поток Third 0 1 2 3 0 1 2 3
Делегат можно вызвать на выполнение либо синхронно, как во всех приведенных ранее примерах, либо асинхронно с помощью методов BeginInvoke и EndInvoke.
При вызове делегата с помощью метода BeginInvoke среда выполнения создает для исполнения метода отдельный поток и возвращает управление оператору, следующему за вызовом. При этом в исходном потоке можно продолжать вычисления.
Если при вызове BeginInvoke был указан метод обратного вызова, этот метод вызывается после завершения потока. Метод обратного вызова также задается с помощью делегата, при этом используется стандартный делегат AsyncCallback. В методе обратного вызова для получения возвращаемого значения и выходных параметров применяется метод EndInvoke.
Если метод обратного вызова не был указан в параметрах метода BeginInvoke, метод EndInvoke можно использовать в потоке, инициировавшем запрос.
В листинге 10.8 приводится два примера асинхронного вызова метода, выполняющего разложение числа на множители. Листинг приводится по документации Visual Studio с некоторыми изменениями.
Класс Factorizer содержит метод Factorize, выполняющий разложение на множители. Этот метод асинхронно вызывается двумя способами: в методе Num1 метод обратного вызова задается в BeginInvoke, в методе Num2 имеют место ожидание завершения потока и непосредственный вызов EndInvoke.
using System;
using System.Threading;
using System.Runtime.Remoting.Messaging;
// асинхронный делегат
public delegate bool AsyncDelegate ( int Num, out int m1, out int m2 );
// класс, выполняющий разложение числа на множители
public class Factorizer
{
public bool Factorize( int Num, out int m1, out int m2 )
{
m1 = 1; m2 = Num;
for ( int i = 2; i < Num; i++ )
if ( 0 == (Num % i) ) { m1 = i; m2 = Num / i; break; }
if (1 == m1 ) return false;
else return true;
}
}
// класс, получающий делегат и результаты
public class PNum
{
private int Number;
public PNum( int number ) { Number = number; }
[OneWayAttribute()]
// метод, получающий результаты
public void Res( IAsyncResult ar )
{
int m1, m2;
// получение делегата из AsyncResult
AsyncDelegate ad = (AsyncDelegate)((AsyncResult)ar).AsyncDelegate;
// получение результатов выполнения метода Factorize
ad.EndInvoke( out m1, out m2, ar );
// вывод результатов
Console.WriteLine( "Первый способ : множители {0} : {1} {2}",
Number, m1, m2 );
}
}
// демонстрационный класс
public class Simple
{
// способ 1: используется функция обратного вызова
public void Num1()
{
Factorizer f = new Factorizer();
AsyncDelegate ad = new AsyncDelegate ( f.Factorize );
int Num = 1000589023, tmp;
// создание экземпляра класса, который будет вызван
// после завершения работы метода Factorize
PNum n = new PNum( Num );
// задание делегата метода обратного вызова
AsyncCallback callback = new AsyncCallback( n.Res );
// асинхронный вызов метода Factorize
IAsyncResult ar = ad.BeginInvoke(
Num, out tmp, out tmp, callback, null );
//
// здесь - выполнение неких дальнейших действий
// ...
}
// способ 2: используется ожидание окончания выполнения
public void Num2()
{
Factorizer f = new Factorizer();
AsyncDelegate ad = new AsyncDelegate ( f.Factorize );
int Num = 1000589023, tmp;
// создание экземпляра класса, который будет вызван
// после завершения работы метода Factorize
PNum n = new PNum( Num );
// задание делегата метода обратного вызова
AsyncCallback callback = new AsyncCallback( n.Res );
// асинхронный вызов метода Factorize
IAsyncResult ar = ad.BeginInvoke(
Num, out tmp, out tmp, null, null );
// ожидание завершения
ar.AsyncWaitHandle.WaitOne( 10000, false );
if ( ar.IsCompleted )
{
int m1, m2;
// получение результатов выполнения метода Factorize
ad.EndInvoke( out m1, out m2, ar );
// вывод результатов
Console.WriteLine( "Второй способ : множители {0} : {1} {2}",
Num, m1, m2 );
}
}
public static void Main()
{
Simple s = new Simple();
s.Num1();
s.Num2();
}
}
Результат работы программы:
Первый способ : множители 1000589023 : 7 142941289 Второй способ : множители 1000589023 : 7 142941289
Примечание
Атрибут [OneWayAttribute()] помечает метод как не имеющий возвращаемого значения и выходных параметров.
На этом данный курс лекций завершается. В него не вошли многие темы, рассмотренные в учебнике [4]: структуры, перечисления, работа с файлами, регулярные выражения, сборки, атрибуты, небезопасный код, основы программирования под Windows. Это связано с формулировкой договора, заключенного автором с издательством ПИТЕР при публикации книги.
Презентацию к данной лекции Вы можете скачать здесь.
Делегат — это вид класса, предназначенный для хранения ссылок на методы. Делегат, как любой класс, можно передать в качестве параметра, а затем вызвать инкапсулированный в нем метод. Делегаты используются для поддержки событий, а также как самостоятельная конструкция языка (в том числе в многопоточных приложениях).
Описание делегата задает сигнатуру методов, которые могут быть вызваны с его помощью:
[ атрибуты ] [ спецификаторы ] delegate тип имя_делегата ( [ параметры ] )
Допускаются спецификаторы new, public, protected, internal и private. Тип описывает возвращаемое значение методов, вызываемых с помощью делегата, а необязательными параметрами делегата являются параметры этих методов. Делегат может хранить ссылки на несколько методов и вызывать их поочередно; естественно, что сигнатуры всех методов должны совпадать.
Пример описания делегата:
public delegate void D ( int i );
Здесь описан тип делегата, который может хранить ссылки на методы, возвращающие void и принимающие один параметр целого типа. Объявление делегата можно размещать непосредственно в пространстве имен или внутри класса.
Чтобы воспользоваться делегатом, необходимо создать его экземпляр и задать имена методов, на которые он будет ссылаться. При вызове
Делегаты применяются в основном для следующих целей:
Рассмотрим сначала пример реализации первой из этих целей. В листинге 10.1 объявляется делегат, с помощью которого один и тот же оператор используется для вызова двух разных методов ( C00l и ).
using System;
namespace ConsoleApplication1
{
delegate void Del ( ref string s ); // объявление делегата
class Class1
{
public static void C00l ( ref string s ) // метод 1
{
string temp = "";
for ( int i = 0; i < s.Length; ++i )
{
if ( s[i] == 'o' || s[i] == 'O') temp += '0';
else if ( s[i] == 'l' ) temp += '1';
else temp += s[i];
}
s = temp;
}
public static void Hack ( ref string s ) // метод 2
{
string temp = "";
for ( int i = 0; i < s.Length; ++i )
if ( i / 2 * 2 == i ) temp += char.ToUpper( s[i] );
else temp += s[i];
s = temp;
}
static void Main()
{
string s = "cool hackers";
Del d; // экземпляр делегата
for ( int i = 0; i < 2; ++i )
{
d = new Del( C00l ); // инициализация методом 1
if ( i == 1 ) d = new Del(Hack); // инициализация методом 2
d( ref s ); // использование делегата для вызова методов
Console.WriteLine( s );
}
}
}
}
Результат работы программы:
c001 hackers C001 hAcKeRs
Использование делегата имеет тот же синтаксис, что и вызов метода. Если делегат хранит ссылки на несколько методов, они вызываются последовательно в том порядке, в котором были добавлены в делегат.
Добавление метода в список выполняется либо с помощью метода Combine, унаследованного от класса System.Delegate, либо, что удобнее, с помощью перегруженной операции сложения. Вот как выглядит измененный метод Main из предыдущего листинга, в котором одним вызовом делегата выполняется преобразование исходной строки сразу двумя методами.
static void Main()
{
string s = "cool hackers";
Del d = new Del( C00l );
d += new Del( Hack ); // добавление метода в делегат
d( ref s );
Console.WriteLine( s ); // результат: C001 hAcKeRs
}
При вызове последовательности методов с помощью делегата необходимо учитывать следующее:
out или метод возвращает значение, результатом выполнения делегата является значение, сформированное последним из методов списка (в связи с этим рекомендуется формировать списки только из делегатов, имеющих возвращаемое значение типа void );System.NullReferenceException.Рассмотрим применение делегатов для обеспечения связи между объектами по типу "источник — наблюдатель". В результате разбиения системы на множество совместно работающих классов появляется необходимость поддерживать согласованное состояние взаимосвязанных объектов. При этом желательно избежать жесткой связанности классов, так как это часто негативно сказывается на возможности многократного использования кода.
Для обеспечения связи между объектами во время выполнения программы применяется следующая стратегия. Объект, называемый источником, при изменении своего состояния, которое может представлять интерес для других объектов, посылает им уведомления. Эти объекты называются наблюдателями. Получив уведомление, наблюдатель опрашивает источник, чтобы синхронизировать с ним свое состояние. Примером такой стратегии может служить связь электронной таблицы с созданными на ее основе диаграммами.
Программисты часто используют одну и ту же схему организации и взаимодействия объектов в разных контекстах. За такими схемами закрепилось название паттерны, или шаблоны проектирования. Описанная стратегия известна под названием паттерн "наблюдатель".
Наблюдатель (observer) определяет между объектами зависимость типа "один ко многим", так что при изменении состояния одного объекта все зависящие от него объекты получают извещение и автоматически обновляются. Рассмотрим пример (листинг 10.2), в котором демонстрируется схема оповещения источником трех наблюдателей. Гипотетическое изменение состояния объекта моделируется сообщением "ОЙ!". Один из методов в демонстрационных целях сделан статическим.
using System;
namespace ConsoleApplication1
{
public delegate void Del( object o ); // объявление делегата
class Subj // класс-источник
{
Del dels; // объявление экземпляра делегата
public void Register( Del d ) // регистрация делегата
{
dels += d;
}
public void OOPS() // что-то произошло
{
Console.WriteLine( "ОЙ!" );
if ( dels != null ) dels( this ); // оповещение наблюдателей
}
}
class ObsA // класс-наблюдатель
{
public void Do( object o ) // реакция на событие источника
{
Console.WriteLine( "Бедняжка!" );
}
}
class ObsB // класс-наблюдатель
{
public static void See( object o ) // реакция на событие источника
{
Console.WriteLine( "Да ну, ерунда!" );
}
}
class Class1
{
static void Main()
{
Subj s = new Subj(); // объект класса-источника
ObsA o1 = new ObsA(); // объекты
ObsA o2 = new ObsA(); // класса-наблюдателя
s.Register( new Del( o1.Do ) ); // регистрация методов
s.Register( new Del( o2.Do ) ); // наблюдателей в источнике
s.Register( new Del( ObsB.See ) ); // ( экземпляры делегата )
s.OOPS(); // инициирование события
}
}
}
В источнике объявляется
Результат работы программы:
ОЙ! Бедняжка! Бедняжка! Да ну, ерунда!
Для обеспечения обратной связи между наблюдателем и источником делегат объявлен с параметром типа object, через который в вызываемый метод передается ссылка на вызывающий объект. Следовательно, в вызываемом методе можно получать информацию о состоянии вызывающего объекта и посылать ему сообщения.
Связь "источник — наблюдатель" устанавливается во время выполнения программы для каждого объекта по отдельности. Если наблюдатель больше не хочет получать уведомления от источника, можно удалить соответствующий метод из списка делегата с помощью метода Remove или перегруженной операции вычитания, например:
public void UnRegister( Del d ) // удаление делегата
{
dels -= d;
}
Делегаты можно сравнивать на равенство и неравенство. Два делегата равны, если они оба не содержат ссылок на методы или если они содержат ссылки на одни и те же методы в одном и том же порядке. Сравнивать можно даже делегаты различных типов при условии, что они имеют один и тот же тип возвращаемого значения и одинаковые списки параметров.
С делегатами одного типа можно выполнять операции простого и сложного присваивания, например:
Del d1 = new Del( o1.Do ); // o1.Do Del d2 = new Del( o2.Do ); // o2.Do Del d3 = d1 + d2; // o1.Do и o2.Do d3 += d1; // o1.Do, o2.Do и o1.Do d3 -= d2; // o1.Do и o1.Do
Эти операции могут понадобиться, например, в том случае, если в разных обстоятельствах требуется вызывать разные наборы и комбинации наборов методов.
Делегат, как и строка string, является неизменяемым типом данных, поэтому при любом изменении создается новый экземпляр, а старый впоследствии удаляется сборщиком мусора.
Поскольку делегат является классом, его можно передавать в методы в качестве параметра. Таким образом обеспечивается функциональная параметризация: в метод можно передавать не только различные данные, но и различные функции их обработки. Функциональная параметризация применяется для создания универсальных методов и обеспечения возможности обратного вызова.
В качестве простейшего примера универсального метода можно привести метод вывода таблицы значений функции, в который передается диапазон значений аргумента, шаг его изменения и вид вычисляемой функции. Пример приведен в листинге 10.3.
using System;
namespace ConsoleApplication1
{
public delegate double Fun( double x ); // объявление делегата
class Class1
{
public static void Table( Fun F, double x, double b )
{
Console.WriteLine( " ----- X ----- Y -----" );
while (x <= b)
{
Console.WriteLine( "| {0,8:0.000} | {1,8:0.000} |", x, F(x));
x += 1;
}
Console.WriteLine( " ---------------------" );
}
public static double Simple( double x )
{
return 1;
}
static void Main()
{
Console.WriteLine( " Таблица функции Sin " );
Table( new Fun( Math.Sin ), -2, 2 );
Console.WriteLine( " Таблица функции Simple " );
Table( new Fun( Simple ), 0, 3 );
}
}
}
Результат работы программы:
Таблица функции Sin ----- X ----- Y ----- | -2,000 | -0,909 | | -1,000 | -0,841 | | 0,000 | 0,000 | | 1,000 | 0,841 | | 2,000 | 0,909 | --------------------- Таблица функции Simple ----- X ----- Y ----- | 0,000 | 1,000 | | 1,000 | 1,000 | | 2,000 | 1,000 | | 3,000 | 1,000 | ---------------------
Обратный вызов (callback) представляет собой вызов функции, передаваемой в другую функцию в качестве параметра. Рассмотрим рисунок 10.1. Допустим, в библиотеке описана функция А, параметром которой является имя другой функции. В вызывающем коде описывается функция с требуемой сигнатурой ( В ) и передается в функцию А. Выполнение функции А приводит к вызову В, то есть управление передается из библиотечной функции обратно в вызывающий код.
(рис 10.1) Механизм обратного вызоваМеханизм обратного вызова широко используется в программировании. Например, он реализуется во многих стандартных функциях Windows.
Начиная с Visual Studio 2005, использующей версию 2.0 языка C#, можно применять упрощенный синтаксис для делегатов. Первое упрощение заключается в том, что в большинстве случаев явным образом создавать
static void Main()
{
Console.WriteLine( " Таблица функции Sin " );
Table( Math.Sin, -2, 2 ); // упрощение 1
Console.WriteLine( " Таблица функции Simple " );
Table( delegate (double x ){ return 1; }, 0, 3 ); // упрощение 2
}
}
}
В первом случае Sin, создается автоматически. Чтобы это могло произойти, список параметров и тип возвращаемого значения функции должны быть совместимы с делегатом. Во втором случае не требуется оформлять простой фрагмент кода в виде отдельной функции Simple, как это было сделано в предыдущем листинге, — код функции оформляется как анонимный метод и встраивается прямо в место передачи.
Событие — это элемент класса, позволяющий ему посылать другим объектам уведомления об изменении своего состояния. При этом для объектов, являющихся наблюдателями события, активизируются методы-обработчики этого события. Обработчики должны быть зарегистрированы в объекте-источнике события. Таким образом, механизм событий формализует на языковом уровне паттерн "наблюдатель", который рассматривался в предыдущем разделе.
Механизм событий можно также описать с помощью модели "публикация — подписка": один класс, являющийся отправителем (sender) сообщения, публикует события, которые он может инициировать, а другие классы, являющиеся получателями (receivers) сообщения, подписываются на получение этих событий.
События построены на основе делегатов: с помощью делегатов вызываются методы-обработчики событий. Поэтому создание события в классе состоит из следующих частей:
Синтаксис события похож на синтаксис делегата:
[ атрибуты ] [ спецификаторы ] event тип имя_события
Для событий применяются спецификаторы new, public, protected, internal, private, static, virtual, sealed, override, abstract и extern. Например, так же как и методы, событие может быть статическим ( static ), тогда оно связано с классом в целом, или обычным — в этом случае оно связано с экземпляром класса. Тип события — это тип делегата, на котором основано событие.
Пример описания делегата и соответствующего ему события:
public delegate void Del( object o ); // объявление делегата
class A
{
public event Del Oops; // объявление события
...
}
Обработка событий выполняется в классах-получателях сообщения. Для этого в них описываются методы-обработчики событий, сигнатура которых соответствует типу делегата. Каждый объект (не класс!), желающий получать сообщение, должен зарегистрировать в объекте-отправителе этот метод.
Как видите, это в точности тот же самый механизм, который рассматривался в предыдущем разделе. Единственное отличие состоит в том, что при использовании событий не требуется описывать метод, регистрирующий обработчики, поскольку события поддерживают операции += и -=, добавляющие обработчик в список и удаляющие его из списка.
В листинге 10.4 приведен код из листинга 10.2, переработанный с использованием событий, а рисунок 10.2 поясняет организацию обработки событий.

(рис 10.4) Выполнение программы с двумя нулевыми коэффициентами(рис 10.2) Оповещение наблюдателей с помощью событийusing System; namespace ConsoleApplication1 { public delegate void Del(); // объявление делегата class Subj // класс-источник { public event Del Oops; // объявление события public void CryOops() // метод, инициирующий событие { Console.WriteLine( "ОЙ!" ); if ( Oops != null ) Oops(); } } class ObsA // класс-наблюдатель { public void Do(); // реакция на событие источника { Console.WriteLine( "Бедняжка!" ); } } class ObsB // класс-наблюдатель { public static void See() // реакция на событие источника { Console.WriteLine( "Да ну, ерунда!" ); } } class Class1 { static void Main() { Subj s = new Subj(); // объект класса-источника ObsA o1 = new ObsA(); // объекты ObsA o2 = new ObsA(); // класса-наблюдателя s.Oops += new Del( o1.Do ); // добавление s.Oops += new Del( o2.Do ); // обработчиков s.Oops += new Del( ObsB.See ); // к событию s.CryOops(); // инициирование события } } }
Внешний код может работать с событиями единственным образом: добавлять обработчики в список или удалять их, поскольку вне класса могут использоваться только операции += и -=. Тип результата этих операций — void, в отличие от операций сложного присваивания для арифметических типов.
Внутри класса, в котором описано событие, с ним можно обращаться, как с обычным полем, имеющим тип делегата: использовать операции отношения, присваивания и т. д. Значение события по умолчанию — null.
В библиотеке .NET описано огромное количество стандартных делегатов, предназначенных для реализации механизма обработки событий. Большинство этих классов оформлено по одним и тем же правилам:
EventHandler ;object ;EventArgs или производный от него.Если обработчикам события требуется специфическая информация о событии, то для этого создают класс, производный от стандартного класса EventArgs, и добавляют в него необходимую информацию. Если делегат не использует такую информацию, можно обойтись стандартным классом делегата System.EventHandler.
Имя обработчика события принято составлять из префикса On и имени события. В листинге 10.5 приведен пример из листинга 10.4, оформленный в соответствии со стандартными соглашениями .NET.
using System;
namespace ConsoleApplication1
{
class Subj
{
public event EventHandler Oops;
public void CryOops()
{
Console.WriteLine( "ОЙ!" );
if ( Oops != null ) Oops( this, null );
}
}
class ObsA
{
public void OnOops( object sender, EventArgs e )
{
Console.WriteLine( "Бедняжка!" );
}
}
class ObsB
{
public static void OnOops( object sender, EventArgs e )
{
Console.WriteLine( "Да ну, ерунда!" );
}
}
class Class1
{ static void Main()
{
Subj s = new Subj();
ObsA o1 = new ObsA();
ObsA o2 = new ObsA();
s.Oops += new EventHandler( o1.OnOops );
s.Oops += new EventHandler( o2.OnOops );
s.Oops += new EventHandler( ObsB.OnOops );
s.CryOops();
}
}
}
Те, кто работает с C# версии начиная с 2.0, могут упростить эту программу, используя возможность неявного создания делегатов при регистрации обработчиков событий. Об этом можно прочитать в учебнике [4].
Приложение .NET состоит из одного или нескольких процессов. Процессу принадлежат выделенная для него область оперативной памяти и ресурсы. Каждый процесс может состоять из нескольких доменов (частей) приложения, ресурсы которых изолированы друг от друга. В рамках домена может быть запущено несколько потоков выполнения. Поток (thread) представляет собой часть исполняемого кода программы. Иногда термин "thread" переводится буквально — "нить", чтобы отличить его от потоков ввода-вывода. Поэтому в литературе можно встретить и термин "многонитевые приложения".
В каждом процессе есть первичный поток, исполняющий роль точки входа в приложение. Для консольных приложений это метод Main.
Многопоточные приложения создают как для многопроцессорных, так и для однопроцессорных систем. Основной целью при этом являются повышение общей производительности и сокращение времени реакции приложения. Управление потоками осуществляет операционная система. Каждый поток получает некоторое количество квантов времени, по истечении которого управление передается другому потоку. Это создает у пользователя однопроцессорной машины впечатление одновременной работы нескольких потоков и позволяет, к примеру, выполнять ввод текста одновременно с длительной операцией по передаче данных.
Недостатки многопоточности:
В .NET многопоточность поддерживается в основном с помощью пространства имен System.Threading. Некоторые типы этого пространства описаны в таблице 10.1.
| Тип | Описание |
|---|---|
Monitor |
Класс, обеспечивающий синхронизацию доступа к объектам |
|
Класс-примитив синхронизации, который используется также для синхронизации между процессами |
Thread |
Класс, который создает поток, устанавливает его приоритет, получает информацию о состоянии |
ThreadPool |
Класс, используемый для управления набором взаимосвязанных потоков — пулом потоков |
Timer |
Класс, определяющий механизм вызова заданного метода в заданные интервалы времени для пула потоков |
WaitHandle |
Класс, инкапсулирующий объекты синхронизации, которые ожидают доступа к разделяемым ресурсам |
ThreadStart |
Делегат, представляющий метод, который должен быть выполнен при запуске потока |
TimerCallback |
Делегат, представляющий метод, обрабатывающий вызовы от класса Timer |
WaitCallback |
Делегат, представляющий метод для элементов класса ThreadPool |
ThreadPriority |
Перечисление, описывающее |
ThreadState |
Перечисление, описывающее состояние потока |
Первичный поток создается автоматически. Для запуска вторичных потоков используется класс Thread. При создании объекта-потока ему передается делегат, определяющий метод, выполнение которого выделяется в отдельный поток:
Thread t = new Thread ( new ThreadStart( имя_метода ) );
После создания потока заданный метод начинает в нем свою работу, а первичный поток продолжает выполняться. В листинге 10.6 приведен пример одновременной работы двух потоков.
using System;
using System.Threading;
namespace ConsoleApplication1
{ class Program
{
static public void Hedgehog() // метод для вторичного потока
{
for ( int i = 0; i < 6; ++i )
{
Console.Write(" " + i ); Thread.Sleep( 1000 );
}
}
static void Main()
{
Console.WriteLine( "Первичный поток " +
Thread.CurrentThread.GetHashCode() );
Thread ta = new Thread( new ThreadStart(Hedgehog) );
Console.WriteLine( "Вторичный поток " + ta.GetHashCode() );
ta.Start();
for ( int i = 0; i > -6; --i )
{
Console.Write( " " + i ); Thread.Sleep( 400 );
}
}
}
}
Результат работы программы:
Первичный поток 1 Вторичный поток 2 0 0 -1 -2 1 -3 -4 2 -5 3 4 5
В листинге используется метод Sleep, останавливающий функционирование потока на заданное количество миллисекунд. Как видите, оба потока работают одновременно. Если бы они работали с одним и тем же файлом, он был бы испорчен так же, как и приведенный вывод на консоль, поэтому такой способ распараллеливания вычислений имеет смысл только для работы с различными ресурсами.
В таблице 10.2 перечислены основные элементы класса Thread.
| Элемент | Вид | Описание |
|---|---|---|
CurrentThread |
Статическое свойство | Возвращает ссылку на выполняющийся поток (только для чтения) |
Name |
Свойство | Установка текстового имени потока |
Priority |
Свойство | Получить/установить ThreadPriority ) |
ThreadState |
Свойство | Возвращает состояние потока (используются значения перечисления ThreadState ) |
Abort |
Метод | Генерирует исключение ThreadAbortException. Вызов этого метода обычно завершает работу потока |
Sleep |
Статический метод | Приостанавливает выполнение текущего потока на заданное количество миллисекунд |
Interrupt |
Метод | Прерывает работу текущего потока |
Join |
Метод | Блокирует вызывающий поток до завершения другого потока или указанного промежутка времени и завершает поток |
Resume |
Метод | Возобновляет работу после приостановки потока |
Start |
Метод | Начинает выполнение потока, определенного делегатом ThreadStart |
Suspend |
Метод | Приостанавливает выполнение потока. Если выполнение потока уже приостановлено, то игнорируется |
Можно создать несколько потоков, которые будут совместно использовать один и тот же код. Пример приведен в листинге 10.7.
using System;
using System.Threading;
namespace ConsoleApplication1
{
class Class1
{ public void Do()
{
for ( int i = 0; i < 4; ++i )
{ Console.Write( " " + i ); Thread.Sleep( 3 ); }
}
}
class Program
{ static void Main()
{
Class1 a = new Class1();
Thread t1 = new Thread( new ThreadStart( a.Do ) );
t1.Name = "Second";
Console.WriteLine( "Поток " + t1.Name );
t1.Start();
Thread t2 = new Thread( new ThreadStart( a.Do ) );
t2.Name = "Third";
Console.WriteLine( "Поток " + t2.Name );
t2.Start();
}
}
}
Результат работы программы:
Поток Second Поток Third 0 0 1 1 2 2 3 3
Варианты вывода могут несколько различаться, поскольку один поток прерывает выполнение другого в неизвестные моменты времени.
Для того чтобы блок кода мог использоваться в каждый момент только одним потоком, применяется оператор lock. Формат оператора:
lock ( выражение ) блок_операторов
Выражение определяет объект, который требуется заблокировать. Для обычных методов в качестве выражения используется ключевое слово this, для статических — typeof( класс ). Блок операторов задает критическую секцию кода, которую требуется заблокировать.
Например, блокировка операторов в приведенном ранее методе Do выглядит следующим образом:
public void Do()
{
lock( this )
{
for ( int i = 0; i < 4; ++i )
{ Console.Write( " " + i ); Thread.Sleep( 30 ); }
}
}
Для такого варианта метода результат работы программы изменится:
Поток Second Поток Third 0 1 2 3 0 1 2 3
Делегат можно вызвать на выполнение либо синхронно, как во всех приведенных ранее примерах, либо асинхронно с помощью методов BeginInvoke и EndInvoke.
При вызове делегата с помощью метода BeginInvoke среда выполнения создает для исполнения метода отдельный поток и возвращает управление оператору, следующему за вызовом. При этом в исходном потоке можно продолжать вычисления.
Если при вызове BeginInvoke был указан метод обратного вызова, этот метод вызывается после завершения потока. Метод обратного вызова также задается с помощью делегата, при этом используется стандартный делегат AsyncCallback. В методе обратного вызова для получения возвращаемого значения и выходных параметров применяется метод EndInvoke.
Если метод обратного вызова не был указан в параметрах метода BeginInvoke, метод EndInvoke можно использовать в потоке, инициировавшем запрос.
В листинге 10.8 приводится два примера асинхронного вызова метода, выполняющего разложение числа на множители. Листинг приводится по документации Visual Studio с некоторыми изменениями.
Класс Factorizer содержит метод Factorize, выполняющий разложение на множители. Этот метод асинхронно вызывается двумя способами: в методе Num1 метод обратного вызова задается в BeginInvoke, в методе Num2 имеют место ожидание завершения потока и непосредственный вызов EndInvoke.
using System;
using System.Threading;
using System.Runtime.Remoting.Messaging;
// асинхронный делегат
public delegate bool AsyncDelegate ( int Num, out int m1, out int m2 );
// класс, выполняющий разложение числа на множители
public class Factorizer
{
public bool Factorize( int Num, out int m1, out int m2 )
{
m1 = 1; m2 = Num;
for ( int i = 2; i < Num; i++ )
if ( 0 == (Num % i) ) { m1 = i; m2 = Num / i; break; }
if (1 == m1 ) return false;
else return true;
}
}
// класс, получающий делегат и результаты
public class PNum
{
private int Number;
public PNum( int number ) { Number = number; }
[OneWayAttribute()]
// метод, получающий результаты
public void Res( IAsyncResult ar )
{
int m1, m2;
// получение делегата из AsyncResult
AsyncDelegate ad = (AsyncDelegate)((AsyncResult)ar).AsyncDelegate;
// получение результатов выполнения метода Factorize
ad.EndInvoke( out m1, out m2, ar );
// вывод результатов
Console.WriteLine( "Первый способ : множители {0} : {1} {2}",
Number, m1, m2 );
}
}
// демонстрационный класс
public class Simple
{
// способ 1: используется функция обратного вызова
public void Num1()
{
Factorizer f = new Factorizer();
AsyncDelegate ad = new AsyncDelegate ( f.Factorize );
int Num = 1000589023, tmp;
// создание экземпляра класса, который будет вызван
// после завершения работы метода Factorize
PNum n = new PNum( Num );
// задание делегата метода обратного вызова
AsyncCallback callback = new AsyncCallback( n.Res );
// асинхронный вызов метода Factorize
IAsyncResult ar = ad.BeginInvoke(
Num, out tmp, out tmp, callback, null );
//
// здесь - выполнение неких дальнейших действий
// ...
}
// способ 2: используется ожидание окончания выполнения
public void Num2()
{
Factorizer f = new Factorizer();
AsyncDelegate ad = new AsyncDelegate ( f.Factorize );
int Num = 1000589023, tmp;
// создание экземпляра класса, который будет вызван
// после завершения работы метода Factorize
PNum n = new PNum( Num );
// задание делегата метода обратного вызова
AsyncCallback callback = new AsyncCallback( n.Res );
// асинхронный вызов метода Factorize
IAsyncResult ar = ad.BeginInvoke(
Num, out tmp, out tmp, null, null );
// ожидание завершения
ar.AsyncWaitHandle.WaitOne( 10000, false );
if ( ar.IsCompleted )
{
int m1, m2;
// получение результатов выполнения метода Factorize
ad.EndInvoke( out m1, out m2, ar );
// вывод результатов
Console.WriteLine( "Второй способ : множители {0} : {1} {2}",
Num, m1, m2 );
}
}
public static void Main()
{
Simple s = new Simple();
s.Num1();
s.Num2();
}
}
Результат работы программы:
Первый способ : множители 1000589023 : 7 142941289 Второй способ : множители 1000589023 : 7 142941289
Примечание
Атрибут [OneWayAttribute()] помечает метод как не имеющий возвращаемого значения и выходных параметров.
На этом данный курс лекций завершается. В него не вошли многие темы, рассмотренные в учебнике [4]: структуры, перечисления, работа с файлами, регулярные выражения, сборки, атрибуты, небезопасный код, основы программирования под Windows. Это связано с формулировкой договора, заключенного автором с издательством ПИТЕР при публикации книги.
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.