有没有一种方法可以让C#订阅对象在不直接知道订阅对象的情况下取消订阅事件
本文关键字:对象 事件 取消 情况下 方法 一种 有没有 | 更新日期: 2023-09-27 18:28:42
我有一个用户控件,它将inner
对象列表订阅到另一个组件中的对象。有时我想根据用户控件的属性设置更改来删除inner
对象。如果我在不取消订阅的情况下盲目地将它们从列表中删除,我认为这将导致孤立inner
对象的内存泄漏。
有时我可能无法访问我们订阅时的对象列表。例如,我订阅的项目列表可能已更改,这也是我取消订阅的原因之一。
我当然可以在代码中为每个内部对象添加对订阅对象的引用,但我很感兴趣的是,看看是否有一个内置机制可以利用。
更新#1:汉斯要求提供一些代码。我制作了一个样本,我认为该样本显示,当列表被清除时,innerObject
没有发布。我认为这相当于将它们的引用设置为null。
/*
* Program that demonstrates that event subscribers stay alive after
* loosing scope.
*
* The question is asking if I don't have a reference to myObject is there a way to
* unsubscribe with what the innerObject knows natively.
*
* In the Unsubscribe() method I have examples of unsubscribing using a "known" myObject
* and one with a self reference to the myObject.
*
* The solution that uses IObserver and IObservable automates a way to store an explicit
* reference to the subscription holder.
*
*/
using System;
using System.Collections.Generic;
namespace ConsoleApplication1
{
/// <summary>
/// Sample objects that subscribe to MyObject's Updated event
/// </summary>
public class InnerObject
{
public MyObject Subscribed { get; set; }
public void Updated(object sender, MyObjectUpdateEventArgs e)
{
Console.WriteLine(e.Data);
}
}
/// <summary>
/// Object that publishes the Updated event
/// </summary>
public class MyObject
{
private string data_;
public event EventHandler<MyObjectUpdateEventArgs> Updated;
public string Data { get { return data_; } set { SetMyData(value); } }
/// <summary>
/// Outputs the Data string
/// </summary>
/// <returns></returns>
public override string ToString()
{
return Data;
}
/// <summary>
/// Event handler
/// </summary>
/// <param name="e"></param>
private void OnUpdate(MyObjectUpdateEventArgs e)
{
EventHandler<MyObjectUpdateEventArgs> handler = Updated;
if (Updated != null) {
handler(this, e);
}
}
private void SetMyData(string value)
{
if (Data != value) {
data_ = value;
OnUpdate(new MyObjectUpdateEventArgs(Data));
}
}
}
/// <summary>
/// EventArgs to provide updated MyData value;
/// </summary>
public class MyObjectUpdateEventArgs
{
public MyObjectUpdateEventArgs(string data)
{
Data = data;
}
public string Data { get; set; }
}
internal class Program
{
private static List<InnerObject> innerObjectsList = new List<InnerObject>();
private static void Main(string[] args)
{
MyObject myObject = new MyObject();
myObject.Data = "Hello World";
Console.WriteLine(myObject.ToString());
Console.ReadLine();
// Create the innerObjectts and subscribe
MakeNewInnerObjects(5, myObject);
// This will cause all of the inner objects to respond with myObjects Data string
Console.WriteLine("Assigning new data to myObject'n");
myObject.Data = "Hello InnerObjects";
// Shows they are responding, even though the list and the items are out of scope
Console.ReadLine();
// Uncomment to unsubscribe
// Unsubscribe(myObject);
innerObjectsList.Clear();
// Force garbage collection for our example
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("Assigning new data to myObject'n");
myObject.Data = "Are you still there InnerObjects?";
// Shows they still exist if we don't unsubscribe
Console.ReadLine();
}
/// <summary>
/// Create a count of InnerObjects, subscribe them to myObject and add to the list
/// </summary>
/// <param name="count"></param>
private static void MakeNewInnerObjects(int count, MyObject myObject)
{
// Uncomment if you want to have the list go out of scope as well to show
// That the reference keeps them alive
//List<InnerObject> innerObjectsList = new List<InnerObject>();
for (int i = 0; i < count; i++) {
InnerObject innerObject = new InnerObject();
innerObject.Subscribed = myObject;
myObject.Updated += innerObject.Updated;
innerObjectsList.Add(innerObject);
}
}
// Unsubscribe the list of innerObJects from MyObject
private static void Unsubscribe(MyObject myObject)
{
// Two ways to unsubscribe, the first depends on knowing what we subscribbed to
// the second uses a stored reference to the object
foreach (InnerObject innerObject in innerObjectsList) {
// myObject.Updated -= innerObject.Updated;
innerObject.Subscribed.Updated -= innerObject.Updated;
}
}
}
}
如果您为发布者实现IObservable
,那么Subscribe
调用将返回一个订阅对象,该对象将在您销毁它时取消订阅
public class Observable<T> : IObservable<T>
{
protected readonly List<IObserver<T>> _subscribers = new List<IObserver<T>>();
private class Subscription : IDisposable
{
List<IObserver<T>> _subscribers;
IObserver<T> _observer;
public Subscription(List<IObserver<T>> subscribers, IObserver<T> observer)
{
_subscribers = subscribers;
_observer = observer;
}
public void Dispose()
{
_subscribers.Remove(_observer);
}
}
public IDisposable Subscribe(IObserver<T> observer)
{
_subscribers.Add(observer);
return new Subscription(_subscribers, observer);
}
}
Subscription
类完成断开连接的所有工作。难题的另一面是订户:
public abstract class Observer<T> : IObserver<T>, IDisposable
{
private IDisposable _subscription = null;
public void Dispose()
{
Unsubscribe();
}
public void Unsubscribe()
{
if (_subscription != null)
{
_subscription.Dispose();
_subscription = null;
}
}
public void SubscribeTo(IObservable<T> publisher)
{
Unsubscribe();
_subscription = publisher.Subscribe(this);
}
public virtual void OnCompleted()
{ }
public abstract void OnError(Exception error)
{ }
public abstract void OnNext(T value);
}
即使您不使用IObservable<T>
和IObserver<T>
,基本原理也是相同的。您可以跟踪发布服务器/IObservable
上的订阅服务器,并分发可以从发布服务器列表中删除该订阅服务器的订阅服务器。然后,订阅者只需要跟踪他们的订阅。
更新:这里有一个以上类的用法示例。
首先,在Observable<T>
类中添加一个方便的函数:
public virtual void Publish(T value)
{
foreach (var sub in _subscribers.Distinct().ToArray())
{
try
{
sub.OnNext(value);
}
catch { }
}
}
这提供了一个简单的方法来通知所有订阅者发生了一些事情。现在,一个略显做作的例子:
public class KeyPublisher : Observable<ConsoleKeyInfo>
{
}
public class PrintKeys : Observer<ConsoleKeyInfo>
{
public override void OnNext(ConsoleKeyInfo next)
{
if (next.Modifiers != 0)
Console.Write("{0}-", next.Modifiers.ToString().Replace(", ", "-"));
Console.WriteLine(next.Key);
}
}
public class DetectEscape : Observer<ConsoleKeyInfo>
{
public bool FoundEscape { get; private set; }
public override void OnNext(ConsoleKeyInfo next)
{
if (next.Key == ConsoleKey.Escape)
FoundEscape = true;
}
}
class Program
{
static void Main(string[] args)
{
var pub = new KeyPublisher();
using (var sub1 = new PrintKeys())
using (var sub2 = new DetectEscape())
{
sub1.SubscribeTo(pub);
sub2.SubscribeTo(pub);
while (!sub2.FoundEscape)
{
pub.Publish(Console.ReadKey(true));
}
}
}
}
如果由于各种列表和订阅持有引用而导致对象寿命出现问题,解决方案可能是使用WeakReference
s。下面是使用弱引用的Subscription
类的一个版本:
private class Subscription : IDisposable
{
WeakReference<List<IObserver<T>>> _subscribers;
WeakReference<IObserver<T>> _observer;
public Subscription(List<IObserver<T>> subscribers, IObserver<T> observer)
{
_subscribers = new WeakReference<List<IObserver<T>>>(subscribers);
_observer = new WeakReference<IObserver<T>>(observer);
}
public void Dispose()
{
if (_subscribers != null && _observer != null)
{
List<IObserver<T>> subscribers;
IObserver<T> observer;
if (_subscribers.TryGetTarget(out subscribers) && _observer.TryGetTarget(out observer))
subscribers.Remove(observer);
_subscribers = null;
_observer = null;
}
}
}
您可以对_subscribers
列表执行同样的操作(使其保持WeakReference<IObserver<T>>
),以阻止其阻止订阅服务器进行垃圾收集。。。但我认为管理IDisposable
对象的生命周期是更好的做法。