Поднять события С# с помощью метода расширения - это плохо?
Мы все знакомы с ужасом, который является объявлением события С#. Чтобы обеспечить безопасность потока, стандарт должен написать что-то вроде этого:
public event EventHandler SomethingHappened;
protected virtual void OnSomethingHappened(EventArgs e)
{
var handler = SomethingHappened;
if (handler != null)
handler(this, e);
}
Недавно в каком-то другом вопросе на этой доске (который я не могу найти сейчас) кто-то отметил, что методы расширения могут быть использованы в этом сценарии. Вот один из способов сделать это:
static public class EventExtensions
{
static public void RaiseEvent(this EventHandler @event, object sender, EventArgs e)
{
var handler = @event;
if (handler != null)
handler(sender, e);
}
static public void RaiseEvent<T>(this EventHandler<T> @event, object sender, T e)
where T : EventArgs
{
var handler = @event;
if (handler != null)
handler(sender, e);
}
}
С помощью этих методов расширения все, что вам нужно объявить и поднять событие, выглядит примерно так:
public event EventHandler SomethingHappened;
void SomeMethod()
{
this.SomethingHappened.RaiseEvent(this, EventArgs.Empty);
}
Мой вопрос: Это хорошая идея? Не хватает ли мы чего-либо, не имея стандартного метода On? (Одна вещь, которую я замечаю, это то, что она не работает с событиями, у которых есть явный код добавления/удаления.)
Ответы
Ответ 1
Он по-прежнему будет работать с событиями с явным добавлением/удалением - вам просто нужно использовать переменную делегата (или, тем не менее, вы сохранили делегат) вместо имени события.
Однако есть более простой способ сделать его потокобезопасным - инициализировать его обработчиком no-op:
public event EventHandler SomethingHappened = delegate {};
Достижение производительности при вызове дополнительного делегата будет незначительным, и это, несомненно, упростит код.
Кстати, в вашем методе расширения вам не нужна дополнительная локальная переменная - вы можете просто сделать:
static public void RaiseEvent(this EventHandler @event, object sender, EventArgs e)
{
if (@event != null)
@event(sender, e);
}
static public void RaiseEvent<T>(this EventHandler<T> @event, object sender, T e)
where T : EventArgs
{
if (@event != null)
@event(sender, e);
}
Лично я бы не использовал ключевое слово в качестве имени параметра, но он вообще не меняет вызывающую сторону, поэтому сделайте то, что вы хотите:)
EDIT: Что касается метода OnXXX: планируете ли вы, на какие производные классы? На мой взгляд, большинство классов должны быть запечатаны. Если вы это сделаете, хотите ли вы, чтобы эти производные классы могли поднять это событие? Если ответ на любой из этих вопросов "нет", то не беспокойтесь. Если ответ на оба "да", то выполните:)
Ответ 2
Теперь здесь С# 6 существует более компактный, потокобезопасный способ запуска события:
SomethingHappened?.Invoke(this, e);
Invoke()
вызывается только в том случае, если делегаты зарегистрированы для события (т.е. это не null), благодаря оператору с нулевым условием "?".
Проблема с потоками кода "обработчик" в вопросе, который задается для решения, обходит здесь, потому что, как и в этом коде, SomethingHappened
доступен только один раз, поэтому нет возможности установить его значение null между тестом и вызов.
Этот ответ, возможно, тангенциальен для исходного вопроса, но очень уместен для тех, кто ищет более простой метод для повышения событий.
Ответ 3
[Здесь мысль]
Просто напишите код один раз в рекомендуемом порядке и сделайте с ним. Тогда вы не будете путать своих коллег, просматривающих код, думая, что вы сделали что-то неправильно?
[Я читаю больше сообщений, пытаясь найти способы написания обработчика событий, чем когда-либо писал писать обработчик событий.]
Ответ 4
Меньше кода, более читаемый. Мне нравится.
Если вы не заинтересованы в производительности, вы можете объявить свое событие таким образом, чтобы избежать нулевой проверки:
public event EventHandler SomethingHappened = delegate{};
Ответ 5
Вы не обеспечиваете безопасность потоков, назначая обработчик локальной переменной. После назначения ваш метод все равно может быть прерван. Если, например, класс, который использовался для прослушивания события, удаляется во время прерывания, вы вызываете метод в удаленном классе.
Вы спасаете себя от исключения с нулевой ссылкой, но есть более простые способы сделать это, как указал Джон Скит и кристианлибардо в своих ответах.
Другое дело, что для непечатаемых классов метод OnFoo должен быть виртуальным, который, как я думаю, невозможен с помощью методов расширения.
Ответ 6
Чтобы сделать вышеупомянутые ответы на шаг вперед, вы можете защитить себя от одного из ваших обработчиков, выдавшего исключение. Если это произойдет, последующие обработчики не будут вызваны.
Аналогично, вы могли бы назначить обработчики так, чтобы длительный обработчик не вызывал чрезмерную задержку для информирования последних обработчиков. Это также может защитить исходный поток от перехвата долго работающим обработчиком.
public static class EventHandlerExtensions
{
private static readonly log4net.ILog _log = log4net.LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
public static void Taskify(this EventHandler theEvent, object sender, EventArgs args)
{
Invoke(theEvent, sender, args, true);
}
public static void Taskify<T>(this EventHandler<T> theEvent, object sender, T args)
{
Invoke(theEvent, sender, args, true);
}
public static void InvokeSafely(this EventHandler theEvent, object sender, EventArgs args)
{
Invoke(theEvent, sender, args, false);
}
public static void InvokeSafely<T>(this EventHandler<T> theEvent, object sender, T args)
{
Invoke(theEvent, sender, args, false);
}
private static void Invoke(this EventHandler theEvent, object sender, EventArgs args, bool taskify)
{
if (theEvent == null)
return;
foreach (EventHandler handler in theEvent.GetInvocationList())
{
var action = new Action(() =>
{
try
{
handler(sender, args);
}
catch (Exception ex)
{
_log.Error(ex);
}
});
if (taskify)
Task.Run(action);
else
action();
}
}
private static void Invoke<T>(this EventHandler<T> theEvent, object sender, T args, bool taskify)
{
if (theEvent == null)
return;
foreach (EventHandler<T> handler in theEvent.GetInvocationList())
{
var action = new Action(() =>
{
try
{
handler(sender, args);
}
catch (Exception ex)
{
_log.Error(ex);
}
});
if (taskify)
Task.Run(action);
else
action();
}
}
}