Использование нулевой проверки в обработчике событий
При проверке, является ли обработчик событий нулевым, выполняется ли это по-потоку?
Обеспечение того, чтобы кто-то слушал событие, выполняется следующим образом:
EventSeven += new DivBySevenHandler(dbsl.ShowOnScreen);
Если я добавлю код, следуя этому шаблону выше, где я проверяю значение null, то зачем мне нужна нулевая проверка (код, взятый с этого сайта). Что мне не хватает?
Кроме того, что такое правило с событиями и GC?
Ответы
Ответ 1
Непонятно, что вы имеете в виду, я боюсь, но если есть вероятность того, что делегат будет пустым, вам нужно проверить это отдельно для каждого потока. Обычно вы делаете:
public void OnSeven()
{
DivBySevenHandler handler = EventSeven;
if (handler != null)
{
handler(...);
}
}
Это гарантирует, что даже если EventSeven
изменяется в течение OnSeven()
, вы не получите NullReferenceException
.
Но вы правы, что вам не нужна нулевая проверка, если вы определенно получили подписанный обработчик. Это можно легко сделать в С# 2 с обработчиком "no-op":
public event DivBySevenHandler EventSeven = delegate {};
С другой стороны, вам может потребоваться некоторая блокировка, чтобы убедиться, что у вас есть "последний" набор обработчиков, если вы можете получать подписки из разных потоков. У меня есть пример в моем учебнике по потокам, который может помочь - хотя обычно я рекомендую попробовать избежать его.
С точки зрения сбора мусора, издатель событий получает ссылку на подписчика событий (т.е. цель обработчика). Это только проблема, если издатель должен жить дольше, чем абонент.
Ответ 2
Проблема в том, что если никто не подписывается на событие, он равен нулю. И вы не можете ссылаться на null. Три подхода бросаются в глаза:
- проверить значение null (см. ниже)
- добавьте обработчик "ничего не делать":
public event EventHandler MyEvent = delegate {};
- используйте метод расширения (см. ниже)
При проверке нулевого значения, чтобы быть потокобезопасным, вы должны теоретически сначала захватить ссылку делегата (в случае, если она изменяется между проверкой и вызовом):
protected virtual void OnMyEvent() {
EventHandler handler = MyEvent;
if(handler != null) handler(this, EventArgs.Empty);
}
Методы расширения имеют необычное свойство, которое они могут быть вызваны в нулевых экземплярах...
public static void SafeInvoke(this EventHandler handler, object sender)
{
if (handler != null) handler(sender, EventArgs.Empty);
}
public static void SafeInvoke<T>(this EventHandler<T> handler,
object sender, T args) where T : EventArgs
{
if (handler != null) handler(sender, args);
}
то вы можете позвонить:
MyEvent.SafeInvoke(this);
и он является нулевым (через проверку) и потокобезопасным (только путем чтения ссылки только).
Ответ 3
Я выкапываю очень старое сообщение, но я просто хочу добавить небольшую информацию о С# 6.0-синтаксисе:
Теперь можно заменить это:
var handler = EventSeven;
if (handler != null)
handler.Invoke(this, EventArgs.Empty);
с этим:
handler?.Invoke(this, EventArgs.Empty);
РЕДАКТИРОВАТЬ: Объединяя его с членами с выражением тела, вы можете сократить следующий код:
protected virtual void OnMyEvent()
{
EventHandler handler = MyEvent;
handler?.Invoke(this, EventArgs.Empty);
}
до однострочного:
protected virtual void OnMyEvent() => MyEvent?.Invoke(this, EventArgs.Empty);
См. MSDN для получения дополнительной информации об операторе с нулевым условием
См. этот блог о членах, работающих с выражением
Ответ 4
Всегда рекомендуется проверять обработчик событий, прежде чем запускать его. Я делаю это, даже если я изначально "гарантирую" себя, что он всегда установлен. Если я позже изменю это, мне не нужно проверять все мои действия. Поэтому для каждого события у меня всегда есть сопровождающий метод OnXXX:
private void OnEventSeven()
{
var handler = EventSeven;
if (handler != null)
{
handler(this, EventArgs.Empty);
}
}
Это особенно важно, если обработчик событий является общедоступным для вашего класса, поскольку внешние вызывающие могут по желанию добавлять и удалять обработчики событий.
Ответ 5
Если вы имеете в виду это:
public static void OnEventSeven(DivBySevenEventArgs e)
{
if(EventSeven!=null)
EventSeven(new object(),e);
}
фрагмент кода, тогда ответ будет следующим:
Если никто не подписывается на обработчик событий EventSeven, вы получите исключение с помощью NULL-ссылки в "EventSeven (новый объект(), e);
И правило:
Абонент несет ответственность за добавление обработчика (+ =) и его удаление (- =), когда он больше не хочет получать события. Сбор мусора идет по правилам по умолчанию, если объект больше не ссылается, его можно очистить.
Ответ 6
Используя PostSharp, можно отредактировать скомпилированную сборку на этапе посткомпиляции. Это позволяет применять "аспекты" к коду, разрешая сквозные проблемы.
Хотя нулевые проверки или пустая инициализация делегата может быть очень незначительной проблемой, я написал аспект, который разрешает его, добавляя пустой делегат ко всем событиям в сборке.
Использование довольно просто:
[assembly: InitializeEventHandlers( AttributeTargetTypes = "Main.*" )]
namespace Main
{
...
}
I подробно обсудил этот вопрос в моем блоге. Если у вас есть PostSharp, вот аспект:
/// <summary>
/// Aspect which when applied on an assembly or class, initializes all the event handlers (<see cref="MulticastDelegate" />) members
/// in the class(es) with empty delegates to prevent <see cref="NullReferenceException" />'s.
/// </summary>
/// <author>Steven Jeuris</author>
[AttributeUsage( AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Event )]
[MulticastAttributeUsage( MulticastTargets.Event, AllowMultiple = false )]
[AspectTypeDependency( AspectDependencyAction.Commute, typeof( InitializeEventHandlersAttribute ) )]
[Serializable]
public class InitializeEventHandlersAttribute : EventLevelAspect
{
[NonSerialized]
Action<object> _addEmptyEventHandler;
[OnMethodEntryAdvice, MethodPointcut( "SelectConstructors" )]
public void OnConstructorEntry( MethodExecutionArgs args )
{
_addEmptyEventHandler( args.Instance );
}
// ReSharper disable UnusedMember.Local
IEnumerable<ConstructorInfo> SelectConstructors( EventInfo target )
{
return target.DeclaringType.GetConstructors( BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic );
}
// ReSharper restore UnusedMember.Local
public override void RuntimeInitialize( EventInfo eventInfo )
{
base.RuntimeInitialize( eventInfo );
// Construct a suitable empty event handler.
MethodInfo delegateInfo = DelegateHelper.MethodInfoFromDelegateType( eventInfo.EventHandlerType );
ParameterExpression[] parameters = delegateInfo.GetParameters().Select( p => Expression.Parameter( p.ParameterType ) ).ToArray();
Delegate emptyDelegate
= Expression.Lambda( eventInfo.EventHandlerType, Expression.Empty(), "EmptyDelegate", true, parameters ).Compile();
// Create a delegate which adds the empty handler to an instance.
_addEmptyEventHandler = instance => eventInfo.AddEventHandler( instance, emptyDelegate );
}
}
... и вспомогательный метод, который он использует:
/// <summary>
/// The name of the Invoke method of a Delegate.
/// </summary>
const string InvokeMethod = "Invoke";
/// <summary>
/// Get method info for a specified delegate type.
/// </summary>
/// <param name = "delegateType">The delegate type to get info for.</param>
/// <returns>The method info for the given delegate type.</returns>
public static MethodInfo MethodInfoFromDelegateType( Type delegateType )
{
Contract.Requires( delegateType.IsSubclassOf( typeof( MulticastDelegate ) ), "Given type should be a delegate." );
return delegateType.GetMethod( InvokeMethod );
}