Самостоятельная ссылка в делегатах слушателя событий (C #) - PullRequest
3 голосов
/ 21 июля 2011

Возможно, это вопрос семантики, но, возможно, нет, поэтому я спрашиваю: есть ли заметная разница в следующих 2 фрагментах?

public Parent()
{
    Child newChild = new Child();
    newChild.RequestSpecialEvent += (sender, e) =>
    {
        newChild.DoMagic();
    }
}

или

public Parent()
{
    Child newChild = new Child();
    newChild.RequestSpecialEvent += (sender, e) =>
    {
        ((Child)sender).DoMagic();
    }
}

ОчевидноеРазница заключается в том, что вариант 1 сам ссылается на себя, тогда как вариант 2 выполняет приведение объекта.С точки зрения производительности, я ожидаю, что приведение будет более дорогим.

Тем не менее, я предполагаю, что в варианте 1 технически это «Parent», который содержит ссылку на «newChild» (через делегат, определенный в Parent), поэтому, даже если newChild пропадает (newChild = null или что-то подобное), объект newChild не может быть собран сборщиком мусора (gc'ed), потому что Parent определил делегат, который все еще присоединен к нему.newChild может быть только gc'ed, когда Parent в конечном счете уходит.

Однако, в варианте 2, Parent никогда не создает такую ​​«жесткую ссылку» на newChild, поэтому, когда происходит newChild = null, это действительно может быть gc 'немедленно.

Я предпочитаю вариант 1 из-за его краткости и читабельности, но беспокоюсь, что вариант 2 будет лучше.Мысли?Правильна ли моя теория или нет?Есть ли альтернативный или более предпочтительный подход (со здравым смыслом) для объявления того же отношения слушателя событий с родительскими / дочерними классами?

Ответ на @StriplingWarrior:

ВЧто касается сбора мусора, я все еще немного скептически.Делегат ссылается на newChild, поэтому мне кажется, что newChild не может уйти, пока делегат не уйдет.Теперь делегат уйдет, если newChild уйдет ... но все равно newChild не сможет уйти, пока делегат не уйдет!Кажется круглым (почти).Похоже, это должно было бы произойти:

//newChild = null;
//This alone won't truly free up the newChild object because the delegate still
//points to the newChild object.

//Instead, this has to happen
newChild.RequestSpecialEvent = null; //destroys circular reference to newChild
newChild = null; //truly lets newChild object be gc'd

Или, может быть, сказав 'newChild = null;'только newChild.RequestSpecialEvent перестает указывать на делегата, что позволяет делегату уходить, а затем позволяет newChild уходить?Может быть, я только что заговорил сам в свой ответ.:)

Ответы [ 2 ]

2 голосов
/ 27 июля 2011

Циркулярные ссылки не являются проблемой для .Net GC, так как он не использует подсчет ссылок для идентификации живых объектов. GC идентифицирует ссылки, которые гарантированно все еще используются, называемые корнями (например, статические ссылки, ссылки на стек выполняемых в настоящее время методов и т. Д.). Объекты, к которым относятся эти корни, гарантированно будут живыми. Транзитивно объекты, на которые ссылаются эти объекты, гарантированно будут живыми. Поэтому GC может проследить путь от корней до всех объектов, о которых известно, что они живы. Остальные мертвы.

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

Родительский класс в вашем примере не хранит никаких ссылок на newChild - он просто создается как локальный в конструкторе Parent. Таким образом, он должен иметь право на сбор сразу после назначения обработчика события.

Это хорошая статья о ГХ: http://msdn.microsoft.com/en-us/magazine/bb985010.aspx

2 голосов
/ 21 июля 2011

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

Эти два фрагмента функционально эквивалентны: в варианте 1 используется немного больше памяти из-за дополнительной ссылки в делегате, но при вызове происходит немного быстрее, поскольку он избегает приведения.Разница в любом случае настолько незначительна, что я бы предложил использовать то, что кажется вам более чистым (# 1, если вы спросите меня).

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

var handler = new RequestSpecialEventHandler((sender, e) =>
    {
        ((Child)sender).DoMagic();
    });
foreach(var child in children)
{
    child.RequestSpecialEvent += handler;
}

И еще одно предостережение, о котором следует помнить, это то, что вариант № 1 ссылается на переменную newChild, если значение этой переменной изменяетсяпозже в методе новое значение будет использовано при вызове обработчика.Например, в этом примере:

foreach(var child in children)
{
    child.RequestSpecialEvent += (sender, e) =>
        {
            // BEWARE: Don't do this! Modified closure!
            child.DoMagic();
        };
}

... Каждый раз, когда событие запускается для любого из этих дочерних элементов, «Магия» будет выполняться N раз только для последнего дочернего элемента в children коллекция .

...