目录
介绍
在.NET中的经典内存泄漏模式中,您将一个短生命周期的对象注册到一个长生命周期的对象中,并且不能(或忘记)在其生命周期即将结束之前注销它。在这里,我使用弱引用来实现一个可自我清理的集合和一个可自我清理的事件,以解决此问题(并最终创建一些其他问题)。
总览
.NET中的弱引用(以防万一)是一个类,该类包装对您可能会委托给它的对象的引用,但不会增加被包装实例的引用计数器。如果在以后的某个时刻,您想获得包装对象的结果取决于它,那么是否仍然在应用程序的其他地方引用了该对象(强烈的含义)。在前一种情况下,您只是让您的心上人回来,而在后一种情况下,则不确定。如果在此期间对其进行了垃圾回收,您将一无所获。“对不起,它不见了”。这样,使用弱引用可能会在程序中带来一些不确定性。
考虑到这一点,我实现了一个集合(准确地说是一个可枚举的),可以在其中存储引用而不会影响它们的引用计数器:
public class SelfCleanableCollection<T> : IEnumerable<T> where T class
{
public void Add(T obj){...}
public void Add(IEnumerable<T> objs){...}
public void Clear(){...}
public void RemoveFirst(T obj){...}
public void RemoveLast(T obj){...}
public void RemoveAllt(T obj){...}
// and the IEnumerable<T> stuff
}
另一个具有相似动机的类是SelfCleanableEventHost<T>,其注册事件处理程序并且与SelfCleanableCollection<T>相同,不会阻止它们被垃圾收集:
public class SelfCleanableEventHost<T>
{
public event EventHandler<T> Event
{
add{..}
remove{..}
}
public event Func<object, T, Task> EventAsync
{
add{..}
remove{..}
}
public void Add(IEnumerable<T> objs){...}
public void Raise(T arg){...}
public void Raise(object sender, T arg){...}
public async Task RaiseAsync(T arg){...}
public async Task RaiseAsync(object sender, T arg){...}
}
在这两个类中,如果已对存储的实例(例如,事件订阅者)进行了垃圾回收,则将自动清理其存储位置。
它的核心
这两个类的自我清洁行为的核心是:
internal class SelfCleanableEnumerator<S, W> : IEnumerator<S> where S : class
{
internal SelfCleanableEnumerator(Func<W, S> toStrongFunc,
Func<S, W> toWeakFunc,
Func<S, W, bool> matchFunc)
{
this.list = new List<W>();
this.strongReferenceFunc = toStrongFunc;
this.weakReferenceFunc = toWeakFunc;
this.matchFunc = matchFunc;
}
}
在这里,我们有两种参数类型:
- S (strong)代表用户认为他们存储在枚举器中的引用类型,
- W (weak)是真正存储的对象的类型
三个传递函数,如他们的名字所暗示的,S和W类型之间的转换器,和它们之间的equality函数。
使用泛型类型W而不仅仅是System.WeakReference是因为SelfCleanableEventHost<T>中的重用,在SelfCleanableEventHost<T>中,事件处理程序存储为类型W的对象,实际上是一对<WeakReference, MethodInfo>。
自清洁行为在SelfCleanableEnumerator<S, W>.MoveNext()中实现,很简单:
public bool MoveNext()
{
while (this.counter < this.list.Count)
{
S currentStrong = this.strongReferenceFunc(this.list[this.counter]);
if (currentStrong != null)
{
this.Current = currentStrong;
this.counter++;
return true;
}
else
{
this.list.RemoveAt(this.counter);
}
}
this.Current = default;
return false;
}
自清洁集合
SelfCleanableCollection<T> 是有关如何使用上述枚举器的简单示例。它的类型W为System.WeakReference,因此更易于理解:
public class SelfCleanableCollection<T> :
SelfCleanableCollection<T, WeakReference> where T : class
{
public SelfCleanableCollection() : base(el => (T)el.Target, //to-strong: get the
//wrapped object from the weak reference
el => new WeakReference(el), // to-weak: wrap the
//object into a weak reference
(s, w) => s == w.Target) // match: compare
// strong references
{}
}
因此,to-strong、to-weak和match函数都是很简单的。to-weak函数指定了无引用计数器存储机制,因此可以对存储的对象进行静默垃圾收集。
神秘的class SelfCleanableCollection<T, W>只是一个IEnumerable<T>的实现,它有一个SelfCleanableEnumerator<S, W>的实例(参见上文),并将IEnumerable<T>调用路由到包含的枚举器。
自清洁事件托管
就我而言,这是所有这一切中最有趣的部分。首先,因为事件托管假装复制托管事件行为(我的随想)。为类似托管的递归/并发订阅/取消订阅编写测试是一个特殊的挑战。如果您有足够的耐心去看一下单元测试,那么不要惊讶地找到一些可以实际检查托管.NET行为的对象。
不过,让我们从不太复杂的东西开始:
public class SelfCleanableEventHost<TEventArgs>
{
public SelfCleanableEventHost()
{
this.subscribers =
new SelfCleanableCollection<StrongSubscriber, WeakSubscriber>(w => ToStrong(w),
s => ToWeak(s),
(s,w)=> s.Reference ==
w.Reference.Target);
}
}
在这里,StrongSubscriber 仅仅是一对<context, method>:
internal class StrongSubscriber
{
public StrongSubscriber(object context, MethodInfo methodInfo)
{
this.Reference = context;
this.Method = methodInfo;
}
public object Reference { get; }
public MethodInfo Method { get; }
}
WeakSubscriber 是将上下文(调用目标)存储为弱引用的对象:
internal class WeakSubscriber
{
public WeakSubscriber(object context MethodInfo methodInfo)
{
this.Reference = new WeakReference(context);
this.Method = methodInfo;
}
public WeakReference Reference { get; }
public MethodInfo Method { get; }
}
正如我们在关于SelfCleanableEnumerator<S, W>的讨论中所看到的,对于自清洁行为, 如果弱引用的对象已被垃圾回收,则to-strong函数返回null就足够了。它确实:
private static StrongSubscriber ToStrong(WeakSubscriber wr)
{
var context = wr.Reference.Target;
if (context != null)
{
return new StrongSubscriber(context, wr.Method);
}
else
{
return null;
}
}
不幸的是,我没有在单元测试中证明事件托管类的自清洁行为,就像我为集合所做的那样。“垂死的”事件订阅者仍未被收集,因此相关的单元测试被归为[Ignore]。但是,类似的集合测试也可以正常工作。此外,事件托管的自清洁行为在相应的演示应用程序(可下载)中按预期工作。
事件处理程序的注册和注销发生在AddInvocations中:
private void AddInvocations(Delegate[] invocations)
{
lock (this.syncObject)
{
foreach (var del in invocations.Where(el => el != null))
{
var target = del.Target;
if (target != null)
{
subscribers.Add((new StrongSubscriber(target, del.GetMethodInfo())));
}
else
{
var obj = new object();
this.staticSubscribers.Add(obj);
this.subscribers.Add((new StrongSubscriber(obj, del.GetMethodInfo())));
}
}
}
}
和RemoveInvocations中(分别的):
private void RemoveInvocations(Delegate[] invocations)
{
lock (this.syncObject)
{
foreach (var del in invocations.Where(el => el != null))
{
var target = del.Target;
if (target != null)//is non-static
{
var toRemove = this.subscribers
.LastOrDefault(x =>
object.ReferenceEquals(x.Reference, del.Target) &&
x.Method == del.GetMethodInfo());
if (toRemove != null)
this.subscribers.RemoveLast(toRemove);
}
else//is static
{
var toRemove = this.subscribers.LastOrDefault
(el => el.Method == del.GetMethodInfo());
if (toRemove != null)
{
this.subscribers.RemoveLast(toRemove);
this.staticSubscribers.Remove(toRemove.Reference);
}
}
}
}
}
请注意,静态处理程序(也应与它们一起使用,尽管在应用程序退出之前不会收集静态订阅者)具有null 调用目标(即上下文)。因此,为了在调用时易于识别,将它们存储在单独的列表中。
为了具有自然外观,事件托管类公开了两个事件,它们共享相同的调用列表:
public event EventHandler<TEventArgs> Event
{
add { AddInvocations(value.GetInvocationList()); }
remove { RemoveInvocations(value.GetInvocationList());}
}
public event Func<object, TEventArgs, Task> EventAsync
{
add { AddInvocations(value.GetInvocationList()); }
remove { RemoveInvocations(value.GetInvocationList());}
}
实现后一个事件的原因是,为了等待,事件处理程序应返回Task。经典事件处理程序不返回任何内容,因为通常来说,很难就如何聚合来自多个处理程序的返回值达成合理的共识。但是在async Task处理程序的特定情况下,(至少)有一个,名为when-all:
public async Task RaiseAsync(object sender, TEventArgs args)
{
StrongSubscriber[] snapshotSubscribers = null;
object[] snapshotStaticSubscribers = null;
lock (this.syncObject)
{
snapshotSubscribers = this.subscribers.ToArray();
snapshotStaticSubscribers = this.staticSubscribers.ToArray();
}
var tasks = new List<Task>();
foreach (var subscriber in snapshotSubscribers.Where(el=> el != null))
{
var context = subscriber.Reference;
Task ret = default;
if (!this.staticSubscribers.Contains(context))
{
ret = subscriber.Method.Invoke(context, new[] { sender, args }) as Task;
}
else
{
ret = subscriber.Method.Invoke(null, new[] { sender, args }) as Task;
}
if (ret != null)
{
tasks.Add(ret);
}
}
await Task.WhenAll(tasks);
}
正如从该代码得出的那样,在此方法内,经典订阅者和等待订阅者都按其注册顺序被调用。
在调用之前获取静态和非静态调用列表的快照是为了在递归订阅/取消订阅的情况下提供类似托管的行为。
所有其他Raise重载都路由到上述方法:
public void Raise(TEventArgs args)
{
Raise(this, args);
}
public void Raise(object sender, TEventArgs args)
{
RaiseAsync(sender, args).Wait();
}
public async Task RaiseAsync(TEventArgs args)
{
await RaiseAsync(this, args);
}
捕获的上下文可以存活多长时间?
好吧,与其他任何对象一样,只要被引用即可。但是,我们无法手动捕捉捕获的上下文。因此,如果您使用lambda表达式作为此事件托管的事件处理程序,如下所示:
var target = new SelfCleanableEventHost<int>();
target.Event += el => Console.WriteLine(el);
// and, to make the thing worse,
return Task.FromResult(target);
并在返回后将其与一些异步行为结合在一起,可能会发生在Raise(...)调用失败的情况中。
确实,在上述代码的第二行中的某个地方,创建了一个捕获的上下文对象。然后将其作为调用目标传递到事件主机,包装为弱引用,并且...不再引用。因此,将尽早对其进行垃圾收集。确实,这是一个搞笑的错误。
使用代码
可下载的代码包括带有上述类源代码的VS2019解决方案,两个图形(WPF)应用程序,这些应用程序演示了自清洁行为以及同步和异步事件调用的工作方式。好奇的读者可以检查单元测试以获取更多详细信息。
有什么价值?
好吧,SelfCleanableEventHost<T>的应用价值是有限的,即因为无法预测,哪个垂死的订阅者将进入下一个Raise(...),而哪个则不会。除非您有特殊情况,否则情况并不重要。我用它来解决一些旧代码问题。除此之外,我将其视为“C# fantasy”。