C# 中的观察者模式
C# 观察者模式教程文章
引言
这是一篇关于 C# 中观察者模式的教程文章。目标读者是中级及以上 C# 程序员。我们首先介绍经典的观察者模式,它是 GoF 模式之一,并在文献中经常被提及。
像 C# 这样的现代语言已经集成了事件机制,通过该机制,观察者模式实际上已集成到语言机制中。在 C# 中,观察者模式的现代用法实际上与事件机制的使用有关,而经典的观察者模式在 C# 中已变得过时。
事件机制实际上提供同步调用的事实经常被忽视,并且没有得到足够的强调。程序员通常会产生并行的错觉,但这不是现实,并且在当今的多核处理器世界中是一个重要的问题。
所提供的代码是教程,概念演示级别,为了简洁起见,不处理或显示所有变体/问题。
经典观察者模式
观察者模式,有时也称为主题-观察者模式,定义了对象之间的一对多依赖关系,以便当一个对象(Subject
,主题)改变其状态时,所有依赖它的对象(Observers
,观察者)都会得到通知并更新。 我们说 Observer
向 Subject
“订阅”以获取 Subject
的状态更改通知。 简单来说,一个组件 (Subject
) 通知其他组件 (Observers
) 状态已更改。
虽然在 C# 中,Event
关键字已经集成,这是观察者模式的实现,但仍然可以决定推出自己的版本/实现。 以下是 GoF 文献中描述的经典观察者的外观。
类图
实现代码示例
public class Args
{
}
public abstract class ASubject
{
protected List<IObserver> observersList = new List<IObserver>();
public void AddObserver(IObserver observer)
{
observersList.Add(observer);
}
public void RemoveObserver(IObserver observer)
{
observersList.Remove(observer);
}
}
public interface IObserver
{
void Update(object subject, Args args);
}
public class Subject : ASubject
{
public string SubjectState = null;
public void NotifyObservers()
{
ArgsSubject args = new ArgsSubject();
args.SubjectState = this.SubjectState;
foreach (IObserver o in observersList)
{
o.Update(this, args);
}
}
}
public class Observer : IObserver
{
private string name;
private string observerState;
public Observer(string name)
{
this.name = name;
}
public void Update(object s, Args args)
{
observerState = ((ArgsSubject)args).SubjectState;
Console.WriteLine("Observer {0}'s new state is {1}",
name, observerState);
}
}
public class ArgsSubject : Args
{
public string SubjectState = null;
}
class Client
{
public static void Main(string[] args)
{
Subject s = new Subject();
s.AddObserver(new Observer("1"));
s.AddObserver(new Observer("2"));
s.AddObserver(new Observer("3"));
// Change subject state and notify observers
s.SubjectState = "ABC123";
s.NotifyObservers();
Console.ReadLine();
}
}
示例执行
C# 中的现代观察者模式 - 事件
C# 已经集成了 Event
关键字,它是观察者模式的实现。 C# 带来了自己的术语
Subject
->Event
Observer
->EventHandler
(更准确地说,Observer.Notify()
方法映射到EventHandler
方法,Observer
对象本身被认为是过时的)SubjectStateInfo
->EventArgs
(更准确地说,SubjectStateInfo
映射到从EventArgs
继承的类的属性)AddObserver()
方法 ->operator +=
RemoveObserver()
方法 ->operator -=
C# 文献建议使用具有特定签名的 EventHandler
委托,带有参数 (this
, EventArgs
),但 C# 语言中没有任何内容阻止您使用您喜欢的签名。
这是新解决方案的类图
这是代码
public class Subject
{
public event EventHandler<EventArgsSubject> SubjectEvent;
public string SubjectState;
public void NotifyObservers()
{
EventArgsSubject args = new EventArgsSubject();
args.SubjectState = this.SubjectState;
// (1)
if (SubjectEvent != null)
{
SubjectEvent(this, args);
}
}
}
public class Observer
{
private string name;
private string observerState;
public Observer(string name)
{
this.name = name;
}
public void Update(object subject, EventArgsSubject args)
{
observerState = args.SubjectState;
Console.WriteLine("Observer {0}'s new state is {1}",
name, observerState);
}
}
public class EventArgsSubject : EventArgs
{
public string SubjectState = null;
}
class Client
{
public static void Main(string[] args)
{
Subject s = new Subject();
s.SubjectEvent += (new Observer("1")).Update;
s.SubjectEvent += (new Observer("2")).Update;
s.SubjectEvent += (new Observer("3")).Update;
// Change subject state and notify observers
s.SubjectState = "ABC123";
s.NotifyObservers();
Console.ReadLine();
}
}
这是示例执行
让我们提到,在 (1) 中,我们使用了一种在文献中很流行的方法。但是,从理论上讲,在检查 SubjectEvent
是否为 null
和调用事件之间,可能会出现“线程竞争”情况,某些其他线程可以从 SubjectEvent
中删除处理程序,这会导致 null
引用异常。 今天更流行的编写处理程序调用的方法是
SubjectEvent?.Invoke(this, args);
事件机制在单个线程上提供同步调用
需要强调的是,在调用中
if (SubjectEvent != null)
{
SubjectEvent(this, args);
}
//or
SubjectEvent?.Invoke(this, args);
订阅的 EventHandler
是在单个线程上同步调用的。 这有一些不太明显的后果
EventHandler
按照它们订阅事件的顺序依次执行。- 这意味着在较早订阅的
EventHandler
中的对象/值比在其他EventHandler
中的对象/值更早更新,这可能会对程序逻辑产生影响。 - 对特定
EventHandler
的调用会阻塞线程,直到该EventHandler
中的所有工作完成为止。 - 如果在某个
EventHandler
中抛出异常,则在该EventHandler
之后订阅的所有EventHandler
都不会被执行。
结论
观察者模式是一种非常重要的模式,并且 C# 语言通过使用事件直接支持它。 尽管线程问题可能很棘手,但程序员必须对从线程角度来看通知机制的工作方式有很好的理解。
历史
- 2022年3月7日:初始版本
- 2022年9月11日:决定将文章分成两部分