В этой статье я расскажу
о некоторых нюансах при использовании событий (events) в языке C#. Подразумеваю, что читатель
довольно неплохо разбирается с базовыми понятиями событий и делегатов. Поэтому начну из одной особенности, о
которой многие программисты не знают.
Приведем небольшой пример кода:
class Program
{
public delegate void LogDelegate(string message);
public static event LogDelegate LogEvent;
public static int counter = 0;
static void Main(string[] args)
{
var logToConsoleDelegate1 = new LogDelegate(LogToConsole);
var logToConsoleDelegate2 = new LogDelegate(LogToConsole);
var logToConsoleDelegate3 = new LogDelegate(LogToConsole);
var logToConsoleDelegate4 = new LogDelegate(LogToConsole);
try
{
LogEvent += logToConsoleDelegate1;
LogEvent += logToConsoleDelegate2;
LogEvent += logToConsoleDelegate3;
LogEvent += logToConsoleDelegate4;
LogEvent.Invoke("Test");
}
catch (Exception ex)
{
Console.WriteLine("Catch
exception {0}", ex);
}
finally
{
LogEvent -= logToConsoleDelegate1;
LogEvent -= logToConsoleDelegate2;
LogEvent -= logToConsoleDelegate3;
LogEvent -= logToConsoleDelegate4;
}
Console.ReadLine();
}
public static void LogToConsole(string message)
{
Console.WriteLine("Call
LoginToConsole {0}", message);
Console.WriteLine(message);
if (counter == 1)
throw new ArgumentNullException("Test");
counter++;
}
}
Описан пример кода, в
котором в одном из цепочки событий генерируется exception. Вопрос, будет ли продолжено выполнение после того, как
мы перехватим событие? Если Вы предполагали, что будет вызвано три события и
одно – которое вызвало исключение и перехвачено, то у меня для вас плохая новость.
Если вызванный метод выбрасывает исключение,
выполнение этого метода прекращается, исключение передается обратно коду,
вызвавшему делегат – и оставшиеся в списке вызовов методы не вызываются.
Перехват исключения в вызывающем коде не меняет этого поведения. Многие
могут сказать, что пример надуманный и кому придет в голову делать столько привязок на один
метод. А что, если сделать разные методы и так же перехватывать их? Перепишем пример с разными методами:
class Program
{
public delegate void LogDelegate(string message);
public static event LogDelegate LogEvent;
static void Main(string[] args)
{
var program = new Program();
var logToConsoleDelegate = new LogDelegate(LogToConsole);
var logToMessageBoxDelegate = new LogDelegate(program.LogToMessageBox);
var logToFileDelegate = new LogDelegate(program.LogToFile);
var logToEventLogDelegate = new LogDelegate(program.LogToOutput);
try
{
LogEvent += logToConsoleDelegate;
LogEvent +=
logToMessageBoxDelegate;
LogEvent += logToFileDelegate;
LogEvent += logToEventLogDelegate;
LogEvent.Invoke("Test");
}
catch (Exception ex)
{
Console.WriteLine("Catch
exception {0}", ex);
}
finally
{
LogEvent -= logToConsoleDelegate;
LogEvent -=
logToMessageBoxDelegate;
LogEvent -= logToFileDelegate;
LogEvent -= logToEventLogDelegate;
}
Console.ReadLine();
}
public static void LogToConsole(string message)
{
Console.WriteLine("Call
LoginToConsole {0}", message);
Console.WriteLine(message);
}
public void LogToFile(string message)
{
Console.WriteLine("Call
LoginToFile {0}", message);
File.WriteAllText(Path.Combine(Directory.GetCurrentDirectory(), "test.txt"), message);
}
public void LogToMessageBox(string message)
{
Console.WriteLine("Call
LoginToMessageBox {0}", message);
MessageBox.Show(message);
throw new ArgumentNullException("Test");
}
public void LogToOutput(string message)
{
Console.WriteLine("Call
LogToOutput {0}", message);
}
}
К сожалению, приведенный выше код тоже не работает из-за описанной выше проблемы. Вы можете переписать код таким образом:
static void Main(string[] args)
{
var program = new Program();
var logDelegate = new LogDelegate(LogToConsole);
logDelegate += program.LogToMessageBox;
logDelegate += program.LogToFile;
logDelegate += program.LogToOutput;
try
{
LogEvent += logDelegate;
LogEvent.Invoke("Test");
}
catch (Exception ex)
{
Console.WriteLine("Catch
exception {0}", ex);
}
finally
{
LogEvent -= logDelegate;
}
Console.ReadLine();
}
И это
ничего не изменит. Так устроена работа делегатов. Единственное решение, которое
я знаю на данный момент, – это перехватывать внутри функции код через try->catch и гасить ошибку. Пример:
public void LogToMessageBox(string message)
{
try
{
Console.WriteLine("Call
LoginToMessageBox {0}", message);
MessageBox.Show(message);
throw new ArgumentNullException("Test");
}
catch (Exception exception)
{
Console.WriteLine(exception.ToString());
}
}
И другой
способ для решения такой проблемы – это использовать известные паттерны, которые,
по сути, помогают заменить использование событий. Это классический паттерн Observer (наблюдатель)
и паттерн Publisher->Subscriber, на основании которого работают Event Aggregator с Prism. Есть и
другие варианты решения этой проблемы, но, к сожалению, я с ними не знаком.
Злоупотребление лямбда-выражениями
Вторая
проблема с использованием событий в C# для новичков выражается
в использовании лямбда-выражений. Вот как выглядит пример с использованием лямбда-выражений:
class Program
{
public static event EventHandler LogEvent;
static void Main(string[] args)
{
LogEvent += (sender, arg) => { };
Console.ReadLine();
}
}
Очень часто новички начинают использовать такой удобный способ привязки повсюду. Вот какой способ использования я
встретил однажды в реальном коде:
class Program
{
public static event EventHandler LogEvent;
static void Main(string[] args)
{
LogEvent += (sender, arg) => { };
LogEvent -= (sender, arg) => { };
Console.ReadLine();
}
}
Разработчик даже не понимал, что он делает подписку на одно
событие, а отписывается уже от другого. Он, наверное, предполагал, что JIT компилятор настолько умный и сам
догадается о том, что это один и тот же
метод.
Но компилятор интерпретировал этот код так, как написал его
разработчик. За это огромное спасибо разработчикам JIT компилятора. Вот как выглядит IL код приведенного выше метода:
.method private hidebysig static
void Main (
string[] args
) cil managed
{
// Method begins at RVA 0x20cc
// Code size 82 (0x52)
.maxstack 2
.entrypoint
IL_0000: nop
IL_0001: ldsfld class [mscorlib]System.EventHandler ConsoleApplicationTestEvent.Program::'CS$<>9__CachedAnonymousMethodDelegate2'
IL_0006: brtrue.s IL_001b
IL_0008: ldnull
IL_0009: ldftn void ConsoleApplicationTestEvent.Program::'<Main>b__0'(object, class [mscorlib]System.EventArgs)
IL_000f: newobj instance void [mscorlib]System.EventHandler::.ctor(object, native int)
IL_0014: stsfld class [mscorlib]System.EventHandler ConsoleApplicationTestEvent.Program::'CS$<>9__CachedAnonymousMethodDelegate2'
IL_0019: br.s IL_001b
IL_001b: ldsfld class [mscorlib]System.EventHandler ConsoleApplicationTestEvent.Program::'CS$<>9__CachedAnonymousMethodDelegate2'
IL_0020: call void ConsoleApplicationTestEvent.Program::add_LogEvent(class [mscorlib]System.EventHandler)
IL_0025: nop
IL_0026: ldsfld class [mscorlib]System.EventHandler ConsoleApplicationTestEvent.Program::'CS$<>9__CachedAnonymousMethodDelegate3'
IL_002b: brtrue.s IL_0040
IL_002d: ldnull
IL_002e: ldftn void ConsoleApplicationTestEvent.Program::'<Main>b__1'(object, class [mscorlib]System.EventArgs)
IL_0034: newobj instance void [mscorlib]System.EventHandler::.ctor(object, native int)
IL_0039: stsfld class [mscorlib]System.EventHandler ConsoleApplicationTestEvent.Program::'CS$<>9__CachedAnonymousMethodDelegate3'
IL_003e: br.s IL_0040
IL_0040: ldsfld class [mscorlib]System.EventHandler ConsoleApplicationTestEvent.Program::'CS$<>9__CachedAnonymousMethodDelegate3'
IL_0045: call void ConsoleApplicationTestEvent.Program::remove_LogEvent(class [mscorlib]System.EventHandler)
IL_004a: nop
IL_004b: call string [mscorlib]System.Console::ReadLine()
IL_0050: pop
IL_0051: ret
} // end of method Program::Main
void Main (
string[] args
) cil managed
{
// Method begins at RVA 0x20cc
// Code size 82 (0x52)
.maxstack 2
.entrypoint
IL_0000: nop
IL_0001: ldsfld class [mscorlib]System.EventHandler ConsoleApplicationTestEvent.Program::'CS$<>9__CachedAnonymousMethodDelegate2'
IL_0006: brtrue.s IL_001b
IL_0008: ldnull
IL_0009: ldftn void ConsoleApplicationTestEvent.Program::'<Main>b__0'(object, class [mscorlib]System.EventArgs)
IL_000f: newobj instance void [mscorlib]System.EventHandler::.ctor(object, native int)
IL_0014: stsfld class [mscorlib]System.EventHandler ConsoleApplicationTestEvent.Program::'CS$<>9__CachedAnonymousMethodDelegate2'
IL_0019: br.s IL_001b
IL_001b: ldsfld class [mscorlib]System.EventHandler ConsoleApplicationTestEvent.Program::'CS$<>9__CachedAnonymousMethodDelegate2'
IL_0020: call void ConsoleApplicationTestEvent.Program::add_LogEvent(class [mscorlib]System.EventHandler)
IL_0025: nop
IL_0026: ldsfld class [mscorlib]System.EventHandler ConsoleApplicationTestEvent.Program::'CS$<>9__CachedAnonymousMethodDelegate3'
IL_002b: brtrue.s IL_0040
IL_002d: ldnull
IL_002e: ldftn void ConsoleApplicationTestEvent.Program::'<Main>b__1'(object, class [mscorlib]System.EventArgs)
IL_0034: newobj instance void [mscorlib]System.EventHandler::.ctor(object, native int)
IL_0039: stsfld class [mscorlib]System.EventHandler ConsoleApplicationTestEvent.Program::'CS$<>9__CachedAnonymousMethodDelegate3'
IL_003e: br.s IL_0040
IL_0040: ldsfld class [mscorlib]System.EventHandler ConsoleApplicationTestEvent.Program::'CS$<>9__CachedAnonymousMethodDelegate3'
IL_0045: call void ConsoleApplicationTestEvent.Program::remove_LogEvent(class [mscorlib]System.EventHandler)
IL_004a: nop
IL_004b: call string [mscorlib]System.Console::ReadLine()
IL_0050: pop
IL_0051: ret
} // end of method Program::Main
У нас было создано
два анонимных метода, вместо одного, как предполагал разработчик. Исправить это очень легко:
class Program
{
public static event EventHandler LogEvent;
static void Main(string[] args)
{
EventHandler handler = (sender, arg)
=> { };
LogEvent += handler;
LogEvent -= handler;
Console.ReadLine();
}
}
И
компилятор нам выдаст то, что нам нужно.
.method private hidebysig static
void Main (
string[] args
) cil managed
{
// Method begins at RVA 0x20cc
// Code size 54 (0x36)
.maxstack 2
.entrypoint
.locals init (
[0] class [mscorlib]System.EventHandler 'handler'
)
IL_0000: nop
IL_0001: ldsfld class [mscorlib]System.EventHandler ConsoleApplicationTestEvent.Program::'CS$<>9__CachedAnonymousMethodDelegate1'
IL_0006: brtrue.s IL_001b
IL_0008: ldnull
IL_0009: ldftn void ConsoleApplicationTestEvent.Program::'<Main>b__0'(object, class [mscorlib]System.EventArgs)
IL_000f: newobj instance void [mscorlib]System.EventHandler::.ctor(object, native int)
IL_0014: stsfld class [mscorlib]System.EventHandler ConsoleApplicationTestEvent.Program::'CS$<>9__CachedAnonymousMethodDelegate1'
IL_0019: br.s IL_001b
IL_001b: ldsfld class [mscorlib]System.EventHandler ConsoleApplicationTestEvent.Program::'CS$<>9__CachedAnonymousMethodDelegate1'
IL_0020: stloc.0
IL_0021: ldloc.0
IL_0022: call void ConsoleApplicationTestEvent.Program::add_LogEvent(class [mscorlib]System.EventHandler)
IL_0027: nop
IL_0028: ldloc.0
IL_0029: call void ConsoleApplicationTestEvent.Program::remove_LogEvent(class [mscorlib]System.EventHandler)
IL_002e: nop
IL_002f: call string [mscorlib]System.Console::ReadLine()
IL_0034: pop
IL_0035: ret
} // end of method Program::Main
void Main (
string[] args
) cil managed
{
// Method begins at RVA 0x20cc
// Code size 54 (0x36)
.maxstack 2
.entrypoint
.locals init (
[0] class [mscorlib]System.EventHandler 'handler'
)
IL_0000: nop
IL_0001: ldsfld class [mscorlib]System.EventHandler ConsoleApplicationTestEvent.Program::'CS$<>9__CachedAnonymousMethodDelegate1'
IL_0006: brtrue.s IL_001b
IL_0008: ldnull
IL_0009: ldftn void ConsoleApplicationTestEvent.Program::'<Main>b__0'(object, class [mscorlib]System.EventArgs)
IL_000f: newobj instance void [mscorlib]System.EventHandler::.ctor(object, native int)
IL_0014: stsfld class [mscorlib]System.EventHandler ConsoleApplicationTestEvent.Program::'CS$<>9__CachedAnonymousMethodDelegate1'
IL_0019: br.s IL_001b
IL_001b: ldsfld class [mscorlib]System.EventHandler ConsoleApplicationTestEvent.Program::'CS$<>9__CachedAnonymousMethodDelegate1'
IL_0020: stloc.0
IL_0021: ldloc.0
IL_0022: call void ConsoleApplicationTestEvent.Program::add_LogEvent(class [mscorlib]System.EventHandler)
IL_0027: nop
IL_0028: ldloc.0
IL_0029: call void ConsoleApplicationTestEvent.Program::remove_LogEvent(class [mscorlib]System.EventHandler)
IL_002e: nop
IL_002f: call string [mscorlib]System.Console::ReadLine()
IL_0034: pop
IL_0035: ret
} // end of method Program::Main
Замыкание на переменных цикла
Замыкание
переменной – это захват значения некоего объекта посредством лямбда-выражений
без дополнительных усилий вроде «out» и «ref»
модификаторов к параметрам. Пример замыкания:
var funcs = new List<Func<int>>();
for (int i = 0; i < 3; i++)
{
funcs.Add(() => i);
}
foreach (var f in funcs)
Console.WriteLine(f());
Более подробно об этом можно посмотреть в моем блоге в статье
про делегаты, а
также найти соответствующую тему в блоге Сергея Теплякова. Поэтому просто приведу способ решения этой проблемы:
var funcs = new List<Func<int>>();
for (int i = 0; i < 3; i++)
{
var temp = i;
funcs.Add(() => temp);
}
foreach (var f in funcs)
Console.WriteLine(f());
После
исправления на каждой итерации цикла будет осуществляться захват нового
экземпляра переменной temp, в результате чего каждый делегат
будет ссылаться на свою копию переменной temp.
Отличие
событий от делегатов
Не упомянуть об отличии
между делегатами и событиями в моей статье было бы непростительно, тем более что Вам наверняка задавали такой вопрос на собеседовании.
class Program
{
public static event EventHandler LogEvent;
static void Main(string[] args)
{
Test();
Console.ReadLine();
}
public static void Test()
{
if(LogEvent != null)
LogEvent(null, new EventArgs());
}
}
Что будет, если в приведенной выше программе удалить ключевое
слово event?
Признаюсь честно, меня это вопрос тоже вводил в ступор. Главное при ответе на
этот вопрос – помнить главные отличия между
delegate и event.
- Благодаря ключевому слову event делегат наделяется некоторыми свойствами, что делает его отличным от делегата.
- Событие нельзя использовать как локальную переменную, в отличие от делегата.
- Событие нельзя запустить вне класса, в котором оно объявлено. Это важное отличие.
Интересный нюанс также связан с тем, что если Вы будете
использовать события вместо делегатов для того, чтобы
предоставлять доступ к Вашему компоненту или классу, то другие разработчики не
смогут стереть цепочку подписавшихся обработчиков.
class Program
{
static void Main(string[] args)
{
var a = new A();
a.LogEvent = null; // ошибка компиляции
a.LogDelegate = null;
Console.ReadLine();
}
}
public class A
{
public event EventHandler LogEvent;
public EventHandler LogDelegate;
public void Test()
{
if (LogEvent != null)
LogEvent(null, new EventArgs());
}
}
Теперь другие разработчики просто не смогут затереть a.LogEvent, так как это вызовет ошибку компиляции. Вы себя обезопасили тем, что
добавили ключевое слово event. И
теперь потенциальные злоумышленники не смогут причинить вам вред.
Итоги:
В
приведенной выше статье я затронул нюансы работы с событиями и
делегатами и привел некоторые типичные ошибки разработчиков при использовании
событий. Надеюсь, что статья будет для Вас полезной.
Источники:
No comments:
Post a Comment