Сборка мусора при использовании анонимных делегатов для обработки событий - PullRequest
13 голосов
/ 16 декабря 2008

ОБНОВЛЕНИЕ

Я объединил различные ответы отсюда в «окончательный» ответ на новый вопрос .

Оригинальный вопрос

В моем коде есть издатель событий, который существует на протяжении всего времени жизни приложения (здесь он сокращен до базовых):

public class Publisher
{
    //ValueEventArgs<T> inherits from EventArgs
    public event EventHandler<ValueEventArgs<bool>> EnabledChanged; 
}

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

public static class Linker
{
    public static void Link(Publisher publisher, Control subscriber)
    {
         publisher.EnabledChanged += (s, e) => subscriber.Enabled = e.Value;
    }

    //(Non-lambda version, if you're not comfortable with lambdas)
    public static void Link(Publisher publisher, Control subscriber)
    {
         publisher.EnabledChanged +=
             delegate(object sender, ValueEventArgs<bool> e)
             {
                  subscriber.Enabled = e.Value;
             };
    }
}

Он работал нормально, пока мы не начали использовать его на небольших машинах, когда я начал получать случайные:

System.ComponentModel.Win32Exception
Not enough storage is available to process this command

Как оказалось, в коде есть одно место, где элементы управления подписчиками динамически создаются, добавляются и удаляются из формы. Учитывая мое глубокое понимание процесса сбора мусора и т. Д. (Т. Е. Ни одного, до вчерашнего дня), я никогда не думал о том, чтобы уйти от меня, поскольку в подавляющем большинстве случаев подписчики также живут в течение всего срока службы приложения.

Некоторое время я возился с WeakEventHandler Дастина Кэмпбелла , но он не работает с анонимными делегатами (не для меня в любом случае).

Есть ли выход из этой проблемы? Я действительно хотел бы избежать необходимости копировать и вставлять код котельной пластины по всему магазину.

(О, и не спрашивайте меня, ПОЧЕМУ мы все время создаем и уничтожаем элементы управления, это не было моим дизайнерским решением ...)

(PS: это приложение winforms, но мы обновились до VS2008 и .Net 3.5, стоит ли мне использовать шаблон Weak Event ?)

(PPS: Хороший ответ от Рори , но если кто-то может придумать эквивалент WeakEventHandler, который избавляет меня от необходимости не забывать явно UnLink / Dispose, это было бы круто ...)

РЕДАКТИРОВАТЬ Я должен признать, что обошел эту проблему, "переработав" рассматриваемые элементы управления. Однако обходной путь вернулся, чтобы преследовать меня, поскольку «ключ», который я использовал, по-видимому, не уникален (всхлип). Я только что обнаружил другие ссылки здесь (пробовал это - кажется немного слишком слабым - GC очищает делегатов, даже если цель все еще жива, та же проблема с s, oɔɯǝɹ ответьте ниже), здесь (вынуждает вас изменять издателя, и на самом деле не работает с анонимными делегатами) и здесь (цитируется Дастином Кэмпбеллом как неполное) ,

Мне приходит в голову, что то, что я ищу, может быть семантически невозможным - затворы предназначены для того, чтобы «торчать даже после моего ухода».

Я нашел другой обходной путь, поэтому я буду придерживаться этого, ожидая голоса от богов .

Ответы [ 4 ]

5 голосов
/ 19 сентября 2009

Я знаю, что этот вопрос древний, но, черт возьми, я его нашел, и думаю, что другие тоже могут. Я пытаюсь решить связанную проблему, и, возможно, есть некоторое понимание.

Вы упомянули WeakEventHandler Дастина Кэмпбелла - он действительно не может работать с анонимными методами. Я пытался совместить что-то, что могло бы произойти, когда я понял, что а) в 99% случаев мне нужно что-то подобное, его первоначальное решение будет безопаснее, и б) в тех немногих случаях, когда мне нужно чтобы, а не «хочу, потому что лямбды намного красивее и лаконичнее») можно заставить его работать, если вы станете немного умнее.

Ваш пример выглядит как единовременный случай, когда немного сложнее может привести к довольно краткому решению.


public static class Linker {
    public static void Link(Publisher publisher, Control subscriber) {
        // anonymous method references the subscriber only through weak 
        // references,so its existance doesn't interfere with garbage collection
        var subscriber_weak_ref = new WeakReference(subscriber);

        // this instance variable will stay in memory as long as the  anonymous
        // method holds a reference to it we declare and initialize  it to 
        // reserve the memory (also,  compiler complains about uninitialized
        // variable otherwise)
        EventHandler<ValueEventArgs<bool>> handler = null;

        // when the handler is created it will grab references to the  local 
        // variables used within, keeping them in memory after the function 
        // scope ends
        handler = delegate(object sender, ValueEventArgs<bool> e) {
            var subscriber_strong_ref = subscriber_weak_ref.Target as Control;

            if (subscriber_strong_ref != null) 
                subscriber_strong_ref.Enabled = e.Value;
            else {
                // unsubscribing the delegate from within itself is risky, but
                // because only one instance exists and nobody else has a
                // reference to it we can do this
                ((Publisher)sender).EnabledChanged -= handler;

                // by assigning the original instance variable pointer to null
                // we make sure that nothing else references the anonymous
                // method and it can be collected. After this, the weak
                //  reference and the handler pointer itselfwill be eligible for
                // collection as well.
                handler = null; 
            }
        };

        publisher.EnabledChanged += handler;
    }
}

Ходят слухи, что шаблон слабых событий WPF сопряжен с большими накладными расходами, поэтому в этой конкретной ситуации я бы его не использовал. Более того, ссылки на основную библиотеку WPF в приложении WinForm также кажутся немного тяжелыми.

4 голосов
/ 16 декабря 2008

Если вы сохраняете ссылку на анонимного делегата, а затем удаляете ее, когда элементы управления удаляются из формы, что должно позволить собирать мусор как элементам управления, так и анонимным делегатам.

Так что-то вроде этого:

public static class Linker
{

    //(Non-lambda version, I'm not comfortable with lambdas:)
    public static EventHandler<ValueEventArgs<bool>> Link(Publisher publisher, Control subscriber)
    {
         EventHandler<ValueEventArgs<bool>> handler = delegate(object sender, ValueEventArgs<bool> e)
             {
                  subscriber.Enabled = e.Value;
             };
         publisher.EnabledChanged += handler;
         return handler;
    }

    public static void UnLink(Publisher publisher, EventHandler<ValueEventArgs<bool>> handler)
    {
        publisher.EnabledChanged -= handler;
    }

}

См. Отмена подписки анонимного метода в C # для примера удаления делегатов.

1 голос
/ 05 июня 2009

Пример кода, который я недавно сделал на основе WeakReference:

// strongly typed weak reference
public class WeakReference<T> : WeakReference
    where T : class
{
    public WeakReference(T target)
        : base(target)
    { }

    public WeakReference(T target, bool trackResurrection)
        : base(target, trackResurrection)
    { }

    public new T Target
    {
        get { return base.Target as T; }
        set { base.Target = value; }
    }
}

// weak referenced generic event handler
public class WeakEventHandler<TEventArgs> : WeakReference<EventHandler<TEventArgs>>
    where TEventArgs : EventArgs
{
    public WeakEventHandler(EventHandler<TEventArgs> target)
        : base(target)
    { }

    protected void Invoke(object sender, TEventArgs e)
    {
        if (Target != null)
        {
            Target(sender, e);
        }
    }

    public static implicit operator EventHandler<TEventArgs>(WeakEventHandler<TEventArgs> weakEventHandler)
    {
        if (weakEventHandler != null)
        {
            if (weakEventHandler.IsAlive)
            {
                return weakEventHandler.Invoke;
            }
        }

        return null;
    }
}

// weak reference common event handler
public class WeakEventHandler : WeakReference<EventHandler>
{
    public WeakEventHandler(EventHandler target)
        : base(target)
    { }

    protected void Invoke(object sender, EventArgs e)
    {
        if (Target != null)
        {
            Target(sender, e);
        }
    }

    public static implicit operator EventHandler(WeakEventHandler weakEventHandler)
    {
        if (weakEventHandler != null)
        {
            if (weakEventHandler.IsAlive)
            {
                return weakEventHandler.Invoke;
            }
        }

        return null;
    }
}

// observable class, fires events
public class Observable
{
    public Observable() { Console.WriteLine("new Observable()"); }
    ~Observable() { Console.WriteLine("~Observable()"); }

    public event EventHandler OnChange;

    protected virtual void DoOnChange()
    {
        EventHandler handler = OnChange;

        if (handler != null)
        {
            Console.WriteLine("DoOnChange()");
            handler(this, EventArgs.Empty);
        }
    }

    public void Change()
    {
        DoOnChange();
    }
}

// observer, event listener
public class Observer
{
    public Observer() { Console.WriteLine("new Observer()"); }
    ~Observer() { Console.WriteLine("~Observer()"); }

    public void OnChange(object sender, EventArgs e)
    {
        Console.WriteLine("-> Observer.OnChange({0}, {1})", sender, e);
    }
}

// sample usage and test code
public static class Program
{
    static void Main()
    {
        Observable subject = new Observable();
        Observer watcher = new Observer();

        Console.WriteLine("subscribe new WeakEventHandler()\n");
        subject.OnChange += new WeakEventHandler(watcher.OnChange);
        subject.Change();

        Console.WriteLine("\nObserver = null, GC");
        watcher = null;
        GC.Collect(0, GCCollectionMode.Forced);
        GC.WaitForPendingFinalizers();

        subject.Change();

        if (Debugger.IsAttached)
        {
            Console.Write("Press any key to continue . . . ");
            Console.ReadKey(true);
        }
    }
}

Создает следующий вывод:

new Observable()
new Observer()
subscribe new WeakEventHandler()

DoOnChange()
-> Observer.OnChange(ConsoleApplication4.Observable, System.EventArgs)

Observer = null, GC
~Observer()
DoOnChange()
~Observable()
Press any key to continue . . .

(обратите внимание, что отписаться (- =) не работает)

0 голосов
/ 06 ноября 2009

Далее, основываясь на ответе Егора , я хотел попробовать создать версию, в которой мне не нужно было заранее определять, к какому событию я хочу присоединиться.

Мне удалось заставить его работать только с универсальными обработчиками событий: для «стандартных» обработчиков событий (например, FormClosingEventHandler) это немного сложно, потому что вы не можете иметь ограничение типа where T : delegate (если только ваше имя заканчивается пони ).

private static void SetAnyGenericHandler<S, T>(
     Action<EventHandler<T>> add,     //to add event listener to publisher
     Action<EventHandler<T>> remove,  //to remove event listener from publisher
     S subscriber,                    //ref to subscriber (to pass to consume)
     Action<S, T> consume)            //called when event is raised*
         where T : EventArgs 
         where S : class
{
    var subscriber_weak_ref = new WeakReference(subscriber);
    EventHandler<T> handler = null;
    handler = delegate(object sender, T e)
    {
        var subscriber_strong_ref = subscriber_weak_ref.Target as S;
        if(subscriber_strong_ref != null)
        {
            Console.WriteLine("New event received by subscriber");
            consume(subscriber_strong_ref, e);
        }
        else
        {
            remove(handler);
            handler = null;
        }
    };
    add(handler);
}

(* Я попытался EventHandler<T> consume здесь, но вызывающий код становится уродливым, потому что вы должны приводить s к Subscriber в лямбде-потреблении.)

Пример кода вызова, взятый из примера выше:

SetAnyGenericHandler(
    h => publisher.EnabledChanged += h, 
    h => publisher.EnabledChanged -= h, 
    subscriber, 
    (Subscriber s, ValueEventArgs<bool> e) => s.Enabled = e.Value);

Или, если вы предпочитаете

SetAnyGenericHandler<Subscriber, ValueEventArgs<bool>>(
    h => publisher.EnabledChanged += h, 
    h => publisher.EnabledChanged -= h, 
    subscriber, 
    (s, e) => s.Enabled = e.Value);

Было бы неплохо иметь возможность передавать Событие как один параметр, но вы не можете получить доступ к добавлению / удалению из события так же, как вы можете получить доступ к извлечению / установке из свойства (без выполнения отвратительного отражения) Я думаю).

...