观察程序设计模式
观察者设计模式使订阅者能够向提供程序注册并接收相关通知。 它适合所有需要推送通知的方案。 该模式定义一个提供程序(也称为主题或观察对象)以及零个、一个或多个观察者。 观察者向提供程序注册,并且当任何预定义的条件、事件或状态更改发生时,提供程序就会调用观察者中的一种方法,自动通知所有观察者。 在此方法调用中,提供程序还可以向观察者提供当前的状态信息。 在 .NET Framework 中,通过实现泛型 System.IObservable<T> 和 System.IObserver<T> 接口来应用观察者设计模式。 泛型类型参数表示提供通知信息的类型。
应用模式
观察者设计模式适用于分布式推送通知,因为它支持两种不同的组件或应用程序层之间的绝对分离,例如数据源(业务逻辑)层和用户界面(显示)层。 每当提供程序使用回调向其客户端提供当前信息时,即可实现该模式。
实现该模式需要您提供以下内容:
-
提供程序或主体,即将通知发送给观察者的对象。 提供程序是一个实现 IObservable<T> 接口的类或结构。 提供程序必须实现一个方法 (IObservable<T>.Subscribe),希望接收提供程序通知的观察者会调用该方法。
-
观察者,即接收提供程序通知的对象。 观察者是一个实现 IObserver<T> 接口的类或结构。 观察者必须实现三个方法,提供程序将调用所有这些方法:
-
IObserver<T>.OnNext ,向观察者提供新信息或当前信息。
-
IObserver<T>.OnError ,通知观察者发生错误。
-
IObserver<T>.OnCompleted ,指示提供程序已完成通知发送。
-
-
允许提供程序跟踪观察者的机制。 通常情况下,提供程序使用容器对象(例如 System.Collections.Generic.List<T> 对象)存放对已订阅通知的 IObserver<T> 实现的引用。 因此目的而使用存储容器时,提供程序能够处理零到无限数量的观察者。 不定义观察者接收通知的顺序;提供程序可以自由选择任何方法来确定该顺序。
-
允许提供程序在完成通知时移除观察者的 IDisposable 实现。 观察者从 Subscribe 方法接收对 IDisposable 实现的引用,所以它们还可以调用 IDisposable.Dispose 方法以在提供程序完成通知发送之前取消订阅。
-
包含提供程序向其观察者发送的数据的对象。 该对象的类型对应于 IObservable<T> 和 IObserver<T> 接口的泛型类型参数。 尽管该对象可以与 IObservable<T> 实现相同,但在通常情况下,该对象为不同的类型。
注意 |
---|
除实现观察者设计模式外,您可能会对探索使用 IObservable<T> 和 IObserver<T> 接口构建的库感兴趣。 例如,Reactive Extensions for .NET (Rx) 包含一组支持异步编程的扩展方法和 LINQ 标准序列运算符。 |
实现模式
下面的示例使用观察者设计模式实现机场行李提取信息系统。 BaggageInfo 类提供有关到达航班和提取各次航班行李所用转盘的信息。 如以下示例中所示。
using System; using System.Collections.Generic; public class BaggageInfo { private int flightNo; private string origin; private int location; internal BaggageInfo(int flight, string from, int carousel) { this.flightNo = flight; this.origin = from; this.location = carousel; } public int FlightNumber { get { return this.flightNo; } } public string From { get { return this.origin; } } public int Carousel { get { return this.location; } } }
BaggageHandler 类负责接收有关到达航班和行李提取转盘的信息。 在内部,它包含两个集合:
-
observers - 接收更新信息的客户端集合。
-
flights - 航班及其指派的转盘的集合。
两个集合都由 BaggageHandler 类构造函数中实例化的泛型 List<T> 对象表示。 下面的示例中显示 BaggageHandler 类的源代码。
public class BaggageHandler : IObservable<BaggageInfo> { private List<IObserver<BaggageInfo>> observers; private List<BaggageInfo> flights; public BaggageHandler() { observers = new List<IObserver<BaggageInfo>>(); flights = new List<BaggageInfo>(); } public IDisposable Subscribe(IObserver<BaggageInfo> observer) { // Check whether observer is already registered. If not, add it if (! observers.Contains(observer)) { observers.Add(observer); // Provide observer with existing data. foreach (var item in flights) observer.OnNext(item); } return new Unsubscriber<BaggageInfo>(observers, observer); } // Called to indicate all baggage is now unloaded. public void BaggageStatus(int flightNo) { BaggageStatus(flightNo, String.Empty, 0); } public void BaggageStatus(int flightNo, string from, int carousel) { var info = new BaggageInfo(flightNo, from, carousel); // Carousel is assigned, so add new info object to list. if (carousel > 0 && ! flights.Contains(info)) { flights.Add(info); foreach (var observer in observers) observer.OnNext(info); } else if (carousel == 0) { // Baggage claim for flight is done var flightsToRemove = new List<BaggageInfo>(); foreach (var flight in flights) { if (info.FlightNumber == flight.FlightNumber) { flightsToRemove.Add(flight); foreach (var observer in observers) observer.OnNext(info); } } foreach (var flightToRemove in flightsToRemove) flights.Remove(flightToRemove); flightsToRemove.Clear(); } } public void LastBaggageClaimed() { foreach (var observer in observers) observer.OnCompleted(); observers.Clear(); } }
希望接收更新信息的客户端调用 BaggageInfo.Subscribe 方法。 如果客户端之前没有订阅过通知,则会将对客户端 IObserver<T> 实现的引用添加到 observers 集合中。
可以调用重载的 BaggageHandler.BaggageStatus 方法以指示是正在卸载还是已卸载航班行李。 在第一种情况中,向该方法传递航班号、航班起飞的机场,以及卸载行李的转盘。 在第二种情况中,仅向该方法传递航班号。 对于正在卸载的行李,该方法检查传递给方法的 BaggageInfo 信息是否位于 flights 集合中。 如果没有,该方法将添加相应信息并调用每个观察者的 OnNext 方法。 对于完成行李卸载的航班,该方法会检查有关此航班的信息是否位于 flights 集合中。 如果在该集合中,该方法会调用每个观察者的 OnNext 方法并从 flights 集合中移除 BaggageInfo 对象。
当天最后一趟航班着陆并处理完其行李后,将调用 BaggageHandler.LastBaggageClaimed 方法。 该方法调用每个观察者的 OnCompleted 方法以指示已完成所有通知,然后清除 observers 集合。
提供程序的 Subscribe 方法返回使观察者可以在调用 OnCompleted 方法之前停止接收通知的 IDisposable 实现。 下面的示例中显示该 Unsubscriber(Of BaggageInfo) 类的源代码。 当类在 BaggageHandler.Subscribe 方法中实例化时,将向其传递对 observers 集合的引用以及对添加至该集合的观察者的引用。 这些引用将指派给局部变量。 当调用对象的 Dispose 方法时,它会检查观察者是否仍在 observers 集合中,如果在,则移除观察者。
internal class Unsubscriber<BaggageInfo> : IDisposable { private List<IObserver<BaggageInfo>> _observers; private IObserver<BaggageInfo> _observer; internal Unsubscriber(List<IObserver<BaggageInfo>> observers, IObserver<BaggageInfo> observer) { this._observers = observers; this._observer = observer; } public void Dispose() { if (_observers.Contains(_observer)) _observers.Remove(_observer); } }
下面的示例提供名为 ArrivalsMonitor 的 IObserver<T> 实现,它是显示行李提取信息的基类。 该信息按起飞城市名称的字母顺序显示。 ArrivalsMonitor 的方法标记为 overridable (Visual Basic) 或 virtual (C#),所以它们可全部由派生类重写。
using System; using System.Collections.Generic; public class ArrivalsMonitor : IObserver<BaggageInfo> { private string name; private List<string> flightInfos = new List<string>(); private IDisposable cancellation; private string fmt = "{0,-20} {1,5} {2, 3}"; public ArrivalsMonitor(string name) { if (String.IsNullOrEmpty(name)) throw new ArgumentNullException("The observer must be assigned a name."); this.name = name; } public virtual void Subscribe(BaggageHandler provider) { cancellation = provider.Subscribe(this); } public virtual void Unsubscribe() { cancellation.Dispose(); flightInfos.Clear(); } public virtual void OnCompleted() { flightInfos.Clear(); } // No implementation needed: Method is not called by the BaggageHandler class. public virtual void OnError(Exception e) { // No implementation. } // Update information. public virtual void OnNext(BaggageInfo info) { bool updated = false; // Flight has unloaded its baggage; remove from the monitor. if (info.Carousel == 0) { var flightsToRemove = new List<string>(); string flightNo = String.Format("{0,5}", info.FlightNumber); foreach (var flightInfo in flightInfos) { if (flightInfo.Substring(21, 5).Equals(flightNo)) { flightsToRemove.Add(flightInfo); updated = true; } } foreach (var flightToRemove in flightsToRemove) flightInfos.Remove(flightToRemove); flightsToRemove.Clear(); } else { // Add flight if it does not exist in the collection. string flightInfo = String.Format(fmt, info.From, info.FlightNumber, info.Carousel); if (! flightInfos.Contains(flightInfo)) { flightInfos.Add(flightInfo); updated = true; } } if (updated) { flightInfos.Sort(); Console.WriteLine("Arrivals information from {0}", this.name); foreach (var flightInfo in flightInfos) Console.WriteLine(flightInfo); Console.WriteLine(); } } }
ArrivalsMonitor 类包括 Subscribe 和 Unsubscribe 方法。 Subscribe 方法允许类将由对 Subscribe 的调用返回的 IDisposable 实现保存至私有变量中。 Unsubscribe 方法允许类通过调用提供程序的 Dispose 实现来取消订阅通知。 ArrivalsMonitor 还提供 OnNext、OnError 和 OnCompleted 方法的实现。 仅 OnNext 实现包含大量代码。 该方法使用已排序的私有泛型 List<T> 对象,此对象维护到达航班的起飞机场及其行李使用的转盘的信息。 如果 BaggageHandler 类报告有新航班到达,则 OnNext 方法实现会将有关该航班的信息添加到列表中。 如果 BaggageHandler 类报告已卸载航班行李,则 OnNext 方法会从列表中移除该航班。 无论何时进行更改,都会对列表进行排序,并将其显示在控制台上。
下面的示例包含实例化 BaggageHandler 类的应用程序入口点,以及两个 ArrivalsMonitor 类的实例,并使用 BaggageHandler.BaggageStatus 方法添加和移除有关到达航班的信息。 在每种情况下,观察者都接收更新并正确显示行李提取信息。
using System; using System.Collections.Generic; public class Example { public static void Main() { BaggageHandler provider = new BaggageHandler(); ArrivalsMonitor observer1 = new ArrivalsMonitor("BaggageClaimMonitor1"); ArrivalsMonitor observer2 = new ArrivalsMonitor("SecurityExit"); provider.BaggageStatus(712, "Detroit", 3); observer1.Subscribe(provider); provider.BaggageStatus(712, "Kalamazoo", 3); provider.BaggageStatus(400, "New York-Kennedy", 1); provider.BaggageStatus(712, "Detroit", 3); observer2.Subscribe(provider); provider.BaggageStatus(511, "San Francisco", 2); provider.BaggageStatus(712); observer2.Unsubscribe(); provider.BaggageStatus(400); provider.LastBaggageClaimed(); } } // The example displays the following output: // Arrivals information from BaggageClaimMonitor1 // Detroit 712 3 // // Arrivals information from BaggageClaimMonitor1 // Detroit 712 3 // Kalamazoo 712 3 // // Arrivals information from BaggageClaimMonitor1 // Detroit 712 3 // Kalamazoo 712 3 // New York-Kennedy 400 1 // // Arrivals information from SecurityExit // Detroit 712 3 // // Arrivals information from SecurityExit // Detroit 712 3 // Kalamazoo 712 3 // // Arrivals information from SecurityExit // Detroit 712 3 // Kalamazoo 712 3 // New York-Kennedy 400 1 // // Arrivals information from BaggageClaimMonitor1 // Detroit 712 3 // Kalamazoo 712 3 // New York-Kennedy 400 1 // San Francisco 511 2 // // Arrivals information from SecurityExit // Detroit 712 3 // Kalamazoo 712 3 // New York-Kennedy 400 1 // San Francisco 511 2 // // Arrivals information from BaggageClaimMonitor1 // New York-Kennedy 400 1 // San Francisco 511 2 // // Arrivals information from SecurityExit // New York-Kennedy 400 1 // San Francisco 511 2 // // Arrivals information from BaggageClaimMonitor1 // San Francisco 511 2
观察程序设计模式最佳做法
在 .NET Framework 中,以一组接口的形式实现观察者设计模式。 System.IObserver<T> 接口表示数据提供程序,还负责提供允许观察者取消订阅通知的 IDisposable 实现。 System.IObserver<T> 接口表示观察者。 本主题介绍开发人员使用这些接口实现观察者设计模式时应遵循的最佳做法。
线程处理
通常,提供程序通过将特定的观察者添加到由某些集合对象表示的订阅者列表中来实现 IObservable<T>.Subscribe 方法,并通过从订阅者列表中移除特定的观察者来实现 IDisposable.Dispose 方法。 观察者可以随时调用这些方法。 另外,由于提供程序/观察者协定并未指定谁负责在 IObserver<T>.OnCompleted 回调方法之后取消订阅,因此提供程序和观察者可能都会尝试从列表中移除相同成员。 因为有可能出现这种情况,所以 Subscribe 和 Dispose 方法都应是线程安全的。 通常,这会涉及使用并发集合或锁。 非线程安全的实现应显式注明它们不是线程安全的。
除提供程序/观察者协定外,还必须在层中指定任何其他保证。 当实施者强加其他要求时应进行明确说明,以避免用户对观察者协定产生混淆。
处理异常
由于数据提供程序和观察者之间存在松耦合,因此观察者设计模式中的异常主要用于提供信息。 这会影响提供程序和观察者在观察者设计模式中处理异常的方式。
提供程序 -- 调用 OnError 方法
OnError 方法旨在为观察者提供信息性消息,它与 IObserver<T>.OnNext 方法相似。 但是,OnNext 方法旨在向观察者提供当前数据或更新的数据,而 OnError 方法旨在指示提供程序无法提供有效数据。
提供程序在处理异常和调用 OnError 方法时应遵循以下最佳做法:
-
如果提供程序具有任何特定的要求,则必须处理自己的异常。
-
提供程序不应期待或要求观察者以任何特殊的方式处理异常。
-
当提供程序处理有损其更新提供能力的异常时,应调用 OnError 方法。 可将此类异常的信息传递给观察者。 在其他情况下,则无需向观察者通知异常。
提供程序调用 OnError 或 IObserver<T>.OnCompleted 方法后,不应有进一步的通知,并且提供程序可以取消订阅其观察者。 但是,观察者也可以随时取消订阅自己,包括在接收 OnError 或 IObserver<T>.OnCompleted 通知的之前和之后。 由于观察者设计模式不指明是提供程序还是观察者负责取消订阅,因此有可能两者都尝试执行取消订阅操作。 通常,当观察者取消订阅时,会从订阅者集合中移除它们。 在单一线程的应用程序中,IDisposable.Dispose 实现在尝试移除对象之前应先确保对象引用有效,并且对象是订阅者集合中的成员。 对于多线程应用程序,应使用线程安全的集合对象,如 System.Collections.Concurrent.BlockingCollection<T> 对象。
观察者 -- 实现 OnError 方法
其他最佳做法
在 IObservable<T>.Subscribe 方法中尝试取消注册可能导致 null 引用。 因此,建议您避免使用这种做法。
尽管可以将一个观察者附加到多个提供程序,但建议的模式是将一个 IObserver<T> 实例仅附加到一个 IObservable<T> 实例。
如何:实现提供程序
观察者设计模式要求在提供程序(监视数据和发送通知)和一个或多个观察者(接收提供程序的通知,即回调)之间进行区分。 本主题讨论如何创建提供程序。 相关主题如何:实现观察程序讨论如何创建观察者。
创建提供程序
-
定义提供程序负责发送给观察者的数据。 尽管提供程序和它发送给观察者的数据可以为一个类型,但是通常它们都采用不同的类型表示。 例如,在温度监控应用程序中,Temperature 结构定义提供程序(由下一步中定义的 TemperatureMonitor 类表示)监控的数据和观察者订阅的数据。
using System; public struct Temperature { private decimal temp; private DateTime tempDate; public Temperature(decimal temperature, DateTime dateAndTime) { this.temp = temperature; this.tempDate = dateAndTime; } public decimal Degrees { get { return this.temp; } } public DateTime Date { get { return this.tempDate; } } }
-
定义数据提供程序,这是实现 System.IObservable<T> 接口的类型。 提供程序的泛型类型参数是提供程序发送给观察者的类型。 下面的示例定义了一个 TemperatureMonitor 类,它是具有 Temperature 泛型类型参数的构造 System.IObservable<T> 实现。
using System; using System.Collections.Generic; public class TemperatureMonitor : IObservable<Temperature> { ... }
-
确定提供程序存储观察者引用的方式,以便每个观察者都能在适当的时候得到通知。 最常用于此目的是集合对象,如泛型 List<T> 对象。 下面的示例定义了一个专用的 List<T> 对象,它在 TemperatureMonitor 类构造函数中实例化。
using System; using System.Collections.Generic; public class TemperatureMonitor : IObservable<Temperature> { List<IObserver<Temperature>> observers; public TemperatureMonitor() { observers = new List<IObserver<Temperature>>(); } ... }
-
定义提供程序可以返回至订阅者的 IDisposable 实现,以便它们能够随时停止接收通知。 下面的示例定义了一个嵌套的 Unsubscriber 类,在实例化类时,会向该类传递对订阅者集合和订阅者的引用。 此代码允许订阅者调用对象的 IDisposable.Dispose 实现,以将其自身从订阅者集合中移除。
private class Unsubscriber : IDisposable { private List<IObserver<Temperature>> _observers; private IObserver<Temperature> _observer; public Unsubscriber(List<IObserver<Temperature>> observers, IObserver<Temperature> observer) { this._observers = observers; this._observer = observer; } public void Dispose() { if (! (_observer == null)) _observers.Remove(_observer); } }
-
实现 IObservable<T>.Subscribe 方法。 将向该方法传递对 System.IObserver<T> 接口的引用,并且该方法应存储在步骤 3 中为该目的而设计的对象中。 然后,该方法应该返回步骤 4 中开发的 IDisposable 实现。 下面的示例演示 TemperatureMonitor 类中 Subscribe 方法的实现。
public IDisposable Subscribe(IObserver<Temperature> observer) { if (! observers.Contains(observer)) observers.Add(observer); return new Unsubscriber(observers, observer); }
-
通过调用观察者的 IObserver<T>.OnNext、IObserver<T>.OnError 和 IObserver<T>.OnCompleted 实现,根据需要通知观察者。 在某些情况下,提供程序在发生错误时可能不会调用 OnError 方法。 例如,下面的 GetTemperature 方法模拟一个监视器,它每五秒读取一次温度数据,并对照之前的读数,如果温度的变化大于或等于 0.1 度则通知观察者。 如果设备不报告温度(即其值为 null),则提供程序会通知观察者传输已完成。 请注意,除调用每个观察者的 OnCompleted 方法外,GetTemperature 方法还会清除 List<T> 集合。 在这种情况下,提供程序不调用其观察者的 OnError 方法。
public void GetTemperature() { // Create an array of sample data to mimic a temperature device. Nullable<Decimal>[] temps = {14.6m, 14.65m, 14.7m, 14.9m, 14.9m, 15.2m, 15.25m, 15.2m, 15.4m, 15.45m, null }; // Store the previous temperature, so notification is only sent after at least .1 change. Nullable<Decimal> previous = null; bool start = true; foreach (var temp in temps) { System.Threading.Thread.Sleep(2500); if (temp.HasValue) { if (start || (Math.Abs(temp.Value - previous.Value) >= 0.1m )) { Temperature tempData = new Temperature(temp.Value, DateTime.Now); foreach (var observer in observers) observer.OnNext(tempData); previous = temp; if (start) start = false; } } else { foreach (var observer in observers.ToArray()) if (observer != null) observer.OnCompleted(); observers.Clear(); break; } } }
示例
下面的示例包含定义温度监控应用程序的 IObservable<T> 实现的完整源代码。 它包括 Temperature 结构(发送给观察者的数据)和 TemperatureMonitor 类(即 IObservable<T> 实现)。
using System.Threading; using System; using System.Collections.Generic; public class TemperatureMonitor : IObservable<Temperature> { List<IObserver<Temperature>> observers; public TemperatureMonitor() { observers = new List<IObserver<Temperature>>(); } private class Unsubscriber : IDisposable { private List<IObserver<Temperature>> _observers; private IObserver<Temperature> _observer; public Unsubscriber(List<IObserver<Temperature>> observers, IObserver<Temperature> observer) { this._observers = observers; this._observer = observer; } public void Dispose() { if (! (_observer == null)) _observers.Remove(_observer); } } public IDisposable Subscribe(IObserver<Temperature> observer) { if (! observers.Contains(observer)) observers.Add(observer); return new Unsubscriber(observers, observer); } public void GetTemperature() { // Create an array of sample data to mimic a temperature device. Nullable<Decimal>[] temps = {14.6m, 14.65m, 14.7m, 14.9m, 14.9m, 15.2m, 15.25m, 15.2m, 15.4m, 15.45m, null }; // Store the previous temperature, so notification is only sent after at least .1 change. Nullable<Decimal> previous = null; bool start = true; foreach (var temp in temps) { System.Threading.Thread.Sleep(2500); if (temp.HasValue) { if (start || (Math.Abs(temp.Value - previous.Value) >= 0.1m )) { Temperature tempData = new Temperature(temp.Value, DateTime.Now); foreach (var observer in observers) observer.OnNext(tempData); previous = temp; if (start) start = false; } } else { foreach (var observer in observers.ToArray()) if (observer != null) observer.OnCompleted(); observers.Clear(); break; } } } }
如何:实现观察程序
观察者设计模式要求在观察者(注册通知)和提供程序(监视数据并将通知发送至一个或多个观察者)之间插入分隔符。 本主题讨论如何创建观察者。 相关主题如何:实现提供程序讨论如何创建提供程序。
创建观察者
-
定义作为实现 System.IObserver<T> 接口的类型的观察者。 例如,下面的代码定义一个名为 TemperatureReporter 的类型,它是使用 Temperature 泛型类型参数构造的 System.IObserver<T> 实现。
public class TemperatureReporter : IObserver<Temperature>
-
如果观察者可以在提供程序调用其 IObserver<T>.OnCompleted 实现之前停止接收通知,请定义保留提供程序的 IObservable<T>.Subscribe 方法返回的 IDisposable 实现的私有变量。 还应定义调用提供程序的 Subscribe 方法并存储返回的 IDisposable 对象的订阅方法。 例如,下面的代码定义一个名为 unsubscriber 的私有变量,并定义一个调用提供程序的 Subscribe 方法并将返回的对象指派给 unsubscriber 变量的 Subscribe 方法。
public class TemperatureReporter : IObserver<Temperature> { private IDisposable unsubscriber; private bool first = true; private Temperature last; public virtual void Subscribe(IObservable<Temperature> provider) { unsubscriber = provider.Subscribe(this); } ... }
-
定义使观察者可以在提供程序调用其 IObserver<T>.OnCompleted 实现之前停止接收通知的方法(如果需要此功能)。 下面的示例定义 Unsubscribe 方法。
public virtual void Unsubscribe() { unsubscriber.Dispose(); }
-
提供由 IObserver<T> 接口定义的三种方法的实现:IObserver<T>.OnNext、IObserver<T>.OnError 和 IObserver<T>.OnCompleted。 根据提供程序和应用程序的需求,OnError 和 OnCompleted 方法可以为存根实现。 注意,OnError 方法不应将传递的 Exception 对象作为异常处理,而 OnCompleted 方法可以自由调用提供程序的 IDisposable.Dispose 实现。 下面的示例演示 TemperatureReporter 类的 IObserver<T> 实现。
public virtual void OnCompleted() { Console.WriteLine("Additional temperature data will not be transmitted."); } public virtual void OnError(Exception error) { // Do nothing. } public virtual void OnNext(Temperature value) { Console.WriteLine("The temperature is {0}°C at {1:g}", value.Degrees, value.Date); if (first) { last = value; first = false; } else { Console.WriteLine(" Change: {0}° in {1:g}", value.Degrees - last.Degrees, value.Date.ToUniversalTime() - last.Date.ToUniversalTime()); } }
示例
下面的示例包含 TemperatureReporter 类(提供温度监控应用程序的 IObserver<T> 实现)的完整源代码。
public class TemperatureReporter : IObserver<Temperature> { private IDisposable unsubscriber; private bool first = true; private Temperature last; public virtual void Subscribe(IObservable<Temperature> provider) { unsubscriber = provider.Subscribe(this); } public virtual void Unsubscribe() { unsubscriber.Dispose(); } public virtual void OnCompleted() { Console.WriteLine("Additional temperature data will not be transmitted."); } public virtual void OnError(Exception error) { // Do nothing. } public virtual void OnNext(Temperature value) { Console.WriteLine("The temperature is {0}°C at {1:g}", value.Degrees, value.Date); if (first) { last = value; first = false; } else { Console.WriteLine(" Change: {0}° in {1:g}", value.Degrees - last.Degrees, value.Date.ToUniversalTime() - last.Date.ToUniversalTime()); } } }