Sunday, November 10, 2013

Слабые события в C#

В данной статье я рассмотрю проблемы утечек памяти в программах на C#, а также способы их решения через WeakReference. Так же будут рассмотрены Weak Event Patterns для устранения утечек памяти в Windows Presentation Foundation (WPF).
При использовании событий с использованием ключевого слова event, подписка на событие создает ссылку из объекта, содержащего событие, на объект-подписчик.

Если исходный объект живет дольше, чем объект-подписчик, возможны утечки памяти: при отсутствии других ссылок на подписчика, на него по-прежнему будет ссылаться исходный объект. Поэтому память, которую занимает объект-подписчик, не может быть освобождена сборщиком мусора.

Детально ознакомиться со способами устранения утечек памяти Вы можете в статье Weak Events in C#. Практически 90% решений, связанных с утечками памяти при использовании событий, решаются через WeakReference. Поэтому моя цель показать, как можно этого достичь.
Приведу интересный способ решения (Источник) :
public sealed class WeakEventHandler<TEventArgs> where TEventArgs : EventArgs
{
    private readonly WeakReference _targetReference;
    private readonly MethodInfo _method;

    public WeakEventHandler(EventHandler<TEventArgs> callback)
    {
        _method = callback.Method;
        _targetReference = new WeakReference(callback.Target, true);
    }

    public void Handler(object sender, TEventArgs e)
    {
        var target = _targetReference.Target;
        if (target != null)
        {
            var callback = (Action<object, TEventArgs>)Delegate.CreateDelegate(typeof(Action<object, TEventArgs>), target, _method, true);
            if (callback != null)
            {
                callback(sender, e);
            }
        }
    }
}

Unit test
[TestFixture]
public class WeakEventsTests
{
    #region Example

    public class Alarm
    {
        public event PropertyChangedEventHandler Beeped;

        public void Beep()
        {
            var handler = Beeped;
            if (handler != null) handler(this, new PropertyChangedEventArgs("Beep!"));
        }
    }

    public class Sleepy
    {
        private readonly Alarm _alarm;
        private int _snoozeCount;

        public Sleepy(Alarm alarm)
        {
            _alarm = alarm;
            _alarm.Beeped += new WeakEventHandler<PropertyChangedEventArgs>(Alarm_Beeped).Handler;
        }

        private void Alarm_Beeped(object sender, PropertyChangedEventArgs e)
        {
            _snoozeCount++;
        }

        public int SnoozeCount
        {
            get { return _snoozeCount; }
        }
    }

    #endregion

    [Test]
    public void ShouldHandleEventWhenBothReferencesAreAlive()
    {
        var alarm = new Alarm();
        var sleepy = new Sleepy(alarm);
        alarm.Beep();
        alarm.Beep();

        Assert.AreEqual(2, sleepy.SnoozeCount);
    }

    [Test]
    public void ShouldAllowSubscriberReferenceToBeCollected()
    {
        var alarm = new Alarm();
        var sleepyReference = null as WeakReference;
        new Action(() =>
        {
            // Run this in a delegate to that the local variable gets garbage collected
            var sleepy = new Sleepy(alarm);
            alarm.Beep();
            alarm.Beep();
            Assert.AreEqual(2, sleepy.SnoozeCount);
            sleepyReference = new WeakReference(sleepy);
        })();

        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();

        Assert.IsNull(sleepyReference.Target);
    }

    [Test]
    public void SubscriberShouldNotBeUnsubscribedUntilCollection()
    {
        var alarm = new Alarm();
        var sleepy = new Sleepy(alarm);

        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();

        alarm.Beep();
        alarm.Beep();
        Assert.AreEqual(2, sleepy.SnoozeCount);
    }
}
Как видим из примера, использование WeakReference не составляет труда. Объект сам будет уничтожен, как только на него перестанут ссылаться. 

Шаблоны слабых событий в WPF (Weak Event Patterns)
В WPF для решения проблемы с утечкой памяти при использовании событий представляется шаблон, который можно использовать путем предоставления выделенного класса диспетчера для конкретных событий и реализации интерфейса прослушивателей для данного события. Этот шаблон разработки называется шаблоном слабых событий.
Существует три способа реализации шаблона слабых событий.

Способ №1. Использовать существующий простой класс диспетчера событий
Если событие, на которое следует подписаться, имеет сопоставления WeakEventManager, используйте существующий простой класс диспетчера событий. Список диспетчеров слабых событий, входящих в состав WPF, приведён в разделе иерархии наследования класса WeakEventManager. Обратите внимание, что относительно небольшое количество слабых событий, входящих в состав WPF диспетчеров, скорее всего, будет необходимо выбрать один из прочих подходов.

Способ №2. Использование класса диспетчера как универсальный шаблон слабых событий
Использование шаблона WeakEventManager<TEventSource, TEventArgs> в случае, если WeakEventManager недоступен и приоритетным является простой способ реализации перед эффективностью использования. 
Шаблон WeakEventManager<TEventSource, TEventArgs> менее эффективный, по сравнению с WeakEventManager, а также пользовательским диспетчером слабых событий. Например, универсальный шаблон класса выполняет несколько отражений для обнаружения события по заданному имени события. 

Способ №3. Создание пользовательского класса диспетчера слабых событий
Этот способ подразумевает создание пользовательского менеджера, наследуемого от WeakEventManager, с целью обеспечить наилучшую производительность. Использование пользовательского интерфейса WeakEventManager с тем, чтобы подписаться на событие, является более эффективным, чем использование универсального шаблона слабых событий.
Пример использования:
#region Constructor
public MainWindow()
{
    InitializeComponent();
    WeakEventManager<Window, EventArgs>
            .AddHandler(App.Current.MainWindow, "Activated", MainWindow_Activated);
}
#endregion

void MainWindow_Activated(object sender, EventArgs e)
{
    //Do something here
}

Event Aggregators
Агрегаторы событий (Event Aggregators) предлагают альтернативу традиционным C # событиям, с дополнительным преимуществом полной развязки издателя и подписчиков событий. Каждый, кто может получить доступ к агрегатору события, может опубликовать событие на него, и он не может быть подписан ни от кого другого, имеющего доступ к агрегатору события. 
Как работают Event Aggregators, можно посмотреть здесь.

Источники:
Event Aggregator with Reactive Extensions

No comments:

Post a Comment