Ответ 1
После нескольких исследований и после сбора всех полезных бит информации из ответов, которые были опубликованы здесь, я понял, что не будет элегантного и закрытого решения проблемы. Поскольку это проблема реальной жизни, мы пошли с прагматичным подходом, пытаясь, по крайней мере, уменьшить ее, обработав как можно больше сценариев, поэтому я хотел опубликовать то, что мы сделали.
Более глубокое исследование объекта Action, переданного конструктору WeakEvent, и особенно свойство Action.Target, показало, что есть фактически два разных случая замыкающих объектов.
В первом случае Lambda использует локальные переменные из области вызывающей функции, но не использует никакой информации из экземпляра класса A. В следующем примере предположим EventAggregator.Register - это метод, который выполняет действие и хранит WeakAction, который его обертывает.
public class A
{
public void Listen(int num)
{
EventAggregator.Register<SomeEvent>(_createListenAction(num));
}
public Action _createListenAction(int num)
{
return new Action(() =>
{
if (num > 10) MessageBox.Show("This is a large number");
});
}
}
Созданная здесь лямбда использует переменную num
, которая является локальной переменной, определенной в области функции _createListenAction
. Поэтому компилятор должен обернуть его классом замыкания, чтобы поддерживать переменные замыкания. Однако, поскольку лямбда не имеет доступа ни к одному из членов класса А, нет необходимости хранить ссылку на А. Таким образом, цель действия не будет содержать ссылки на экземпляр А, и нет никакого способа для конструктора WeakAction чтобы достичь этого.
Второй случай показан в следующем примере:
public class A
{
int _num = 10;
public void Listen()
{
EventAggregator.Register<SomeEvent>(_createListenAction());
}
public Action _createListenAction()
{
return new Action(() =>
{
if (_num > 10) MessageBox.Show("This is a large number");
});
}
}
Теперь _num
не предоставляется в качестве параметра функции, он исходит из экземпляра класса A. Использование отражения для изучения структуры объекта Target показывает, что последнее поле, которое определяет компилятор, содержит ссылку на экземпляр класса A. Этот случай также применяется, когда лямбда содержит вызовы методов-членов, как в следующем примере:
public class A
{
private void _privateMethod()
{
// do something here
}
public void Listen()
{
EventAggregator.Register<SomeEvent>(_createListenAction());
}
public Action _createListenAction()
{
return new Action(() =>
{
_privateMethod();
});
}
}
_privateMethod
является функцией-членом, поэтому она вызывается в контексте экземпляра класса A, поэтому закрытие должно содержать ссылку на нее, чтобы вызвать лямбда в правильном контексте.
Итак, первым случаем является Закрытие, которое содержит только локальную переменную функций, вторая содержит ссылку на родительский экземпляр. В обоих случаях нет жестких ссылок на экземпляр Closure, поэтому, если конструктор WeakAction просто оставляет вещи так, как они есть, WeakAction будет "умирать" мгновенно, несмотря на то, что экземпляр класса A все еще жив.
Мы столкнулись с тремя различными проблемами:
- Как определить, что цель действия - вложенное закрытие экземпляр класса, а не исходный экземпляр экземпляра?
- Как получить ссылку на исходный экземпляр класса A?
- Как продлить срок службы экземпляра закрытия так, чтобы он до тех пор, пока существует экземпляр A, но не выше этого?
Ответ на первый вопрос заключается в том, что мы полагаемся на 3 характеристики экземпляра замыкания: - Он является частным (точнее, он не является "видимым". При использовании компилятора С# отраженный тип имеет IsPrivate, установленный в true, но с VB это не так. Во всех случаях свойство IsVisible является ложным). - Он вложен. - Как заметил в своем ответе @DarkFalcon, он украшен атрибутом [CompilerGenerated].
private static bool _isClosure(Action a)
{
var typ = a.Target.GetType();
var isInvisible = !typ.IsVisible;
var isCompilerGenerated = Attribute.IsDefined(typ, typeof(CompilerGeneratedAttribute));
var isNested = typ.IsNested && typ.MemberType == MemberTypes.NestedType;
return isNested && isCompilerGenerated && isInvisible;
}
Хотя это не 100% -ый запечатанный предикат (вредоносный программист может генерировать вложенный частный класс и украшать его атрибутом CompilerGenerated), в реальных сценариях это достаточно точно, и снова мы строим прагматическое решение, а не академический.
Итак, проблема номер 1 решена. Конструктор слабых действий определяет ситуации, когда цель действия является закрытием и отвечает на это.
Задача 3 также легко разрешима. Как написал @usr в своем ответе, как только мы получим экземпляр класса A, добавив ConditionalWeakTable с единственной записью, в которой экземпляр класса A является ключом, а экземпляр замыкания является целью, решает проблему. Сборщик мусора знает, что он не собирает экземпляр замыкания, пока живет экземпляр класса A. Так что хорошо.
Единственная неразрешимая проблема - вторая, , как получить ссылку на экземпляр класса A?Как я уже сказал, есть два случая закрытия. Там, где компилятор создает элемент, который содержит этот экземпляр, и тот, где он не работает. Во втором случае просто нет способа получить его, поэтому единственное, что мы можем сделать, это создать твердую ссылку на экземпляр закрытия, чтобы сохранить его от моментального сбора мусора. Это означает, что он может проживать экземпляр класса А (на самом деле он будет жить до тех пор, пока живет экземпляр WeakAction, который может быть вечно). Но это не такой ужасный случай. Класс замыкания в этом случае содержит только несколько локальных переменных, а в 99,9% случаев это очень малая структура. Хотя это все еще утечка памяти, она не является существенной.
Но для того, чтобы пользователи могли избежать утечки памяти, мы добавили дополнительный конструктор к классу WeakAction следующим образом:
public WeakAction(object target, Action action) {...}
И когда этот конструктор вызывается, мы добавляем запись ConditionalWeakTable, где цель - это ключ, а целью действия является значение. Мы также придерживаемся слабой ссылки как на цель, так и на цель действий, и если кто-либо из них умирает, мы очищаем оба. Таким образом, цель действий не меньше, чем цель. Это в основном позволяет пользователю WeakAction сказать, чтобы он держался за экземпляр замыкания, пока жив мишень. Поэтому новым пользователям будет предложено использовать его, чтобы избежать утечек памяти. Но в существующих проектах, где этот новый конструктор не используется, это, по крайней мере, минимизирует утечки памяти для закрытий, которые не имеют ссылки на экземпляр класса A.
Случай замыканий, которые ссылаются на родителя, более проблематичен, поскольку они влияют на сбор гарбаз. Если мы серьезно относимся к закрытию, мы вызываем гораздо более резкую утечку памяти, потому что экземпляр класса A также никогда не будет очищен. Но этот случай также легче лечить. Поскольку компилятор добавляет последний элемент, который содержит ссылку на экземпляр класса A, мы просто используем рефлексию для ее извлечения и выполняем именно то, что делаем, когда пользователь предоставляет его в конструкторе. Мы идентифицируем этот случай, когда последний член экземпляра замыкания имеет тот же тип, что и тип объявления замыкающего вложенного класса. (Опять же, он не на 100% точнее, но для реальных случаев его достаточно близко).
Подводя итог, решение, представленное здесь, не является 100% -ным запечатанным решением, просто потому, что похоже, что такого решения не существует. Но поскольку мы должны предоставить НЕКОТОРЫЙ ответ на эту досадную ошибку, это решение по крайней мере существенно уменьшает проблему.