Как стать автором
Обновить

Комментарии 35

Спасибо, сам недавно фиксил баги с мемори-ликами в чужом коде. Вот, кстати, неплохая статья, с обзором основных моментов.
Спасибо, хороший списочек.
Для поиска ликов я использую .NET Memory Profiler. В нем видно, кто держит ссылку на объект. Из этого можно понять как от лика избавиться. Обычно это проще и быстрее, чем перебирать возможные варианты.
Да, есть такой инструмент, платный правда.
Я использовал CLRProfiler и SOS.dll.
А список — чтобы понимать в чем принципиально может быть проблема.
А почему в WPF не применяется IDisposable? не знаете?
Могу только догадываться. Думаю, что мелкие хотели как лучше, чтобы никому не приходилось вызывать Dispose ни у кого. Например, при изменении деревьев.
IDisposable задумывался как интерфейс для детерминистического освобождения неуправляемых ресурсов, таких как file-, mutex-handle, sockets.

WPF — это вроде бы целиком managed library с COM-оберткой на DirectX 3D.
Но WPF не ограничивает использование файлов, сокетов или просто подписок на события (мой случай). Соответственно, если у вас есть элемент, который их использует, то вам нужно найти способ узнать, когда ресурсы можно освободить.
Вообще IDisposable автор поста упомянул не к месту. IDisposable нужен тогда, когда есть необходимость управлять временем «жизни», или работоспособности, объекта, не важно оборачивает он неуправляемый ресурс, или нет. С событиями проблема в том, что время существования в памяти одного объекта попадает в нежелательную зависимость от времени существования другого. Тут IDisposable никак не поможет. Единственный выход без использования слабых ссылок — управлять явно подписками на события и заблаговременно отписываться. Но сложность контроля подписок сопоставима по сложности с управлением памятью в неуправляемых средах исполнения. Думаю те, кто писал сложные системы, между частями которых передавались блоки памяти, понимают о чем я.
Тут IDisposable никак не поможет


Отписывание от событий зачастую происходит в Dispose
Ну это уже смотря какие паттерны использовать. Мне, например, ни разу не приходилось отписываться от событий из реализации Dispose, я предпочитаю классы, которые реализуют IDisposeable делать пассивными.
Что подразумевается под «пассивным классом»?
В принципе можно добавить отдельный интерфейс с методом Unsubscribe, чтобы уведомить класс, что ему надо отписаться от событий, но поскольку IDisosable и так поддерживается во многих местах, то проще отписываться именно в Dispose.
Под пассивным я имел ввиду класс, в котором реализован минимум логики по обработке собственных данных. Например реализация IEnumerator.
К сожалению, не всегда класс сам может узнать когда он уже не нужен, соответственно его об этом надо уведомить.
Да, с этой проблемой мне сталкиваться приходилось. Хотелось бы сделать небольшую поправку, касательно RoutedEvent — поддержка вводится в классе UIElement, а не в классе DependencyObject.
Еще одна довольно забавная штука — у Microsoft есть еще одна реализация слабых событий — CommandManager.InvalidateRequerySuggested.
Забавно то, что эта реализация хотя и повсеместно используется в WPF, но содержит ошибку — в WeakReferenc'ы оборачиваются сами делегаты, а не их Target'ы. Похоже, что разработчики WPF нашли ошибку слишком поздно, и вместо переделки решили вставить костыль — каждый кто подписывается на InvalidateRequerySuggested обязан сохранять сильную ссылку на делегат в экземпляре объекта…
Ну а я, как обычно, пошел своим путём, и реализовал класс слабого делегата при помощи малоизвестной фичи .net framework под названием «не привязанный делегат» :) Но подожду того, что напишет ivann, сравним.
Если вы имеете ввиду unbound delegate, то их в C# как бы еще нет.
Ну да, делегаты на статические методы есть с .NET 2.0. Не догадался, что вы именно их подразумевали.

А каким макаром они позволяют решить проблему жестких ссылок на события?

Почитайте справку повнимательнее. Фраза «Threat method as static» не обозначает, что нельзя с экземплярными методами работать как со статическими.
Да, вы правы UIElement. Спасибо.
Да, можно поподробнее на тему как в делегате обернуть target в WeakReference?
Пишу не в редакторе, могут быть синтаксические ошибки.

object o = new object();
MethodInfo mi = new Func(o.ToString).Method; // Работает быстрее рефлексии.
Func<object, string> unboundDelegate = (Func<object, string>)Delegate.CreateDelegate(
typeof(Func<object, string>), null, mi);
WeakReference wr = new WeakReference(o);
o = null; // Больше на объект ссылок нет!

// Попытка вызова выглядит так:
object strongRef = wr.Target;
if (null != strongRef) Console.WriteLine(unboundDelegate(strongRef));
Это частный случай, когда у вас есть живые методы и полный контроль над вызовом делегата.

А как быть, если у вас уже есть EventHandler, который надо преобразовать в weak EventHandler.

Помимо этого, как быть, когда ваш WeakEventHandler обнаруживает свой target в мусорке? Как от события отписываться, если не знаешь, что это за событие и где?
Ну как, просто всё. Все названные задачи решаются, просто нужно написать класс инкапсулирующий список пар WeakDeference+Delegate (unbound). Я думаю очевидно как… По поводу EventHandler — это Delegate, а у делегата есть свойства Target и Method, а собственно больше ничего не надо. Минус в том, что для того, чтобы подписаться на слабое событие нужно создать экземпляр делегата.
В итоге я написал шаблон, по которому генерируются классы слабых шаблонных делегатов под заданное число параметров. Типа как WeakAction<T1...Tn>.
плюсанул, добавил в избранное, теперь у меня есть весомый аргумент в холиварах C++ vs C#…
С# обратно пропорциональный язык: простые вещи в нем делаются сложно, но при этом сложные — просто…
сори за офтоп.
Зачем же холивары? Это просто разные языки с разной областью применения.
я знаю… но судя по неделям ненависти к С++, и постоянным холиварам на ГК, это знают не все. и основной аргумент используемый оппонентами это отсутствие утечек памяти…
Ха, нет… Так могут говорить только те, кто ничего серьезного не писал. Другое дело, что порой их проще находить, но это уже вопрос отдельный.
И вообще я, как человек, который поровну писал и пишет на плюсах и шарпе, смотрю на эти холивары с улыбкой:)
А в C++ отписка от событий как происходит? Как-то еще проще?
Просто в с++ легко наделать ликов и без событий;)
А что уважаемое сообщество скажет на счет того, что утечки памяти от неотписанных событий — миф?
Только что наткнулся в msdn на msdn.microsoft.com/en-us/library/ms366768.aspx

The Memory Leak Myth
A popular myth is that events cause memory leaks because the listener keeps the source open. This is not true. Although you should always strive to unsubscribe from events, it's not always possible, and it would often mean trying to force many classes to be IDisposable.

Ian Griffiths demonstrates how objects are correctly GCed even when events are left with subscribers, here:

www.interact-sw.co.uk/iangblog/2004/07/07/circulareventrefs

The myth echoes around the internet and even comes from Microsoft (MSDN) themselves:

Ordinary events in the common language runtime (CLR) 2.0 are bidirectional strong references between the event source and the listener and as such can keep an object (either source or listener) alive that otherwise should be dead already. This is why a WeakEvent class was added in the .NET Framework 3.0. It is important that you become familiar with the WeakEvent pattern, which is not yet well known, but is required to implement the Observer pattern successfully. The WeakEvent pattern has been used in the Windows® Presentation Foundation (WPF) Data Binding implementation to prevent leakage due to data binding.

The bold in the pullquote above is wrong. You can use the VS debugger to look at the pack of delegates inside the handler and see there's only a one-way reference to the Target method.

Even circular references are okay; as long as there are no reference to the objects referencing each other then they are seen as an island and they'll be collected.
накидал такой код для проверки, действительно, объект нормально удаляется коллектором, утечек нет:

using System;

namespace ConsoleApplication1
{
  /// <summary>
  /// то что должно остаться живым, так как есть подписчики
  /// </summary>
  class A
  {
    public event EventHandler Event = delegate { };

    public void Fire()
    {
      Event(this, null);
    }

    ~A()
    {
      Console.WriteLine(«a finalized»);
    }
  }

  /// <summary>
  /// слушатель события
  /// </summary>
  class B
  {
    public void f(A a)
    {
      if (a != null)
      {
        Console.WriteLine(«subscrising»);
        a.Event += new EventHandler(a_H);
      }
    }

    void a_H(object sender, EventArgs e)
    {
      Console.WriteLine(«event rised»);
    }

    ~B()
    {
      Console.WriteLine(«b finalized»);
    }
  }

  class Program
  {
    static void Main(string[] args)
    {
      A a = new A();
      B b = new B();
      
      // подписываемся на событие
      // в теории обект А должен оставться живым даже после сборки мусора,
      // так как объект B подписался на его событие
      b.f(a);
      a.Fire();

      // а — мусор
      a = null;

      // собираем мусор
      GC.Collect();
      GC.WaitForPendingFinalizers();
      GC.Collect();
      Console.WriteLine(«collected»);

      // видим:
      // subscrising
      // event rised
      // a finalized
      // collected

      // финализатор для А вызвался, хоть в В есть ссылка на событие из А

      // а — все равно мусор, ничего не происходит
      b.f(a);
      Console.ReadKey();

      // после завершения вызовется финализатор В
    }
  }
}

* This source code was highlighted with Source Code Highlighter.

где ошибка?
В вашем коде утечки нет, т.к. «a» держит ссылку на «b», а не наоборот. Попробуйте обнулить «b», вместо «a» и вы увидите, что «b» не соберется.
спасибо, понятно.
в общем, листенер события (В) действительно не держит объект с событием (А), как сказано в msdn.
но объект с событием (А) удерживает листенера (В), что логично, так как когда событие срабатывает, все, что на него подписалось должно отработать. там может быть задумано, листенер не должен чиститься.
ошибка — ожидать, что это должно произойти.
По английски всё правильно написано, читайте внимательнее.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий