About .NET, ASP.NET, MVC, C#, WPF, WCF and everything related to .NET and more.

Слабые события

Категории: Программирование

Введение

При использовании обычных C# события, при регистрации обработчика события создается "сильная ссылка" из источника события на объект слушателя. Если источник события имеет более длительный срок службы по сравнению со слушателем, и если слушатель станет больше не нужен (когда на него нет других ссылок), чтобы избежать утечки памяти, объект слушателя должен отписаться от исходного события. Если он этого не сделает, использование обычных .NET событий вызывает утечку памяти.
Есть много различных подходов для решения этой проблемы. В этой статье рассматриваются некоторые из них с их преимуществами и недостатками.

Способ 1. Использование слабых-делегатов без кодогенерации.

Этот способ очень простой, нам необходимо создать обертку для "слабого-делегата":
public class WeakDelegate<TDelegate> : IEquatable<TDelegate> where TDelegate : class
{
    private readonly MethodInfo _method;
    private readonly WeakReference _targetReference;

    public WeakDelegate(Delegate realDelegate)
    {
        _targetReference = realDelegate.Target != null ? new WeakReference(realDelegate.Target) : null;
        _method = realDelegate.Method;
    }

    public bool IsAlive
    {
        get { return _targetReference == null || _targetReference.IsAlive; }
    }

    public TDelegate GetDelegate()
    {
        //If it's non-static create delegate with target.
        if (_targetReference != null)
            return Delegate.CreateDelegate(typeof(TDelegate), _targetReference.Target, _method) as TDelegate;
        return Delegate.CreateDelegate(typeof(TDelegate), _method) as TDelegate;
    }

    public bool Equals(TDelegate other)
    {
        var d = (Delegate)(object)other;
        return d != null
                && d.Target == _targetReference.Target
                && d.Method.Equals(_method);
    }

    public void Invoke(params object[] args)
    {
        var handler = (Delegate) (object) GetDelegate();
        handler.DynamicInvoke(args);
    }
}
И для "слабого-события":
public class WeakEvent<TEventHandler> where TEventHandler : class
{
    private readonly List<WeakDelegate<TEventHandler>> _handlers;

    public WeakEvent()
    {
        _handlers = new List<WeakDelegate<TEventHandler>>();
    }

    public void AddHandler(TEventHandler handler)
    {
        var d = (Delegate)(object)handler;
        lock (_handlers)
            _handlers.Add(new WeakDelegate<TEventHandler>(d));
    }

    public void RemoveHandler(TEventHandler handler)
    {
        // also remove "dead" (garbage collected) handlers
        lock (_handlers)
            _handlers.RemoveAll(wd => !wd.IsAlive || wd.Equals(handler));
    }

    public void Raise(params object[] values)
    {
        lock (_handlers)
        {
            for (int index = 0; index < _handlers.Count; index++)
            {
                var weakDelegate = _handlers[index];
                if (weakDelegate.IsAlive)
                    weakDelegate.Invoke(values);
                else
                {
                    _handlers.Remove(weakDelegate);
                    index--;
                }
            }
        }
    }
}
После этого, мы можем создать класс, который будет использовать WeakEvent для хранения событий:
public class Alpha
{
    private readonly WeakEvent<Action<object>> _myEvents;

    public Alpha()
    {
        _myEvents = new WeakEvent<Action<object>>();
    }

    public event Action<object> MyEvent
    {
        add { _myEvents.AddHandler(value); }
        remove { _myEvents.RemoveHandler(value); }
    }

    protected virtual void OnMyEvent(object value)
    {
        _myEvents.Raise(value);
    }
}
Преимущества
  • Работает на всех платформах
  • Простой и эффективный
Недостатки
  • Медленный, потому что на каждый вызов события, создается новый экземляр делегата

Способ 2. Использование "слабых-делегатов" созданных с помощью DynamicMethod.

Второй способ использует DynamicMethod для создания "слабой" версии делегата. Код ниже демонстрирует это.
Первый шаг создать класс, который будет хранить информацию о "слабом-событии":
public sealed class WeakEventInfo
{
    #region Const

    public static readonly FieldInfo TargetFieldInfo = typeof(WeakEventInfo).GetField("Target");

    public static readonly FieldInfo UnsubcribeDelegateFieldInfo =
        typeof(WeakEventInfo).GetField("UnsubcribeDelegate");

    public static readonly FieldInfo DelegateFieldInfo = typeof(WeakEventInfo).GetField("Delegate");

    public static readonly MethodInfo ClearMethodInfo = typeof(WeakEventInfo).GetMethod("Clear");

    #endregion

    #region Fields

    public Delegate Delegate;

    public WeakReference Target;

    public Action<Delegate> UnsubcribeDelegate;

    public MethodInfo OriginalMethod;

    #endregion

    #region Methods

    public void Clear()
    {
        if (UnsubcribeDelegate != null)
            UnsubcribeDelegate(Delegate);
        Delegate = null;
        Target = null;
        UnsubcribeDelegate = null;
        OriginalMethod = null;
    }

    #endregion
}
Описание полей:
  • Delegate - содержит слабую версию делегата. Используется для того, чтобы отписаться от события, когда слушатель был удален сборщиком мусора(GC).
  • Target - содержит цель делегата, как слабую ссылку.
  • UnsubcribeDelegate - содержит делегат, который используется для того, чтобы отписаться от события, когда слушатель был удален сборщиком мусора(GC).
  • OriginalMethod - содержит информацию о методе, который должен быть вызван.
Код для создания "слабых-делегатов":
internal static DynamicMethod ConvertToWeakDelegate(Delegate originalAction)
{
    MethodInfo originalMethodDelegate = originalAction.Method;
    ParameterInfo[] parameterInfos = originalMethodDelegate.GetParameters();

    var types = new Type[parameterInfos.Length + 1];
    types[0] = typeof(WeakEventInfo);
    for (int i = 0; i < parameterInfos.Length; i++)
    {
        types[i + 1] = parameterInfos[i].ParameterType;
    }

    var dynamicMethod = new DynamicMethod(
        string.Format("_DynamicMethod_{0}_{1}", originalMethodDelegate.DeclaringType.Name,
                        Guid.NewGuid().ToString("N")), originalMethodDelegate.ReturnType,
        types, originalMethodDelegate.DeclaringType.Module, true);

    ILGenerator ilGenerator = dynamicMethod.GetILGenerator();
    Label originalDelegateIsNotNull = ilGenerator.DefineLabel();
    Label returnNullLabel = ilGenerator.DefineLabel();
    LocalBuilder declareLocal = ilGenerator.DeclareLocal(originalAction.Target.GetType());


    for (Int16 i = 0; i < parameterInfos.Length; i++)
    {
        ParameterInfo parameterInfo = parameterInfos[i];
        if (!parameterInfo.IsDefined(typeof(OutAttribute), true)) continue;
        Type elementType = parameterInfo.ParameterType.GetElementType();
        ilGenerator.Emit(OpCodes.Ldarg, i + 1);
        ilGenerator.Emit(OpCodes.Initobj, elementType);
    }

    ilGenerator.Emit(OpCodes.Ldarg_0);
    ilGenerator.Emit(OpCodes.Ldfld, WeakEventInfo.TargetFieldInfo);
    ilGenerator.Emit(OpCodes.Brfalse_S, returnNullLabel);
    //if (weakEventInfo.Target == null)
    //   return null;

    ilGenerator.Emit(OpCodes.Ldarg_0);
    ilGenerator.Emit(OpCodes.Ldfld, WeakEventInfo.TargetFieldInfo);
    ilGenerator.Emit(OpCodes.Callvirt, typeof(WeakReference).GetProperty("Target").GetGetMethod());

    ilGenerator.Emit(OpCodes.Castclass, declareLocal.LocalType);
    ilGenerator.Emit(OpCodes.Stloc, declareLocal);
    //var target = weakEventInfo.OriginalDelegate.Target;
    ilGenerator.Emit(OpCodes.Ldloc_0);
    ilGenerator.Emit(OpCodes.Brtrue_S, originalDelegateIsNotNull);
    //if (target == null){

    ilGenerator.Emit(OpCodes.Ldarg_0);
    ilGenerator.Emit(OpCodes.Callvirt, WeakEventInfo.ClearMethodInfo);
    //weakEventInfo.Clear();
    //}
    ilGenerator.MarkLabel(returnNullLabel);
    if (dynamicMethod.ReturnType != typeof(void))
    {
        if (dynamicMethod.ReturnType.IsValueType)
        {
            LocalBuilder result = ilGenerator.DeclareLocal(dynamicMethod.ReturnType);
            ilGenerator.Emit(OpCodes.Ldloca_S, result);
            ilGenerator.Emit(OpCodes.Initobj, dynamicMethod.ReturnType);
            ilGenerator.Emit(OpCodes.Ldloc_S, result);
        }
        else
            ilGenerator.Emit(OpCodes.Ldnull);
    }
    ilGenerator.Emit(OpCodes.Ret);
    //return null;
    //}

    ilGenerator.MarkLabel(originalDelegateIsNotNull);
    ilGenerator.Emit(OpCodes.Ldloc_0);
    for (Int16 i = 1; i < types.Length; i++)
    {
        ilGenerator.Emit(OpCodes.Ldarg_S, i);
    }
    ilGenerator.Emit(OpCodes.Callvirt, originalMethodDelegate);
    ilGenerator.Emit(OpCodes.Ret);
    //return target.Invoke(arg1, arg2, .....);


    return dynamicMethod;
}

public static Delegate ConvertToWeakDelegate(Delegate originalAction, Action<Delegate> unsubcribeDelegate)
{
    if (originalAction == null)
        throw new ArgumentNullException("originalAction");
    if (originalAction.Target == null || originalAction.Target is WeakEventInfo)
        return originalAction;
    DynamicMethod weakMethodInternal = ConvertToWeakDelegate(originalAction);
    var weakEventInfo = new WeakEventInfo
        {
            Target = new WeakReference(originalAction.Target),
            OriginalMethod = originalAction.Method
        };
    if (unsubcribeDelegate != null)
        weakEventInfo.UnsubcribeDelegate = unsubcribeDelegate;

    Delegate weakDelegate = weakMethodInternal.CreateDelegate(originalAction.GetType(), weakEventInfo);
    if (unsubcribeDelegate != null)
        weakEventInfo.Delegate = weakDelegate;
    return weakDelegate;
}
Примечание: В этом примере я не кэшировал динамические методы, но я рекомендую вам это сделать.
Теперь мы можем использовать эти методы, чтобы создавать "слабых делегатов":
public class Beta
{
    public event Action<object> MyEvent;

    protected virtual void OnMyEvent(object value)
    {
        Action<object> action = MyEvent;
        if (action != null)
            action(value);
    }
}
....

private void Method()
{
    var beta = new Beta();
    beta.MyEvent += (Action<object>) ConvertToWeakDelegate(new Action<object>(OnMyEvent), @delegate =>
        {
            beta.MyEvent -= (Action<object>) @delegate;
        });
}

private void OnMyEvent(object value)
{
            
}
Для того, чтобы сделать этот синтаксис лучше, напишем этот метод:
public static T ConvertToWeakDelegate<T>(T originalAction, Action<T> unsubcribeDelegate) where T : class
{
    var @delegate = originalAction as Delegate;
    if (@delegate == null)
        throw new ArgumentNullException("originalAction", "Parameter can only be a delegate.");
    Action<Delegate> uns = null;
    if (unsubcribeDelegate != null)
        uns = d => unsubcribeDelegate((T)((object)d));
    return ConvertToWeakDelegate(@delegate, uns) as T;
}
И теперь мы можем переписать предыдущий метод следующим образом:
private void Method()
{
    var beta = new Beta();
    beta.MyEvent += ConvertToWeakDelegate(new Action<object>(OnMyEvent), 
                                   @delegate => beta.MyEvent -= @delegate);
}
Преимущества
  • Быстрый, почти нет накладных расходов.
Недостатки
  • Не работает в режиме частичного доверия, поскольку использует рефлексию для закрытых методов, здесь это описывается более подробно.

Способ 3. Использование "слабых-делегатов" созданных с помощью LambdaExpression.

Этот метод очень похож на второй, отличие только в способе создания "слабых-делегатов". Для создания делегатов используется LambdaExpression и WeakEventInfo классы.
internal static Delegate ConvertToWeakDelegate(WeakEventInfo weakEventInfo, Delegate method)
{
    var methodInfo = weakEventInfo.OriginalMethod;
    Type targetType = method.Target.GetType();
    LabelTarget resultLabel = Expression.Label();
    ConstantExpression constantExpression = Expression.Constant(weakEventInfo);
    ParameterExpression resultVariable = Expression.Variable(methodInfo.ReturnType == typeof(void)
                                                                    ? typeof(object)
                                                                    : methodInfo.ReturnType);
    Expression.Assign(resultVariable, Expression.Default(resultVariable.Type));
    ParameterExpression localField = Expression.Variable(targetType);
    ParameterExpression[] methodCallParameters = methodInfo.GetParameters()
                                                            .Select(
                                                                info => Expression.Parameter(info.ParameterType))
                                                            .ToArray();

    Expression methodCallExpression = Expression.Call(localField, methodInfo, methodCallParameters);
    if (methodInfo.ReturnType != typeof(void))
        methodCallExpression = Expression.Assign(resultVariable, methodCallExpression);
    MemberExpression weakReferenceField = Expression.Field(constantExpression, WeakEventInfo.TargetFieldInfo);

    BlockExpression body = Expression.Block
        (new[] { resultVariable },
            Expression.IfThenElse(Expression.Equal(weakReferenceField, Expression.Constant(null)),
                                Expression.Return(resultLabel),
        //if (weakEventInfo.Target == null)
        //   return null;
        //else
                                Expression.Block
                                    (
                                        new[] { localField },
                                        Expression.Assign(localField,
                                                            Expression.Convert(
                                                                Expression.Property(weakReferenceField,
                                                                                    typeof(WeakReference)
                                                                                        .GetProperty("Target")),
                                                                targetType)),
        //var target = weakEventInfo.OriginalDelegate.Target;
        //if (target == null){
                                        Expression.IfThenElse(
                                            Expression.Equal(localField, Expression.Constant(null)),
                                            Expression.Block
                                                (
                                                    Expression.Call(constantExpression,
                                                                    WeakEventInfo.ClearMethodInfo),
                                                    Expression.Return(resultLabel)
                                                ),
        //weakEventInfo.Clear();
        //return null;
        //}
        //else
        //return target.Invoke(arg1, arg2, .....);
                                            Expression.Block(methodCallExpression, Expression.Return(resultLabel))))),
            Expression.Label(resultLabel), resultVariable);


    return Expression.Lambda(method.GetType(), body, methodCallParameters).Compile();
}

public static Delegate ConvertToWeakDelegate(Delegate originalAction, Action<Delegate> unsubcribeDelegate)
{
    if (originalAction == null) throw new ArgumentNullException("originalAction");
    if (originalAction.Target == null)
        return originalAction;

    var weakEventInfo = new WeakEventInfo
    {
        Target = new WeakReference(originalAction.Target),
        OriginalMethod = originalAction.Method
    };
    Delegate result = ConvertToWeakDelegate(weakEventInfo, originalAction);
    if (unsubcribeDelegate != null)
        weakEventInfo.UnsubcribeDelegate = unsubcribeDelegate;
    if (unsubcribeDelegate != null)
        weakEventInfo.Delegate = result;
    return result;
}
Код для работы с этим методом, такой же, как и в способе 2.

Преимущества Недостатки
  • Накладные расходы на создание делегата, поскольку Expression трудно кэшировать.

Производительность

Вот результаты тестов события с двумя зарегистрированными делегатами (один экземпляр метода и один статический метод):
  • Оригинальные события: 1000000 вызовов, время 0.0278125 секунд.
  • Способ 1: 1000000 вызовов, время 12,1321455 секунд.
  • Способ 2: 1000000 вызовов, время 0,0370756 секунд.
  • Способ 3: 1000000 вызовов, время 0,0692381 секунд.

MugenInjection

MugenInjection также предоставляет методы для создания слабых делегатов, он поддерживает второй и третий способ. Этот код демонстрирует, как это работает:
private void Method()
{
    var beta = new Beta();
    //You can change the type of ReflectionAccessProvider.
    //InjectorUtils.ReflectionAccessProvider = new EmitReflectionAccessProvider();
    //InjectorUtils.ReflectionAccessProvider = new ExpressionReflectionAccessProvider();
    beta.MyEvent += ReflectionExtension.ConvertToWeakDelegate(new Action<object>(OnMyEvent), 
@delegate => beta.MyEvent -= @delegate);    
}

Заключение

Если вы не нуждаетесь в высокой производительности, тогда можно использовать первый способ, в противном случае, лучшим выбором будет второй или третий метод. Третий метод может работать в режиме частичного доверия, что очень важно если вы используете Silverlight.
Кроме того, существует множество различных подходов к этой проблеме, и эта статья рассматривает только некоторые из них.

Комментарии
Оставить комментарий
*bold*
_italics_
+underline+
* Bullet List
** Bullet List 2
# Number List
## Number List 2
{"Do not apply formatting"}
{code:language} code here {code:language}.
Supports: aspx c#, c#, c++, html, sql, xml
[url:http://www.example.com]