烂翻译系列之Rx.NET介绍第二版——关键类型
Rx is a powerful framework that can greatly simplify code that responds to events. But to write good Reactive code you have to understand the basic concepts. The fundamental building block of Rx is an interface called IObservable<T>
. Understanding this, and its counterpart IObserver<T>
, is the key to success with Rx.
Rx 是一个强大的框架,能够极大地简化响应事件的代码。但要编写高质量的响应式代码,你必须理解基本概念。Rx 的基本构建块是一个名为 IObservable<T>
的接口。理解这个接口及其对应的 IObserver<T>
接口,是成功使用 Rx 的关键。
The preceding chapter showed this LINQ query expression as the first example:
前一章将这个 LINQ 查询表达式作为第一个示例进行了展示:
var bigTrades = from trade in trades where trade.Volume > 1_000_000;
Most .NET developers will be familiar with LINQ in at least one of its many popular forms such as LINQ to Objects, or Entity Framework Core queries. Most LINQ implementations allow you to query data at rest. LINQ to Objects works on arrays or other collections, and LINQ queries in Entity Framework Core run against data in a database, but Rx is different: it offers the ability to define queries over live event streams—what you might call data in motion.
大部分.NET 开发人员至少会熟悉 LINQ 的一种流行形式,比如 LINQ to Objects 或 Entity Framework Core queries。大多数 LINQ 实现允许您查询静态数据。LINQ to Objects 对数组或其他集合进行操作,而 Entity Framework Core 中的 LINQ 查询则针对数据库中的数据运行,但 Rx 有所不同:它提供了在实时事件流上定义查询的能力——您可能会将其称为动态数据。
If you don't like the query expression syntax, you can write exactly equivalent code by invoking LINQ operators directly:
如果您不喜欢查询表达式语法,可以直接调用 LINQ 运算符来编写完全相同的代码:
var bigTrades = trades.Where(trade => trade.Volume > 1_000_000);
Whichever style we use, this is the LINQ way of saying that we want bigTrades
to have just those items in trades
where the Volume
property is greater than one million.
无论我们使用哪种风格,这都是 LINQ 的方式,即我们希望 bigTrades
只包含 trades
中那些 Volume
属性大于一百万的项目。
We can't tell exactly what these examples do because we can't see the type of the trades
or bigTrades
variables. The meaning of this code is going to vary greatly depending on these types. If we were using LINQ to objects, these would both likely be IEnumerable<Trade>
. That would mean that these variables both referred to objects representing collections whose contents we could enumerate with a foreach
loop. This would represent data at rest, data that our code could inspect directly.
我们无法确切知道这些示例的具体作用,因为我们无法看到 trades
或 bigTrades
变量的类型。这段代码的含义会根据这些类型的不同而有很大差异。如果我们使用的是 LINQ to Objects,那么这两个变量很可能都是 IEnumerable<Trade>
类型。这意味着这两个变量都引用了表示集合的对象,我们可以使用 foreach
循环来枚举这些集合的内容。这将表示静态数据,即我们的代码可以直接检查的数据。
But let's make it clear what the code means by being explicit about the type:
但是,让我们通过明确说明类型来弄清楚代码的含义:
IObservable<Trade> bigTrades = trades.Where(trade => trade.Volume > 1_000_000);
This removes the ambiguity. It is now clear that we're not dealing with data at rest. We're working with an IObservable<Trade>
. But what exactly is that?
这消除了歧义。现在很清楚,我们处理的不是静态的数据。我们正在处理一个 IObservable<Trade> 对象
。但 IObservable<Trade>
到底是什么?
IObservable<T> 可观察对象
The IObservable<T>
interface represents Rx's fundamental abstraction: a sequence of values of some type T
. In a very abstract sense, this means it represents the same thing as IEnumerable<T>
.
IObservable<T>
接口代表了 Rx 的基本抽象:某种类型 T 的一系列值。从非常抽象的意义上讲,这意味着它与 IEnumerable<T>
表示的是相同的东西。
The difference is in how code consumes those values. Whereas IEnumerable<T>
enables code to retrieve values (typically with a foreach
loop), an IObservable<T>
provides values when they become available. This distinction is sometimes characterised as push vs pull. We can pull values out of an IEnumerable<T>
by executing a foreach
loop, but an IObservable<T>
will push values into our code.
区别在于代码如何消费这些值。IEnumerable<T>
允许代码检索值(通常使用 foreach
循环),而 IObservable<T>
则在值可用时提供这些值。这种区别有时被描述为“推送”与“拉取”。我们可以通过执行 foreach
循环从 IEnumerable<T>
中拉取值,但 IObservable<T>
会将值推送到我们的代码中。
How can an IObservable<T>
push its values into our code? If we want these values, our code must subscribe to the IObservable<T>
, which means providing it with some methods it can invoke. In fact, subscription is the only operation an IObservable<T>
directly supports. Here's the entire definition of the interface:
IObservable<T>
如何将其值推送到我们的代码中?如果我们需要这些值,我们的代码必须订阅 IObservable<T>
,这意味着为它提供它可以调用的一些方法。事实上, Subscribe
(订阅)是 IObservable<T>
直接支持的唯一操作。下面是整个 IObservable<T>
接口的定义:
public interface IObservable<out T> { IDisposable Subscribe(IObserver<T> observer); }
You can see the source for IObservable<T>
on GitHub. Notice that it is part of the .NET runtime libraries, and not the System.Reactive
NuGet package. IObservable<T>
represents such a fundamentally important abstraction that it is baked into .NET. (So you might be wondering what the System.Reactive
NuGet package is for. The .NET runtime libraries define only the IObservable<T>
and IObserver<T>
interfaces, and not the LINQ implementation. The System.Reactive
NuGet package gives us LINQ support, and also deals with threading.)
你可以查看GitHub上的 IObservable<T>
源码。注意,它是.NET 运行时库的一部分,而不是 System.Reactive
NuGet 包的一部分。 IObservable<T>
表示一个非常重要的抽象,以至于它被嵌入到.NET 中。(因此,您可能想知道 System.Reactive
NuGet 包的作用是什么。.NET 运行时库仅定义了 IObservable<T>
和 IObserver<T>
接口,而没有定义 LINQ 实现。System.Reactive
NuGet 包为我们提供了 LINQ 支持,并且还处理了线程问题。)
This interface's only method makes it clear what we can do with an IObservable<T>
: if we want to receive the events it has to offer, we can subscribe to it. (We can also unsubscribe: the Subscribe
method returns an IDisposable
, and if we call Dispose
on that it cancels our subscription.) The Subscribe
method requires us to pass in an implementation of IObserver<T>
, which we will get to shortly.
该接口的唯一方法清楚地说明了我们可以对 IObservable<T>
执行什么操作:如果我们想接收它提供的事件,我们可以订阅它。(我们还可以取消订阅:Subscribe
方法返回一个 IDisposable
,如果我们对其调用 Dispose
,则会取消我们的订阅。)Subscribe
方法要求我们传入一个 IObserver<T>
的实现,我们很快就会谈到这一点。
Observant readers will have noticed that an example in the preceding chapter looks like it shouldn't work. That code created an IObservable<long>
that produced events once per second, and then it subscribed to it with this code:
细心的读者可能已经注意到,前一章中的一个示例看起来似乎不应该工作。那段代码创建了一个 IObservable<long>
,它每秒产生一次事件,然后使用以下代码订阅了它:
ticks.Subscribe( tick => Console.WriteLine($"Tick {tick}"));
That's passing a delegate, and not the IObserver<T>
that IObservable<T>.Subscribe
requires. We'll get to IObserver<T>
shortly, but all that's happening here is that this example is using an extension method from the System.Reactive
NuGet package:
那里传递的是一个委托,而不是 IObservable<T>.Subscribe
所需的 IObserver<T>
。我们很快就会谈到 IObserver<T>
,但这里所发生的是,这个示例正在使用来自 System.Reactive
NuGet 包的扩展方法:
// From the System.Reactive library's ObservableExtensions class public static IDisposable Subscribe<T>(this IObservable<T> source, Action<T> onNext)
This is a helper method that wraps a delegate in an implementation of IObserver<T>
and then passes that to IObservable<T>.Subscribe
. The effect is that we can write just a simple method (instead of a complete implementation of IObserver<T>
) and the observable source will invoke our callback each time it wants to supply a value. It's more common to use this kind of helper than to implement Rx's interfaces ourselves.
这是一个辅助方法,它将一个委托包装在 IObserver<T>
的实现中,然后将其传递给 IObservable<T>.Subscribe
。其效果是,我们可以只编写一个简单的方法(而不是 IObserver<T>
的完整实现),并且每当可观察源想要提供一个值时,它都会调用我们的回调。与我们自己实现 Rx 的接口相比,使用这种辅助方法更为常见。
Hot and Cold Sources 热源和冷源
Since an IObservable<T>
cannot supply us with values until we subscribe, the time at which we subscribe can be important. Imagine an IObservable<Trade>
describing trades occurring in some market. If the information it supplies is live, it's not going to tell you about any trades that occurred before you subscribed. In Rx, sources of this kind are described as being hot.
由于 IObservable<T>
在我们订阅之前无法为我们提供值,因此订阅的时间可能很重要。想象一个 IObservable<Trade>
,它描述某个市场上发生的交易。如果它提供的信息是实时的,那么它就不会告诉你订阅之前发生的任何交易。在 Rx(响应式扩展)中,这种类型的源被描述为“热”的(这种源被称为热源)。
Not all sources are hot. There's nothing stopping an IObservable<T>
always supplying the exact same sequence of events to any subscriber no matter when the call to Subscribe
occurs. (Imagine an IObservable<Trade>
which, instead of reporting live information, generates notifications based on recorded historical trade data.) Sources where it doesn't matter at all when you subscribe are known as cold sources.
并非所有源都是“热”的(热源)。没有任何东西会阻止 IObservable<T>
总是向任何订阅者提供完全相同的事件序列,无论 Subscribe
调用何时发生。(想象一个 IObservable<Trade>
,它不是报告实时信息,而是根据记录的历史交易数据生成通知。)无论何时订阅都无关紧要的源被称为“冷”源。
Here are some sources that might be represented as hot observables: 以下是一些可能表示为热可观察对象(hot observables)的源:
- Measurements from a sensor 来自传感器的测量
- Price ticks from a trading exchange 来自交易交易所的价格变动
- An event source that distributes events immediately such as Azure Event Grid 一个立即分发事件的源,如 Azure Event Grid
- mouse movements 鼠标移动
- timer events 定时器事件
- broadcasts like ESB channels or UDP network packets 如企业服务总线(ESB)通道或用户数据报协议(UDP)网络数据包等广播
And some examples of some sources that might make good cold observables: 以下是一些适合用来作冷可观察对象的源的例子:
- the contents of a collection (such as is returned by the
ToObservable
extension method forIEnumerable<T>
) 集合的内容(例如由IEnumerable<T>
的ToObservable
扩展方法返回的内容) - a fixed range of values, such as
Observable.Range
produces 一个固定范围的值,例如Observable.Range
产生的 - events generated based on an algorithm, such as
Observable.Generate
produces 基于算法生成的事件集,如Observable.Generate
产生的事件集 - a factory for an asynchronous operation, such as
FromAsync
returns 一个异步操作的工厂,例如FromAsync
返回的 - events produced by running conventional code such as a loop; you can create such sources with
Observable.Create
通过运行传统代码(如循环)产生的事件; 您可以使用Observable.Create
创建这样的源 - a streaming event provides such as Azure Event Hub or Kafka (or any other streaming-style source which holds onto events from the past to be able to deliver events from a particular moment in the stream; so not an event source in the Azure Event Grid style) 一个流式事件提供者,如 Azure Event Hub 或 Kafka(或任何其他流式风格的源,它们保留过去的事件以便能够从流中的特定时刻传递事件;所以不是Azure Event Grid 风格的事件源)
Not all sources are strictly completely hot or cold. For example, you could imagine a slight variation on a live IObservable<Trade>
where the source always reports the most recent trade to new subscribers. Subscribers can count on immediately receiving something, and will then be kept up to date as new information arrives. The fact that new subscribers will always receive (potentially quite old) information is a cold-like characteristic, but it's only that first event that is cold. It's still likely that a brand new subscriber will have missed lots of information that would have been available to earlier subscribers, making this source more hot than cold.
并非所有源都严格地完全是热源或冷源。例如,你可以想象一个实时 IObservable<Trade>
的轻微变种,其中源总是向新订阅者报告最新的交易。订阅者可以指望立即收到一些信息,并且随着新信息的到来而不断更新。事实上,新订阅者总是会收到(可能相对旧的)信息,这是一个冷的特征,但这只是第一个事件是冷的。很可能新订阅者会错过许多早期订阅者本可以获取的信息,这使得这个源更倾向于热源而非冷源。
There's an interesting special case in which a source of events has been designed to enable applications to receive every single event in order, exactly once. Event streaming systems such as Kafka or Azure Event Hub have this characteristic—they retain events for a while, to ensure that consumers don't miss out even if they fall behind from time to time. The standard input (stdin) for a process also has this characteristic: if you run a command line tool and start typing input before it is ready to process it, the operating system will hold that input in a buffer, to ensure that nothing is lost. Windows does something similar for desktop applications: each application thread gets a message queue so that if you click or type when it's not able to respond, the input will eventually be processed. We might think of these sources as cold-then-hot. They're like cold sources in that we won't miss anything just because it took us some time to start receiving events, but once we start retrieving the data, then we can't generally rewind back to the start. So once we're up and running they are more like hot events.
存在一个有趣的特殊情况,即事件源被设计为能够使应用程序按顺序接收每一个事件,且每个事件仅接收一次。像 Kafka 或 Azure Event Hub 这样的事件流系统就具有这一特性——它们会保留事件一段时间,以确保即使消费者偶尔落后,也不会错过任何事件。进程的标准输入(stdin)也具有这一特性:如果你运行一个命令行工具并在它准备好处理之前开始输入,操作系统会将输入保存在缓冲区中,以确保不会丢失任何内容。Windows 对于桌面应用程序也做了类似的处理:每个应用程序线程都会获得一个消息队列,因此,即使在你点击或输入时它无法响应,输入最终也会被处理。我们可以将这些源视为先冷后热的源。它们像冷源一样,因为我们不会因为开始接收事件时稍有延迟而错过任何内容,但一旦我们开始检索数据,通常就无法再回退到开头。因此,一旦我们启动并运行,它们就更像热源了。
This kind of cold-then-hot source can present a problem if we want to attach multiple subscribers. If the source starts providing events as soon as subscription occurs, then that's fine for the very first subscriber: it will receive any events that were backed up waiting for us to start. But if we wanted to attach multiple subscribers, we've got a problem: that first subscriber might receive all the notifications that were sitting waiting in some buffer before we manage to attach the second subscriber. The second subscriber will miss out.
如果我们想要连接多个订阅者,这种先冷后热的源可能会带来问题。如果数据源在订阅后立即开始提供事件,那么对于第一个订阅者来说是没问题的:它会接收到任何在等待我们开始之前备份的事件。但是,如果我们想要连接多个订阅者,就会出现问题:在我们成功连接第二个订阅者之前,第一个订阅者可能会接收到所有之前存储在某个缓冲区中的通知。这样,第二个订阅者就会错过这些通知。
In these cases, we really want some way to rig up all our subscribers before kicking things off. We want subscription to be separate from the act of starting. By default, subscribing to a source implies that we want it to start, but Rx defines a specialised interface that can give us more control: IConnectableObservable<T>
. This derives from IObservable<T>
, and adds just a single method, Connect
:
在这些情况下,我们确实希望在正式开始之前,有一种方法能预先设置好所有订阅者。我们希望订阅行为与启动行为分开。默认情况下,订阅一个数据源意味着我们希望它开始运行,但Rx(Reactive Extensions)定义了一个专门的接口,可以让我们拥有更多的控制权:IConnectableObservable<T>
。这个接口继承自IObservable<T>
,并增加了一个名为Connect
的方法。
public interface IConnectableObservable<out T> : IObservable<T> { IDisposable Connect(); }
This is useful in these scenarios where there will be some process that fetches or generates events and we need to make sure we're prepared before that starts. Because an IConnectableObservable<T>
won't start until you call Connect
, it provides you with a way to attach however many subscribers you need before events begin to flow.
这在以下场景中非常有用:在这些场景中,会有一些进程来获取或生成事件,我们需要确保在这些事件开始之前已经做好了准备。因为IConnectableObservable<T>
只有在调用Connect
方法后才会开始,所以它提供了一种方式,让你在事件开始流动之前,可以连接任意数量的订阅者。
The 'temperature' of a source is not necessarily evident from its type. Even when the underlying source is an IConnectableObservable<T>
, that can often be hidden behind layers of code. So whether a source is hot, cold, or something in between, most of the time we just see an IObservable<T>
. Since IObservable<T>
defines just one method, Subscribe
, you might be wondering how we can do anything interesting with it. The power comes from the LINQ operators that the System.Reactive
NuGet library supplies.
一个源的“温度”(即其事件发布的行为特性)并不总是能从其类型上直接看出。即使底层源是 IConnectableObservable<T>
,它也经常被隐藏在多层代码之后。因此,一个源是热的、冷的,还是介于两者之间的某种类型,大多数情况下我们看到的只是 IObservable<T>
。由于 IObservable<T>
只定义了一个方法 Subscribe
,你可能会好奇我们如何用它来做些有趣的事情。它的强大之处在于 System.Reactive NuGet 库提供的 LINQ 运算符。
LINQ Operators and Composition LINQ 运算符与组合
So far I've shown only a very simple LINQ example, using the Where
operator to filter events down to ones that meet certain criteria. To give you a flavour of how we can build more advanced functionality through composition, I'm going to introduce an example scenario.
到目前为止,我只展示了一个非常简单的 LINQ 示例,使用 Where
运算符将事件过滤为满足特定条件的事件。为了让您了解如何通过组合构建更高级的功能,我将介绍一个示例场景。
Suppose you want to write a program that watches some folder on a filesystem, and performs automatic processing any time something in that folder changes. For example, web developers often want to trigger automatic rebuilds of their client side code when they save changes in the editor so they can quickly see the effect of their changes. Filesystem changes often come in bursts. Text editors might perform a few distinct operations when saving a file. (Some save modifications to a new file, then perform a couple of renames once this is complete, because this avoids data loss if a power failure or system crash happens to occur at the moment you save the file.) So you typically won't want to take action as soon as you detect file activity. It would be better to give it a moment to see if any more activity occurs, and take action only after everything has settled down.
假设你想编写一个程序来监视文件系统上的某个文件夹,并在该文件夹中的任何内容发生变化时自动执行处理。例如,Web开发人员通常希望在编辑器中保存更改时触发其客户端代码的自动重建,以便他们可以快速查看更改的效果。文件系统变化通常是一连串发生的。文本编辑器在保存文件时可能会执行几个不同的操作。(有些编辑器会将修改保存到一个新文件中,然后在完成此操作后进行几次重命名,因为如果在保存文件时发生电源故障或系统崩溃,这样做可以避免数据丢失。)因此,你通常不会在检测到文件活动时就立即采取行动。最好是稍等片刻,看看是否还有更多的活动发生,并在所有活动都平静下来之后再采取行动。
So we should not react directly to filesystem activity. We want take action at those moments when everything goes quiet after a flurry of activity. Rx does not offer this functionality directly, but it's possible for us to create a custom operator by combing some of the built-in operators. The following code defines an Rx operator that detects and reports such things. If you're new to Rx (which seems likely if you're reading this) it probably won't be instantly obvious how this works. This is a significant step up in complexity from the examples I've shown so far because this came from a real application. But I'll walk through it step by step, so all will become clear.
因此,我们不应该直接对文件系统的活动做出反应。我们希望在一系列活动之后,一切平静下来时才采取行动。Rx 本身并不直接提供这种功能,但我们可以通过组合一些内置的运算符来创建一个自定义运算符。下面的代码定义了一个 Rx 运算符,用于检测和报告这类情况。如果你刚接触 Rx(如果你在阅读这篇文章,那么很可能就是这样),那么这段代码的工作原理可能不会立即显而易见。与我之前展示的例子相比,这是一个复杂性的显著提升,因为它来自一个实际的应用程序。但我会逐步解释,这样一切都会变得清晰。
static class RxExt { public static IObservable<IList<T>> Quiescent<T>( this IObservable<T> src, TimeSpan minimumInactivityPeriod, IScheduler scheduler) { IObservable<int> onoffs = from _ in src from delta in Observable.Return(1, scheduler) .Concat(Observable.Return(-1, scheduler) .Delay(minimumInactivityPeriod, scheduler)) select delta; IObservable<int> outstanding = onoffs.Scan(0, (total, delta) => total + delta); IObservable<int> zeroCrossings = outstanding.Where(total => total == 0); return src.Buffer(zeroCrossings); } }
The first thing to say about this is that we are effectively defining a custom LINQ-style operator: this is an extension method which, like all of the LINQ operators Rx supplies, takes an IObservable<T>
as its implicit argument, and produces another observable source as its result. The return type is slightly different: it's IObservable<IList<T>>
. That's because once we return to a state of inactivity, we will want to process everything that just happened, so this operator will produce a list containing every value that the source reported in its most recent flurry of activity.
关于这一点,首先要说的是,我们实际上是在定义一个自定义的LINQ风格的运算符:这是一个扩展方法,就像Rx提供的所有LINQ运算符一样,它隐式地接受一个IObservable<T>
作为参数,并产生另一个可观察的数据源作为结果。返回类型略有不同:它是IObservable<IList<T>>
。这是因为一旦我们返回到非活动状态,我们将希望处理刚刚发生的所有事情,所以这个运算符将产生一个列表,该列表包含源在其最近一波(一系列)活动中报告的所有值。
When we want to show how an Rx operator behaves, we typically draw a 'marble' diagram. This is a diagram showing one or more IObservable<T>
event sources, with each one being illustrated by a horizontal line. Each event that a source produces is illustrated by a circle (or 'marble') on that line, with the horizontal position representing timing. Typically, the line has a vertical bar on its left indicating the instant at which the application subscribed to the source, unless it happens to produce events immediately, in which case it will start with a marble. If the line has an arrowhead on the right, that indicates that the observable's lifetime extends beyond the diagram. Here's a diagram showing how the Quiescent
operator above response to a particular input:
当我们想要展示一个Rx运算符的行为时,通常会绘制一个“弹珠”(marble)图。这是一个展示一个或多个IObservable<T>
事件源的图表,每个事件源都用一条水平线来表示。源产生的每个事件都用该线上的一个圆圈(或“弹珠”)来表示,圆圈的水平位置代表时间。通常,线条的左侧有一个垂直条,表示应用程序订阅该源的瞬间,除非它恰好立即产生事件,在这种情况下,它会以一个弹珠开始。如果线条的右侧有一个箭头,则表示可观察对象的生命周期超出了图表的范围。下面是一个图表,展示了上面提到的 Quiescent
运算符如何对特定输入做出响应:
This shows that the source (the top line) produced a couple of events (the values 1
and 2
, in this example), and then stopped for a bit. A little while after it stopped, the observable returned by the Quiescent
operator (the lower line) produced a single event with a list containing both of those events ([1,2]
). Then the source started up again, producing the values 3
, 4
, and 5
in fairly quick succession, and then going quiet for a bit. Again, once the quiet spell had gone on for long enough, the source returned by Quiescent
produced a single event containing all of the source events from this second burst of activity ([3,4,5]
). And then the final bit of source activity shown in this diagram consists of a single event, 6
, followed by more inactivity, and again, once the inactivity has gone on for long enough the Quiescent
source produces a single event to report this. And since that last 'burst' of activity from the source contained only a single event, the list reported by this final output from the Quiescent
observable is a list with a single value: [6]
.
这表明source(最上面的线)产生了一对事件(本例中的值为1和2) ,然后停止了一段时间。在它停止之后不久, Quiescent
运算符返回的可观察对象(下面的线)产生了一个包含这两个事件([1,2]
)的单个事件的列表。然后source再次开始,以相当快的速度连续生成值3、4和5,然后又安静了一段时间。同样,当这种安静状态持续足够长的时间后, Quiescent
返回的可观察对象产生了一个单一的事件,其中包含来自第二波活动的所有source事件([3,4,5])。然后,这个图表中显示的source活动的最后一部分包括一个单独的事件, 6
,然后是更多的静默,再一次,一旦静默持续了足够长的时间, 使得 Quiescent
可观察对象产生一个单独的事件来报告这一点。由于来自source的最后一波活动只包含一个事件,所以这个来自 Quiescent
可观察对象的最后输出的列表是一个只有一个值的列表: [6]
。
So how does the code shown achieve this? The first thing to notice about the Quiescent
method is that it's just using other Rx LINQ operators (the Return
, Scan
, Where
, and Buffer
operators are explicitly visible, and the query expression will be using the SelectMany
operator, because that's what C# query expressions do when they contain two from
clauses in a row) in a combination that produces the final IObservable<IList<T>>
output.
那么,所展示的代码是如何实现这一点的呢?关于 Quiescent
方法,首先要注意的是,它只是通过组合其他Rx LINQ运算符(明确可见的有Return
, Scan
, Where
和 Buffer
运算符,查询表达式将使用 SelectMany
操作符,因为当C#查询表达式连续包含两个from子句时,它们就是这样做的)来产生最终的 IObservable<IList<T>>
输出。
This is Rx's compositional approach, and it is how we normally use Rx. We use a mixture of operators, combined (composed) in a way that produces the effect we want.
这就是Rx的组合方法,也是我们通常使用Rx的方式。我们使用各种运算符的组合(构成),以产生我们想要的效果。
But how does this particular combination produce the effect we want? There are a few ways we could get the behaviour that we're looking for from a Quiescent
operator, but the basic idea of this particular implementation is that it keeps count of how many events have happened recently, and then produces a result every time that number drops back to zero. The outstanding
variable refers to the IObservable<int>
that tracks the number of recent events, and this marble diagram shows what it produces in response to the same source
events as were shown on the preceding diagram:
但是,这种特定的组合是如何产生我们想要的效果的呢?我们可以通过几种方式从 Quiescent
运算符中获得我们想要的行为,但这种特定实现的基本思想是,它保持对最近发生的事件数量的计数,并且每当该数量回落到零时,就产生一个结果。 outstanding
变量指的是跟踪最近事件数量的IObservable<int>
,而这个弹珠图展示了它在响应与前面图表中相同的 source
事件时所产生的内容:
I've colour coded the events this time so that I can show the relationship between source
events and corresponding events produced by outstanding
. Each time source
produces an event, outstanding
produces an event at the same time, in which the value is one higher than the preceding value produced by outstanding
. But each such source
event also causes outstanding
to produce another event two seconds later. (It's two seconds because in these examples, I've presumed that the first argument to Quiescent
is TimeSpan.FromSeconds(2)
, as shown on the first marble diagram.) That second event always produces a value that is one lower than whatever the preceding value was.
这次我为事件添加了颜色编码,以便展示 source
事件与 outstanding
产生的相应事件之间的关系。每次 source
产生事件时, outstanding
也会同时产生一个事件,其值比 outstanding
之前产生的值高1。但是,每个这样的 source
事件也会导致 outstanding
在两秒后产生另一个事件。(这里是两秒钟,因为在这些示例中,我已经假定 Quiescent
的第一个参数是 TimeSpan.FromSeconds(2)
,如第一个弹珠图所示。)第二个事件总是产生一个比前一个值低1的值。
This means that each event to emerge from outstanding
tells us how many events source
produced within the last two seconds. This diagram shows that same information in a slightly different form: it shows the most recent value produced by outstanding
as a graph. You can see the value goes up by one each time source
produces a new value. And two seconds after each value produced by source
, it drops back down by one.
这意味着每个从 outstanding
产生事件都告诉我们在过去两秒内 source
产生了多少事件。这个图表以略微不同的形式显示了相同的信息: 它以图形的形式显示了 outstanding
产生的最新值。您可以看到,每当 source
产生一个新值时,该值就会加1。在 source
生成的每个值产生两秒后,该值又会减1。
In simple cases like the final event 6
, in which it's the only event that happens at around that time, the outstanding
value goes up by one when the event happens, and drops down again two seconds later. Over on the left of the picture it's a little more complex: we get two events in fairly quick succession, so the outstanding
value goes up to one and then up to two, before falling back down to one and then down to zero again. The middle section looks a little more messy—the count goes up by one when the source
produces event 3
, and then up to two when event 4
comes in. It then drops down to one again once two seconds have passed since the 3
event, but then another event, 5
, comes in taking the total back up to two. Shortly after that it drops back to one again because it has now been two seconds since the 4
event happened. And then a bit later, two seconds after the 5
event it drops back to zero again.
在最后一个事件 6
这样的简单情况下,它是在那个时间段发生的唯一事件,当事件发生时, outstanding
的值会加1,然后在两秒后再次减1。在图片的左侧部分情况稍微复杂一些: 我们短时间内收到两个事件,所以 outstanding
的值先上升到1,然后上升到2,然后回落到1,最后再次回落到0。中间部分看起来有点乱——当 source
产生事件 3
时,计数加1,当事件 4
到来时,计数增加到2。在事件 3
发生两秒后,它再次回落到1,但随后另一个事件5的到来使数计再次增加到2。紧接着,它又回落到1,因为现在距离事件 4
发生已经过去了2秒钟。再过一会儿,在事件 5
发生后2秒钟,计数又回落到了0。
That middle section is the messiest, but it's also most representative of the kind of activity this operator is designed to deal with. Remember, the whole point here is that we're expecting to see flurries of activity, and if those represents filesystem activity, they will tend to be slightly chaotic in nature, because storage devices don't always have entirely predictable performance characteristics (especially if it's a magnetic storage device with moving parts, or remote storage in which variable networking delays might come into play).
中间部分是最混乱的,但它也最能代表这个运算符旨在处理的活动类型。请记住,这里的重点是,我们预计会看到一系列频繁的活动,如果这些活动代表文件系统活动,那么它们的性质往往会稍显混乱,因为存储设备的性能特性并不总是完全可预测的(尤其是如果它是带有活动部件的磁存储设备,或者是可能存在可变网络延迟的远程存储设备)。
With this measure of recent activity in hand, we can spot the end of bursts of activity by watching for when outstanding
drops back to zero, which is what the observable referred to by zeroCrossing
in the code above does. (That's just using the Where
operator to filter out everything except the events where outstanding
's current value returns to zero.)
有了这个衡量近期活动的方法,我们就可以通过观察 outstanding
何时回落到零来发现一波活动的结束,这正是上面代码中 zeroCrossings
所引用的可观察对象所做的。(这仅仅是使用 Where
运算符来过滤掉outstanding
的当前值没有回到零的所有事件。)
But how does outstanding
itself work? The basic approach here is that every time source
produces a value, we actually create a brand new IObservable<int>
, which produces exactly two values. It immediately produces the value 1, and then after the specified timespan (2 seconds in these examples) it produces the value -1. That's what's going in in this clause of the query expression:
但 outstanding
本身是如何工作的呢?这里的基本方法是,每次 source
产生一个值,我们实际上创建一个全新的 IObservable<int>
,它刚好产生两个值。它立即产生值1,然后在指定的时间间隔(在这些示例中为2秒)之后产生值 -1。这就是在查询表达式的下面子句中所发生的事情:
from delta in
Observable.Return(1, scheduler)
.Concat(Observable.Return(-1, scheduler)
.Delay(minimumInactivityPeriod, scheduler))
I said Rx is all about composition, and that's certainly the case here. We are using the very simple Return
operator to create an IObservable<int>
that immediately produces just a single value and then terminates. This code calls that twice, once to produce the value 1
and again to produce the value -1
. It uses the Delay
operator so that instead of getting that -1
value immediately, we get an observable that waits for the specified time period (2 seconds in these examples, but whatever minimumInactivityPeriod
is in general) before producing the value. And then we use Concat
to stitch those two together into a single IObservable<int>
that produces the value 1
, and then two seconds later produces the value -1
.
我说过,Rx(响应式扩展)的核心在于组合,这里当然也是这样。我们使用非常简单的 Return
运算符创建一个 IObservable<int>
,它只是立即产生一个值,然后终止。这段代码调用 Return
两次,一次是为了产生值 1
,另一次是为了产生值 -1
。它使用了 Delay
运算符,这样我们就不会立即得到 -1
值,而是得到一个在指定的时间段(在这些示例中为2秒,但一般情况下是 minimumInactivityPeriod
所指定的值)后产生这个值的可观察对象。然后,我们使用 Concat
将这两个 IObservable<int>
拼接成一个新的 IObservable<int>
,最终的 IObservable<int>
首先产生值1,然后在两秒钟后产生值 -1。
Although this produces a brand new IObservable<int>
for each source
event, the from
clause shown above is part of a query expression of the form from ... from .. select
, which the C# compiler turns into a call to SelectMany
, which has the effect of flattening those all back into a single observable, which is what the onoffs
variable refers to. This marble diagram illustrates that:
尽管这为每个 source
事件都生成了一个新的 IObservable<int>
,但上面展示的 from
子句是形式为 from ... from .. select
的查询表达式的一部分,C#编译器会将其转换为对 SelectMany
的调用,其效果是将所有这些可观察对象重新展平为一个单一的可观察对象,这就是 onoffs
变量所引用的。下面的弹珠图说明了这一点:
This also shows the outstanding
observable again, but we can now see where that comes from: it is just the running total of the values emitted by the onoffs
observable. This running total observable is created with this code:
这也再次展示了 outstanding
可观察对象,但是我们现在可以看到它来自哪里:它只是可观察对象 onoffs
所发出的值的累积总和。这个累积总和的可观察对象是通过以下代码创建的:
IObservable<int> outstanding = onoffs.Scan(0, (total, delta) => total + delta);
Rx's Scan
operator works much like the standard LINQ Aggregate
operator, in that it cumulatively applies an operation (addition, in this case) to every single item in a sequence. The different is that whereas Aggregate
produces just the final result once it reaches the end of the sequence, Scan
shows all of its working, producing the accumulated value so far after each input. So this means that outstanding
will produce an event every time onoffs
produces one, and that event's value will be the running total—the sum total of every value from onoffs
so far.
Rx 的 Scan
运算符的工作原理与标准 LINQ Aggregate
运算符非常相似,因为它对序列中的每项累积地应用一个运算(在本例中是加法)。不同之处在于, Aggregate
只在到达序列末尾时产生最终结果,而 Scan
会展示其全部的计算过程,在每个输入之后产生迄今为止的累积值。这意味着每当 onoffs
产生一个事件时, outstanding
将产生一个事件,并且该事件的值将是累积总和——即到目前为止从 onoffs 中获取的所有值的总和。
So that's how outstanding
comes to tell us how many events source
produced within the last two seconds (or whatever minimumActivityPeriod
has been specified).
因此,这就是 outstanding
如何告诉我们 source
在过去两秒内(或任何指定的 minimumActivityPeriod
)产生了多少事件的方式。
The final piece of the puzzle is how we go from the zeroCrossings
(which produces an event every time the source has gone quiescent) to the output IObservable<IList<T>>
, which provides all of the events that happened in the most recent burst of activity. Here we're just using Rx's Buffer
operator, which is designed for exactly this scenario: it slices its input into chunks, producing an event for each chunk, the value of which is an IList<T>
containing the items for the chunk. Buffer
can slice things up a few ways, but in this case we're using the form that starts a new slice each time some IObservable<T>
produces an item. Specifically, we're telling Buffer
to slice up the source
by creating a new chunk every time zeroCrossings
produces a new event.
这个难题的最后一部分是我们如何从 zeroCrossings
(每当source变得静止时就会产生一个事件)转换到输出 IObservable<IList<T>>
,后者提供了在最近一波活动中发生的所有事件。这里我们只需使用 Rx 的 Buffer
运算符,它正是为这种场景设计的: 它将输入切割成块,并为每个块产生一个事件,该事件的值是一个包含该块中项目的 IList<T>
。Buffer
可以通过几种方式切割数据,但是在本例中,我们使用的是每当某个 IObservable<T>
产生一个项时就开始一个新的切片的形式。具体来说,我们告诉 Buffer
通过每次 zeroCrossings
产生一个新事件时来将 source
切分成新的块。
(One last detail, just in case you saw it and were wondering, is that this method requires an IScheduler
. This is an Rx abstraction for dealing with timing and concurrency. We need it because we need to be able to generate events after a one second delay, and that sort of time-driven activity requires a scheduler.)
(最后一个小细节,以防你看到了并感到诧异,这个方法需要一个 IScheduler
。这是 Rx 中用于处理时间和并发的一个抽象概念。我们需要它是因为我们需要能够在延迟一秒后生成事件,而这种由时间驱动的活动需要一个调度器。)
We'll get into all of these operators and the workings of schedulers in more detail in later chapters. For now, the key point is that we typically use Rx by creating a combination of LINQ operators that process and combine IObservable<T>
sources to define the logic that we require.
我们将在后面的章节中更详细地介绍这些运算符和调度器的工作原理。现在,关键点是,我们通常通过使用一系列处理并组合 IObservable<T>
源的 LINQ 运算符来创建 Rx,从而定义我们所需的逻辑。
Notice that nothing in that example actually called the one and only method that IObservable<T>
defines (Subscribe
). There will always be something somewhere that ultimately consumes the events, but most of the work of using Rx tends to entail declaratively defining the IObservable<T>
s we need.
请注意,在该示例中,实际上并没有调用 IObservable<T>
定义的唯一一个方法(Subscribe)。总会有某个地方最终会消费这些事件,但使用 Rx 的大部分工作往往涉及以声明方式定义我们所需的 IObservable<T>
。
Now that you've seen an example of what Rx programming looks like, we can address some obvious questions about why Rx exists at all.
现在您已经看到了 Rx 编程的一个示例,我们可以解决一些关于为什么需要 Rx 的显而易见的问题。
What was wrong with .NET Events? .NET 事件有什么问题?
.NET has had built-in support for events from the very first version that shipped over two decades ago—events are part of .NET's type system. The C# language has intrinsic support for this in the form of the event
keyword, along with specialized syntax for subscribing to events. So why, when Rx turned up some 10 years later, did it feel the need to invent its own representation for streams of events? What was wrong with the event
keyword?
.NET 从20多年前发布的第一个版本开始就内置了对事件的支持——事件是.NET类型系统的一部分。C# 语言以 event
关键字的形式,以及用于订阅事件的专门语法,对此提供了内在的支持。那么,为什么 Rx 在大约十年后出现时,会觉得有必要发明自己表示事件流的方式呢? event
关键字有什么问题吗?
The basic problem with .NET events is that they get special handling from the .NET type system. Ironically, this makes them less flexible than if there had been no built-in support for the idea of events. Without .NET events, we would have needed some sort of object-based representation of events, at which point you can do all the same things with events that you can do with any other objects: you could store them in fields, pass them as arguments to methods, define methods on them and so on.
.NET 事件的基本问题是,它们得到了 .NET 类型系统的特殊处理。讽刺的是,如果没有对事件概念的内置支持,它们的灵活性就会降低。如果没有 .NET 事件,我们就需要某种基于对象的事件表示方式,这时你就可以像处理其他对象一样处理事件:你可以将它们存储在字段中、将它们作为参数传递给方法、在它们上定义方法等等。
To be fair to .NET version 1, it wasn't really possible to define a good object-based representation of events without generics, and .NET didn't get those until version 2 (three and a half years after .NET 1.0 shipped). Different event sources need to be able to report different data, and .NET events provided a way to parameterize events by type. But once generics came along, it became possible to define types such as IObservable<T>
, and the main advantage that events offered went away. (The other benefit was some language support for implementing and subscribing to events, but in principle that's something that could have been done for Rx if Microsoft had chosen to. It's not a feature that required events to be fundamentally different from other features of the type system.)
公正地说,在.NET 1.0版本中,没有泛型的情况下,确实很难定义一个良好的基于对象的事件表示法,而.NET直到2.0版本(即.NET 1.0发布后的三年半)才引入泛型。不同的事件源需要能够报告不同的数据,而.NET事件提供了一种通过类型对事件进行参数化的方法。但是,一旦泛型出现,定义如 IObservable<T>
这样的类型就变得可能了,而事件所提供的主要优势也随之消失。(另一个好处是,某些语言提供了实现和订阅事件的支持,但原则上,如果微软选择这样做,这些也可以为Rx实现。这并不是一个要求事件从根本上不同于类型系统其他特性的特性。)
Consider the example we've just worked through. It was possible to define our own custom LINQ operator, Quiescent
, because IObservable<T>
is just an interface like any other, meaning that we're free to write extension methods for it. You can't write an extension method for an event.
思考我们刚刚研究过的示例。我们之所以能够定义自己的自定义 LINQ 运算符,Quiescent
,是因为 IObservable<T>
就像任何其他接口一样,这意味着我们可以为它编写扩展方法。而对于事件,你不能为其编写扩展方法。
Also, we are able to wrap or adapt IObservable<T>
sources. Quiescent
took an IObservable<T>
as an input, and combined various Rx operators to produce another observable as an output. Its input was a source of events that could be subscribed to, and its output was also a source of events that could be subscribed to. You can't do this with .NET events—you can't write a method that accepts an event as an argument, or that returns an event.
此外,我们还能够包装或适配 IObservable<T>
源。 Quiescent
接受一个 IObservable<T>
作为输入,并组合各种 Rx 运算符来产生另一个可观察对象作为输出。它的输入是一个可以订阅的事件源,它的输出也是一个可以订阅的事件源。而 .NET 事件则无法做到这一点——你不能编写一个接受事件作为参数或返回事件的方法。
These limitations are sometimes described by saying that .NET events are not first class citizens. There are things you can do with values or references in .NET that you can't do with events.
这些限制有时被描述为 .NET 事件不是一等公民。在 .NET 中,有一些你可以对值或引用执行的操作,但你不能对事件执行这些操作。
If we represent an event source as a plain old interface, then it is a first class citizen: it can use all of the functionality we expect with other objects and values precisely because it's not something special.
如果我们将事件源表示为一个普通的旧接口,那么它就是一等公民:它可以使用我们期望与其他对象和值一起使用的所有功能,正是因为它不是什么特殊的东西。
What about Streams? 那么流呢?
I've described IObservable<T>
as representing a stream of events. This raises an obvious question: .NET already has System.IO.Stream
, so why not just use that?
我将 IObservable<T>
描述为表示事件流。这引出了一个显而易见的问题:.NET 已经有了 System.IO.Stream
,那么为什么不直接使用它呢?
The short answer is that streams are weird because they represent an ancient concept in computing dating back long before the first ever Windows operating system shipped, and as such they have quite a lot of historical baggage. This means that even a scenario as simple as "I have some data, and want to make that available immediately to all interested parties" is surprisingly complex to implement though the Stream
type.
简短的回答是,stream很奇怪,因为它们代表了一个古老的计算机概念,其历史可以追溯到第一个 Windows 操作系统发布之前的很久。因此,流带有大量的历史遗留问题。这意味着,即使是一个像“我有一些数据,并希望立即让所有感兴趣的一方都能访问这些数据”这样简单的场景,通过 Stream
类型来实现也异常复杂。
Moreover, Stream
doesn't provide any way to indicate what type of data will emerge—it only knows about bytes. Since .NET's type system supports generics, it is natural to want the types that represent event streams to indicate the event type through a type parameter.
此外, Stream
没有提供任何方式来指示将出现什么类型的数据——它只知道字节。由于 .NET 的类型系统支持泛型,因此很自然地希望表示事件流的类型能够通过类型参数来指定事件类型。
So even if you did use Stream
as part of your implementation, you'd want to introduce some sort of wrapper abstraction. If IObservable<T>
didn't exist, you'd need to invent it.
因此,即使你使用 Stream
作为实现的一部分,你也会想要引入某种包装抽象。如果不存在 IObservable<T>
,你就需要发明它。
It's certainly possible to use IO streams in Rx, but they are not the right primary abstraction.
在 Rx 中使用 IO 流当然是可能的,但它们并不是正确的首要抽象。
(If you are unconvinced, see Appendix A: What's Wrong with Classic IO Streams for a far more detailed explanation of exactly why Stream
is not well suited to this task.)
(如果你还有疑虑,请参见附录A:经典IO流的问题,其中对为什么 Stream
不适合这项任务进行了更详细的解释。)
Now that we've seen why IObservable<T>
needs to exist, we need to look at its counterpart, IObserver<T>
.
既然我们已经了解了为什么需要 IObservable<T>
,接下来我们就需要看看它的对应部分,即 IObserver<T>
。
IObserver<T> 观察者
Earlier, I showed the definition of IObservable<T>
. As you saw, it has just one method, Subscribe
. And this method takes just one argument, of type IObserver<T>
. So if you want to observe the events that an IObservable<T>
has to offer, you must supply it with an IObserver<T>
. In the examples so far, we've just supplied a simple callback, and Rx has wrapped that in an implementation of IObserver<T>
for us, but even though this is very often the way we will receive notifications in practice, you still need to understand IObserver<T>
to use Rx effectively. It is not a complex interface:
前面,我展示了 IObservable<T>
的定义。正如你所看到的,它只有一个方法,即 Subscribe
。这个方法只接受一个参数,类型为 IObserver<T>
。因此,如果你想要观察 IObservable<T>
提供的事件,你必须为它提供一个 IObserver<T>
。在之前的示例中,我们只提供了一个简单的回调函数,而 Rx 为我们将其包装在了 IObserver<T>
的实现中。虽然这通常是我们在实践中接收通知的方式,但为了有效地使用 Rx,你仍然需要理解 IObserver<T>
。这并不是一个复杂的接口:
public interface IObserver<in T> { void OnNext(T value); void OnError(Exception error); void OnCompleted(); }
As with IObservable<T>
, you can find the source for IObserver<T>
in the .NET runtime GitHub repository, because both of these interfaces are built into the runtime libraries.
与 IObservable<T>
一样,你可以在 .NET 运行时的 GitHub 仓库中找到 IObserver<T>
的源代码,因为这两个接口都被构建在了运行时库中。
If we wanted to create an observer that printed values to the console it would be as easy as this:
如果我们想要创建一个将值打印到控制台的观察者,那么就像这样简单:
public class MyConsoleObserver<T> : IObserver<T> { public void OnNext(T value) { Console.WriteLine($"Received value {value}"); } public void OnError(Exception error) { Console.WriteLine($"Sequence faulted with {error}"); } public void OnCompleted() { Console.WriteLine("Sequence terminated"); } }
In the preceding chapter, I used a Subscribe
extension method that accepted a delegate which it invoked each time the source produced an item. This method is defined by Rx's ObservableExtensions
class, which also defines various other extension methods for IObservable<T>
. It includes overloads of Subscribe
that enable me to write code that has the same effect as the preceding example, without needing to provide my own implementation of IObserver<T>
:
在上一章中,我使用了一个 Subscribe
扩展方法,它接受一个委托,每次源产生一项时都会调用该委托。这个方法是由 Rx 的 ObservableExtensions
类定义的,该类还为 IObservable<T>
定义了各种其他扩展方法。它包括了 Subscribe
的重载,使我能够编写与前面示例具有相同效果的代码,而无需提供自己的 IObserver<T>实现
:
source.Subscribe( value => Console.WriteLine($"Received value {value}"), error => Console.WriteLine($"Sequence faulted with {error}"), () => Console.WriteLine("Sequence terminated") );
The overloads of Subscribe
where we don't pass all three methods (e.g., my earlier example just supplied a single callback corresponding to OnNext
) are equivalent to writing an IObserver<T>
implementation where one or more of the methods simply has an empty body. Whether we find it more convenient to write our own type that implements IObserver<T>
, or just supply callbacks for some or all of its OnNext
, OnError
and OnCompleted
method, the basic behaviour is the same: an IObservable<T>
source reports each event with a call to OnNext
, and tells us that the events have come to an end either by calling OnError
or OnCompleted
.
如果我们没有传递所有三个方法(例如,我之前的示例只提供了一个对应于 OnNext
的单个回调), Subscribe
的这些重载就等价于编写一个 IObserver<T>
实现,其中有一个或多个方法是空函数。无论我们是觉得编写自己的 IObserver<T> 的实现类更方便,还是只为 OnNext,
OnError
和 OnCompleted
方法中的一些方法或所有方法提供回调,基本行为都是相同的:一个 IObservable<T>
源通过调用 OnNext 来报告每个事件,并通过调用 OnError
或 OnCompleted
来告诉我们事件已经结束。
If you're wondering whether the relationship between IObservable<T>
and IObserver<T>
is similar to the relationship between IEnumerable<T>
and IEnumerator<T>
, then you're onto something. Both IEnumerator<T>
and IObservable<T>
represent potential sequences. With both of these interfaces, they will only supply data if we ask them for it. To get values out of an IEnumerable<T>
, an IEnumerator<T>
needs to come into existence, and similarly, to get values out of an IObservable<T>
requires an IObserver<T>
.
如果你正在思索 IObservable<T>
和 IObserver<T>
之间的关系是否与 IEnumerable<T>
和 IEnumerator<T>
之间的关系相似,那么你想到了一些东西。 IEnumerable<T>
和 IObservable<T>
都代表潜在的序列。对于这两个接口,它们只会在我们请求时才提供数据。要从 IEnumerable<T>
中获取值,需要有一个 IEnumerator<T>
存在,同样地,要从 IObservable<T>
中获取值,则需要一个 IObserver<T>
。
The difference reflects the fundamental pull vs push difference between IEnumerable<T>
and IObservable<T>
. Whereas with IEnumerable<T>
we ask the source to create an IEnumerator<T>
for us which we can then use to retrieve items (which is what a C# foreach
loop does), with IObservable<T>
, the source does not implement IObserver<T>
: it expects us to supply an IObserver<T>
and it will then push its values into that observer.
这种区别反映了 IEnumerable<T>
和 IObservable<T>
之间的拉取与推送的根本差异。在 IEnumerable<T>
中,我们要求源为我们创建一个 IEnumerator<T>
,然后我们可以使用它来检索项(这就是 C# 中的 foreach
循环所做的)。而在 IObservable<T>
中,源并不实现 IObserver<T>
:它期望我们提供一个 IObserver<T>
,然后它将把值推送到该观察者中。
So why does IObserver<T>
have these three methods? Remember when I said that in an abstract sense, IObserver<T>
represents the same thing as IEnumerable<T>
? I meant it. It might be an abstract sense, but it is precise: IObservable<T>
and IObserver<T>
were designed to preserve the exact meaning of IEnumerable<T>
and IEnumerator<T>
, changing only the detailed mechanism of consumption.
那么,为什么 IObserver<T>
有这三个方法呢?还记得我说过在抽象意义上, IObserver<T>
和 IEnumerator<T>
代表同样的东西吗?我是认真的。这可能是一种抽象意义上的说法,但它很精确:IObservable<T>
和 IObserver<T>
的设计旨在保留 IEnumerable<T>
和 IEnumerator<T>
的确切含义,只改变消费的具体机制。
To see what that means, think about what happens when you iterate over an IEnumerable<T>
(with, say, a foreach
loop). With each iteration (and more precisely, on each call to the enumerator's MoveNext
method) there are three things that could happen: 要理解这意味着什么,请思考当你遍历一个 IEnumerable<T>
(例如,使用 foreach 循环)时会发生什么。在每次迭代中(更准确地说,在每次调用枚举器的 MoveNext
方法时),可能会发生三种事情:
MoveNext
could returntrue
to indicate that a value is available in the enumerator'sCurrent
propertyMoveNext
可能返回true
,它指示枚举器的Current
属性中的值可用MoveNext
could throw an exceptionMoveNext
可能会抛出异常MoveNext
could returnfalse
to indicate that you've reached the end of the collectionMoveNext
可能返回 false 来表示您已经到达集合的末尾
These three outcomes correspond precisely to the three methods defined by IObserver<T>
. We could describe these in slightly more abstract terms: 这三种结果恰好与 IObserver<T>
定义的三个方法相对应。我们可以用稍微更抽象的方式来描述这些:
- Here's another item 这是另一项
- It has all gone wrong 一切都出错了
- There are no more items 没有项了
That describes the three things that either can happen next when consuming either an IEnumerable<T>
or an IObservable<T>
. The only difference is the means by which consumers discover this. With an IEnumerable<T>
source, each call to MoveNext
will tell us which of these three applies. And with an IObservable<T>
source, it will tell you one of these three things with a call to the corresponding member of your IObserver<T>
implementation.
这描述了在消费 IEnumerable<T>
或 IObservable<T>
时接下来可能发生的三种事情。唯一的区别在于消费者发现这一点的方式。对于 IEnumerable<T>
源,每次调用 MoveNext
都会告诉我们这三种事情的哪一个适用。而对于 IObservable<T>
源,它会通过调用你的 IObserver<T>
实现的相应成员来告诉你这三种的哪一个。
The Fundamental Rules of Rx Sequences Rx 序列的基本规则
Notice that two of the three outcomes in the list above are terminal. If you're iterating through an IEnumerable<T>
with a foreach
loop, and it throws an exception, the foreach
loop will terminate. The C# compiler understands that if MoveNext
throws, the IEnumerator<T>
is now done, so it disposes it and then allows the exception to propagate. Likewise, if you get to the end of a sequence, then you're done, and the compiler understands that too: the code it generates for a foreach
loop detects when MoveNext
returns false and when that happens it disposes the enumerator and then moves onto the code after the loop.
请注意,上述列表中的三个结果中有两个是终止性的。如果你使用 foreach 循环遍历一个 IEnumerable<T>,并且它抛出一个异常,那么 foreach 循环将会终止。C# 编译器理解,如果 MoveNext 抛出异常,那么 IEnumerator<T> 现在已经完成,因此它会释放它,然后允许异常传播。同样地,如果你到达序列的末尾,那么你就完成了,编译器也理解这一点:它为 foreach 循环生成的代码会检测 MoveNext 返回 false 的情况,当这种情况发生时,它会释放枚举器,然后移动到循环之后的代码。
These rules might seem so obvious that we might never even think about them when iterating over IEnumerable<T>
sequences. What might be less immediately obvious is that exactly the same rules apply for an IObservable<T>
sequence. If an observable source either tells an observer that the sequence has finished, or reports an error, then in either case, that is the last thing the source is allowed to do to the observer.
这些规则可能看起来如此明显,以至于我们在遍历 IEnumerable<T> 序列时可能从未想过它们。可能不那么明显的是,对于 IObservable<T> 序列,完全相同的规则也适用。如果一个可观察的数据源要么告诉观察者序列已经结束,要么报告一个错误,那么在任何一种情况下,这都是该数据源被允许对观察者做的最后一件事。
That means these examples would be breaking the rules:
这意味着这些例子将打破规则:
public static void WrongOnError(IObserver<int> obs) { obs.OnNext(1); obs.OnError(new ArgumentException("This isn't an argument!")); obs.OnNext(2); // Against the rules! We already reported failure, so iteration must stop } public static void WrongOnCompleted(IObserver<int> obs) { obs.OnNext(1); obs.OnCompleted(); obs.OnNext(2); // Against the rules! We already said we were done, so iteration must stop } public static void WrongOnErrorAndOnCompleted(IObserver<int> obs) { obs.OnNext(1); obs.OnError(new ArgumentException("A connected series of statements was not supplied")); // This next call is against the rules because we reported an error, and you're not // allowed to make any further calls after you did that. obs.OnCompleted(); } public static void WrongOnCompletedAndOnError(IObserver<int> obs) { obs.OnNext(1); obs.OnCompleted(); // This next call is against the rule because already said we were done. // When you terminate a sequence you have to pick between OnCompleted or OnError obs.OnError(new ArgumentException("Definite proposition not established")); }
These correspond in a pretty straightforward way to things we already know about IEnumerable<T>
: 它们以一种非常直观的方式对应于我们已经知道的 IEnumerable<T>
:
WrongOnError
: if an enumerator throws fromMoveNext
, it's done and you mustn't callMoveNext
again, so you won't be getting any more items out of it 如果枚举器在调用MoveNext
时抛出异常,那么它就已经完成了工作,你不应再次调用MoveNext
,因此你将无法再从它那里获取任何更多的项。WrongOnCompleted
: if an enumerator returnsfalse
fromMoveNext
, it's done and you mustn't callMoveNext
again, so you won't be getting any more items out of it 如果枚举器的MoveNext
方法返回false
,那么它就已经遍历完毕,你不应再次调用MoveNext
,因此你将无法再从它那里获取任何更多的项。WrongOnErrorAndOnCompleted
: if an enumerator throws fromMoveNext
, that means its done, it's done and you mustn't callMoveNext
again, meaning it won't have any opportunity to tell that it's done by returningfalse
fromMoveNext
如果枚举器在调用MoveNext
时抛出异常,那就意味着它已经完成了工作,确实已经完成了,你不应再次调用MoveNext
,这意味着它将没有机会通过MoveNext
返回false
来表明它已经遍历完毕。WrongOnCompletedAndOnError
: if an enumerator returnsfalse
fromMoveNext
, it's done and you mustn't callMoveNext
again, meaning it won't have any opportunity to also throw an exception 如果枚举器的MoveNext
方法返回false
,那就意味着它已经遍历完毕,你不应再次调用MoveNext
,这也意味着它不会再有任何机会抛出异常。
Because IObservable<T>
is push-based, the onus for obeying all of these rules fall on the observable source. With IEnumerable<T>
, which is pull-based, it's up to the code using the IEnumerator<T>
(e.g. a foreach
loop) to obey these rules. But they are essentially the same rules.
由于IObservable<T>
是基于推送的,因此遵守所有这些规则的责任落在可观察源上。而对于基于拉取的IEnumerable<T>
,遵守这些规则的责任则落在使用IEnumerator<T>
的代码上(例如,foreach循环)。但它们的规则本质上是相同的。
There's an additional rule for IObserver<T>
: if you call OnNext
you must wait for it to return before making any more method calls into the same IObserver<T>
. That means this code breaks the rules:
对于IObserver<T>
还有一个额外的规则:如果你调用了OnNext
方法,你必须等待它返回后才能对同一个IObserver<T>
进行更多的方法调用。这意味着以下代码违反了规则:
public static void EverythingEverywhereAllAtOnce(IEnumerable<int> obs) { Random r = new(); for (int i = 0; i < 10000; ++i) { int v = r.Next(); Task.Run(() => obs.OnNext(v)); // Against the rules! }}
This calls obs.OnNext
10,000 times, but it executes these calls as individual tasks to be run on the thread pool. The thread pool is designed to be able to execute work in parallel, and that's a a problem here because nothing here ensures that one call to OnNext
completes before the next begins. We've broken the rule that says we must wait for each call to OnNext
to return before calling either OnNext
, OnError
, or OnComplete
on the same observer. (Note: this assumes that the caller won't subscribe the same observer to multiple different sources. If you do that, you can't assume that all calls to its OnNext
will obey the rules, because the different sources won't have any way of knowing they're talking to the same observer.)
这段代码对obs.OnNext
调用了10,000次,但将这些调用作为要在线程池上运行的独立任务执行。线程池被设计为能够并行执行工作,而在这里这是一个问题,因为没有任何东西确保一个OnNext
调用在完成之前下一个调用不会开始。我们违反了这样一个规则:即在同一个观察者上调用OnNext
、OnError
或OnComplete
之前,必须等待每个OnNext
调用的返回。(注意:这假设调用者不会将同一个观察者订阅到多个不同的源。如果你这样做,你不能假设其所有OnNext
调用都会遵守规则,因为不同的源没有任何方式来知道它们在与同一个观察者通信。)
This rule is the only form of back pressure built into Rx.NET: since the rules forbid calling OnNext
if a previous call to OnNext
is still in progress, this enables an IObserver<T>
to limit the rate at which items arrive. If you just don't return from OnNext
until you're ready, the source is obliged to wait. However, there are some issues with this. Once schedulers get involved, the underlying source might not be connected directly to the final observer. If you use something like ObserveOn
it's possible that the IObserver<T>
subscribed directly to the source just puts items on a queue and immediately returns, and those items will then be delivered to the real observer on a different thread. In these cases, the 'back pressure' caused by taking a long time to return from OnNext
only propagates as far as the code pulling items off the queue.
这个规则是Rx.NET中内置的唯一一种背压形式:由于规则禁止在前一个OnNext
调用仍在进行时调用OnNext
,这使得IObserver<T>
能够限制项到达的速率。如果你只在准备好(处理下一个)之后才从OnNext
返回,则源必须等待。然而,这样做存在一些问题。一旦加入调度器,底层数据源可能不是直接连接到最终观察者。如果你使用了类似 ObserveOn
这样的东西,那么直接订阅到源上的IObserver<T>
可能只是将项放入队列并立即返回,而这些项随后将在另外的线程上传递给真正的观察者。在这些情况下,由于OnNext
返回耗时较长而产生的“背压”只会传播到从队列中拉取项目的代码。
It may be possible to use certain Rx operators (such as Buffer
or Sample
) to mitigate this, but there are no built-in mechanisms for cross-thread propagation of back pressure. Some Rx implementations on other platforms have attempted to provide integrated solutions for this; in the past when the Rx.NET development community has looked into this, some thought that these solutions were problematic, and there is no consensus on what a good solution looks like. So with Rx.NET, if you need to arrange for sources to slow down when you are struggling to keep up, you will need to introduce some mechanism of your own. (Even with Rx platforms that do offer built-in back pressure, they can't provide a general-purpose answer to the question: how do we make this source provide events more slowly? How (or even whether) you can do that will depend on the nature of the source. So some bespoke adaptation is likely to be necessary in any case.)
可以使用某些Rx运算符(如 Buffer
或Sample
)来缓解这个问题,但Rx.NET中并没有跨线程传播背压的内置机制。其他平台上的一些Rx实现试图为此提供集成解决方案;过去当Rx.NET开发社区研究这个问题时,有人认为这些解决方案存在问题,并且对于什么是好的解决方案也没有达成共识。因此,在使用Rx.NET时,如果你需要在难以跟上时让源减慢速度,你将需要引入自己的一些机制。(即使在提供内置背压机制的Rx平台上,它们也无法为以下问题提供一个通用的答案:如何让这个源更慢地提供事件?如何(或能否)做到这一点将取决于源的性质。因此,在任何情况下,进行一些定制化的适配可能都是必要的。)
This rule in which we must wait for OnNext
to return is tricky and subtle. It's perhaps less obvious than the others, because there's no equivalent rule for IEnumerable<T>
—the opportunity to break this rule only arises when the source pushes data into the application. You might look at the example above and think "well who would do that?" However, multithreading is just an easy way to show that it is technically possible to break the rule. The harder cases are where single-threaded re-entrancy occurs. Take this code:
这条规则,即我们必须等待OnNext
返回,是复杂且微妙的。它可能比其他规则更不明显,因为IEnumerable<T>
没有类似的规则——只有在数据源将数据推送到应用程序中时,才有机会违反这条规则。你可能会看到上面的例子并想“谁会这么做?”然而,多线程只是展示技术上可能违反这条规则的一种简单方式。更困难的情况是单线程重入的情况。看看这段代码:
public class GoUntilStopped { private readonly IObserver<int> observer; private bool running; public GoUntilStopped(IObserver<int> observer) { this.observer = observer; } public void Go() { this.running = true; for (int i = 0; this.running; ++i) { this.observer.OnNext(i); } } public void Stop() { this.running = false; this.observer.OnCompleted(); } }
This class takes an IObserver<int>
as a constructor argument. When you call its Go
method, it repeatedly calls the observer's OnNext
until something calls its Stop
method.
这个类将IObserver<int>
作为构造函数参数。当你调用它的Go
方法时,它会反复调用观察者的OnNext
方法,直到有某个东西调用它的Stop
方法为止。
Can you see the bug?
你能看出这个bug吗?
We can take a look at what happens by supplying an IObserver<int>
implementation:
我们可以通过提供一个IObserver<int>
的实现来看一看会发生什么:
public class MyObserver : IObserver<int> { private GoUntilStopped? runner; public void Run() { this.runner = new(this); Console.WriteLine("Starting..."); this.runner.Go(); Console.WriteLine("Finished"); } public void OnCompleted() { Console.WriteLine("OnCompleted"); } public void OnError(Exception error) { } public void OnNext(int value) { Console.WriteLine($"OnNext {value}"); if (value > 3) { Console.WriteLine($"OnNext calling Stop"); this.runner?.Stop(); } Console.WriteLine($"OnNext returning"); } }
Notice that the OnNext
method looks at its input, and if it's greater than 3, it tells the GoUntilStopped
object to stop.
请注意,OnNext
方法会检查其输入,如果输入大于3,它会告诉GoUntilStopped
对象停止。
Let's look at the output:
让我们看一看输出:
Starting... OnNext 0 OnNext returning OnNext 1 OnNext returning OnNext 2 OnNext returning OnNext 3 OnNext returning OnNext 4 OnNext calling Stop OnCompleted OnNext returning Finished
The problem is right near the end. Specifically, these two lines:
问题就在最后,具体来说,这两句话:
OnCompleted
OnNext returning
This tells us that the call to our observer's OnCompleted
happened before a call in progress to OnNext
returned. It didn't take multiple threads to make this occur. It happened because the code in OnNext
decides whether it wants to keep receiving events, and when it wants to stop, it immediately calls the GoUntilStopped
object's Stop
method. There's nothing wrong with that. Observers are allowed to make outbound calls to other objects inside OnNext
, and it's actually quite common for an observer to inspect an incoming event and decide that it wants to stop.
这告诉我们,对我们观察者的OnCompleted
的调用发生在OnNext
的调用返回之前。这并不是因为使用了多个线程才发生的。这是因为OnNext
中的代码决定了是否想要继续接收事件,当它想要停止时,它会立即调用GoUntilStopped
对象的Stop
方法。这本身没有问题。观察者被允许在OnNext
内部向其他对象发出调用,实际上,观察者检查传入的事件并决定它想要停止是很常见的。
The problem is in the GoUntilStopped.Stop
method. This calls OnCompleted
but it makes no attempt to determine whether a call to OnNext
is in progress.
问题在于GoUntilStopped.Stop
方法。这个方法调用了OnCompleted
,但它没有尝试确定是否有一个对OnNext
的调用正在进行中。
This can be a surprisingly tricky problem to solve. Suppose GoUntilStopped
did detect that there was a call in progress to OnNext
. What then? In the multithreaded case, we could have solved this by using lock
or some other synchronization primitive to ensure that calls into the observer happened one at at time, but that won't work here: the call to Stop
has happened on the same thread that called OnNext
. The call stack will look something like this at the moment where Stop
has been called and it wants to call OnCompleted
:
这个问题可能是一个惊人地难以解决的问题。假设GoUntilStopped
确实检测到OnNext
的调用正在进行中。那接下来怎么做呢?在多线程的情况下,我们可以通过使用 lock
或其他同步原语来确保对观察者的调用一次只发生一个,但在这里行不通:因为Stop
的调用发生在与OnNext
调用相同的线程上。在Stop
被调用并想要调用OnCompleted
的那一刻,调用栈看起来会像这样:
`GoUntilStopped.Go`
`MyObserver.OnNext`
`GoUntilStopped.Stop`
Our GoUntilStopped.Stop
method needs to wait for OnNext
to return before calling OnCompleted
. But notice that the OnNext
method can't return until our Stop
method returns. We've managed to create a deadlock with single-threaded code!
我们的GoUntilStopped.Stop
方法需要在调用OnCompleted
之前等待OnNext
返回。但请注意,OnNext
方法无法在我们的Stop
方法返回之前返回。我们成功地用单线程代码创建了一个死锁!
In this case it's not all that hard to fix: we could modify Stop
so it just sets the running
field to false
, and then move the call to OnComplete
into the Go
method, after the for
loop. But more generally this can be a hard problem to fix, and it's one of the reasons for using the System.Reactive
library instead of just attempting to implement IObservable<T>
and IObserver<T>
directly. Rx has general purpose mechanisms for solving exactly this kind of problem. (We'll see these when we look at Scheduling.) Moreover, all of the implementations Rx provides take advantage of these mechanisms for you.
在这种情况下,修复它并不太难:我们可以修改Stop
方法,使其只是将running
字段设置为false
,然后将对OnCompleted
的调用移到Go
方法中,放在for
循环之后。但更普遍地,这可能是一个难以解决的问题,这也是为什么我们使用System.Reactive
库而不是直接尝试实现IObservable<T>
和IObserver<T>
的原因之一。Rx有通用的机制来解决这类问题。(当我们学习调度时,我们会看到这些机制。)此外,Rx提供的所有实现都为您利用了这些机制。
If you're using Rx by composing its built-in operators in a declarative way, you never have to think about these rules. You get to depend on these rules in your callbacks that receive the events, and it's mostly Rx's problem to keep to the rules. So the main effect of these rules is that it makes life simpler for code that consumes events.
如果你以声明性方式组合Rx的内置运算符来使用Rx,那么你永远不需要考虑这些规则。您可以在接收事件的回调中依赖这些规则,而保持这些规则主要是Rx的工作。因此,这些规则的主要效果是使消费事件的代码更加简单。
These rules are sometimes expressed as a grammar. For example, consider this regular expression:
这些规则有时表示为一种语法。例如,思考这个正则表达式:
(OnNext)*(OnError|OnComplete)
This formally captures the basic idea: there can be any number of calls to OnNext
(maybe even zero calls), that occur in sequence, followed by either an OnError
or an OnComplete
, but not both, and there must be nothing after either of these.
这正式体现了一个基本思想:可以有任意数量的OnNext
调用(甚至可能没有调用),这些调用是顺序发生的,之后是OnError
或OnComplete
中的一个,但两者不能同时出现,并且在这两者之后不能有任何内容。
One last point: sequences may be infinite. This is true for IEnumerable<T>
. It's perfectly possible for an enumerator to return true
every time MoveNext
is returned, in which case a foreach
loop iterating over it will never reach the end. It might choose to stop (with a break
or return
), or some exception that did not originate from the enumerator might cause the loop to terminate, but it's absolutely acceptable for an IEnumerable<T>
to produce items for as long as you keep asking for them. The same is true of a IObservable<T>
. If you subscribe to an observable source, and by the time your program exits you've not received a call to either OnComplete
or OnError
, that's not a bug.
最后一点:序列可能是无限的。这对于IEnumerable<T>
来说是成立的。枚举器每次返回MoveNext
时都返回true
是完全可能的,在这种情况下,遍历它的foreach
循环将永远不会到达末尾。它可能会选择停止(使用break
或return
),或者来自枚举器之外的一些异常可能会导致循环终止,但IEnumerable<T>
在你不断请求的情况下一直产生项目是完全可接受的。同样地,IObservable<T>
也是如此。如果你订阅了一个可观察源,并且在你的程序退出时你还没有收到OnComplete
或OnError
的调用,那并不是一个bug。
So you might argue that this is a slightly better way to describe the rules formally:
所以你可能认为这是一个稍微更好的方式来正式描述这些规则:
(OnNext)*(OnError|OnComplete)?
More subtly, observable sources are allowed to do nothing at all. In fact there's a built-in implementation to save developers from the effort of writing a source that does nothing: if you call Observable.Never<int>()
it will return an IObservable<int>
, and if you subscribe to that, it will never call any methods on your observer. This might not look immediately useful—it is logically equivalent to an IEnumerable<T>
in which the enumerator's MoveNext
method never returns, which might not be usefully distinguishable from crashing. It's slightly different with Rx, because when we model this "no items emerge ever" behaviour, we don't need to block a thread forever to do it. We can just decide never to call any methods on the observer. This may seem daft, but as you've seen with the Quiescent
example, sometimes we create observable sources not because we want the actual items that emerge from it, but because we're interested in the instants when interesting things happen. It can sometimes be useful to be able to model "nothing interesting ever happens" cases. For example, if you have written some code to detect unexpected inactivity (e.g., a sensor that stops producing values), and wanted to test that code, your test could use a Never
source instead of a real one, to simulate a broken sensor.
更微妙的是,可观察源被允许完全不执行任何操作。事实上,有一个内置的实现可以节省开发人员编写不执行任何操作的数据源的精力:如果你调用Observable.Never<int>()
,它会返回一个IObservable<int>
,如果你订阅它,它将永远不会调用你的观察者的任何方法。这看起来可能不会马上派上用场——它在逻辑上等价于一个IEnumerable<T>
,其中枚举器的MoveNext
方法永远不会返回,这可能无法与崩溃区分开来。但在Rx中略有不同,因为当我们模拟这种“永远不会有项目出现”的行为时,我们不需要永远阻塞一个线程来实现它。我们只需要决定永远不调用观察者的任何方法。这看起来可能有点傻,但正如你在Quiescent
示例中所看到的,有时我们创建可观察源并不是因为我们想要从中产生的实际项目,而是因为我们对有趣的事情发生的时间点感兴趣。有时能够模拟“永远不会有有趣的事情发生”的情况是有用的。例如,如果你编写了一些代码来检测意外的不活动状态(例如,传感器停止产生值),并且想要测试这段代码,你的测试可以使用Never
源而不是真实的源,来模拟一个损坏的传感器。
We're not quite done with the Rx's rules, but the last one applies only when we choose to unsubscribe from a source before it comes to a natural end.
我们还没有完全介绍完Rx的规则,但最后一个规则只在我们选择在一个源自然结束之前取消订阅时才适用。
Subscription Lifetime 订阅的生命周期
There's one more aspect of the relationship between observers and observables to understand: the lifetime of a subscription.
关于观察者和可观察对象之间的关系,还有一个方面需要理解:订阅的生命周期。
You already know from the rules of IObserver<T>
that a call to either OnComplete
or OnError
denotes the end of a sequence. We passed an IObserver<T>
to IObservable<T>.Subscribe
, and now the subscription is over. But what if we want to stop the subscription earlier?
你已经从IObserver<T>
的规则中了解到,对OnComplete
或OnError
的调用表示序列的结束。我们将一个IObserver<T>
传递给IObservable<T>.Subscribe
,也就完成了订阅。但是,如果我们想提前停止订阅呢?
I mentioned earlier that the Subscribe
method returns an IDisposable
, which enables us to cancel our subscription. Perhaps we only subscribed to a source because our application opened some window showing the status of some process, and we wanted to update the window to reflect that's process's progress. If the user closes that window, we no longer have any use for the notifications. And although we could just ignore all further notifications, that could be a problem if the thing we're monitoring never reaches a natural end. Our observer would continue to receive notifications for the lifetime of the application. This is a waste of CPU power (and thus power consumption, with corresponding implications for battery life and environmental impact) and it can also prevent the garbage collector from reclaiming memory that should have become free.
我之前提到Subscribe
方法返回一个IDisposable
,这使得我们能够取消订阅。也许我们只订阅了一个源,是因为我们的应用程序打开了一些窗口来显示一些过程的状态,并且我们想要更新窗口以反映该过程的进度。如果用户关闭了那个窗口,我们就不再需要这些通知了。虽然我们可以选择忽略所有的后续通知,但如果我们正在监控的东西永远不会自然结束,这可能会成为一个问题。我们的观察者将在应用程序的生命周期内继续接收通知。这会浪费CPU资源(从而浪费电能,对电池寿命和环境影响产生相应的影响),还可能阻止垃圾收集器回收应该释放的内存。
So we are free to indicate that we no longer wish to receive notifications by calling Dispose
on the object returned by Subscribe
. There are, however, a few non-obvious details.
因此,我们可以自由地通过调用Subscribe
方法返回的对象上的Dispose
来表明我们不再希望接收通知。但是,这里有一些不太明显的细节。
Disposal of Subscriptions is Optional 对订阅的释放是可选的
You are not required to call Dispose
on the object returned by Subscribe
. Obviously if you want to remain subscribed to events for the lifetime of your process, this makes sense: you never stop using the object, so of course you don't dispose it. But what might be less obvious is that if you subscribe to an IObservable<T>
that does come to an end, it automatically tidies up after itself.
你不需要对Subscribe
方法返回的对象调用Dispose
。显然,如果你希望在你的进程的生命周期内保持对事件的订阅,这是有意义的:你从未停止使用对象,所以你当然不需要释放它。但可能不那么明显的是,如果你订阅了一个终将结束的IObservable<T>
,它会在结束后自动清理自己。
IObservable<T>
implementations are not allowed to assume that you will definitely call Dispose
, so they are required to perform any necessary cleanup if they stop by calling the observer's OnCompleted
or OnError
. This is unusual. In most cases where a .NET API returns a brand new object created on your behalf that implements IDisposable
, it's an error not to dispose it. But IDisposable
objects representing Rx subscriptions are an exception to this rule. You only need to dispose them if you want them to stop earlier than they otherwise would.
IObservable<T>
的实现不允许假定你一定会调用Dispose
,因此,如果它们通过调用观察者的OnCompleted
或OnError
停止,它们需要执行任何必要的清理工作。这是不寻常的。在大多数情况下,如果一个.NET API为你创建并返回了一个实现了IDisposable
的新对象,不释放它就是一个错误。但是,表示Rx订阅的IDisposable
对象是这个规则的例外。你只有在希望它们比预期更早停止时,才需要释放它们。
Cancelling Subscriptions may be Slow or Even Ineffectual 取消订阅可能很慢甚至无效
Dispose
won't necessarily take effect instantly. Obviously it will take some non-zero amount of time in between your code calling into Dispose
, and the Dispose
implementation reaching the point where it actually does something. Less obviously, some observable sources may need to do non-trivial work to shut things down.
Dispose
不一定立即生效。显然,从你的代码调用Dispose
到Dispose
实现真正执行某些操作之间会花费一些非零的时间。不那么明显的是,一些可观察源可能需要执行一些重要的工作来关闭事物。
A source might create a thread to be able to monitor for and report whatever events it represents. (That would happen with the filesystem source shown above when running on Linux on .NET 8, because the FileSystemWatcher
class itself creates its own thread on Linux.) It might take a while for the thread to detect that it is supposed to shut down.
一个源可能会创建一个线程来监视和报告它(即源)所表示的任何事件。(在Linux上运行.NET 8时,文件系统源就会这样做,因为FileSystemWatcher
类本身在Linux上会创建自己的线程。)线程可能需要一段时间才能检测到它应该关闭。
It is fairly common practice for an IObservable<T>
to represent some underlying work. For example, Rx can take any factory method that returns a Task<T>
and wrap it as an IObservable<T>
. It will invoke the factory once for each call to Subscribe
, so if there are multiple subscribers to a single IObservable<T>
of this kind, each one effectively gets its own Task<T>
. This wrapper is able to supply the factory with a CancellationToken
, and if an observer unsubscribes by calling Dispose
before the task naturally runs to completion, it will put that CancellationToken
into a cancelled state. This might have the effect of bringing the task to a halt, but that will work only if the task happens to be monitoring the CancellationToken
. Even if it is, it might take some time to bring things to a complete halt. Crucially, the Dispose
call doesn't wait for that to happen. It will attempt to initiate cancellation but it may return before cancellation is complete.
对于IObservable<T>
来说,它通常用来表示一些底层工作,这是一种相当普遍的做法。例如,Reactive Extensions(Rx)可以接收任何返回Task<T>
的工厂方法,并将其包装为IObservable<T>
。对于每次对Subscribe
的调用,它都会调用一次该工厂方法,因此,如果这种类型的单个IObservable<T>
有多个订阅者,每个订阅者实际上都会获得自己独立的Task<T>
。这个包装器能够为工厂方法提供一个CancellationToken
,如果观察者在任务自然完成之前通过调用Dispose
取消订阅,它将会把这个CancellationToken
置于已取消状态。这可能会使任务停止,但这仅在任务恰好正在监控CancellationToken
时有效。即使如此,也可能需要一段时间才能使所有事物完全停止。至关重要的是,Dispose
调用并不会等待这种情况发生。它会尝试启动取消操作,但可能会在取消操作完成之前返回。
The Rules of Rx Sequences when Unsubscribing 取消订阅时 Rx 序列的规则
The fundamental rules of Rx sequences described earlier only considered sources that decided when (or whether) to come to a halt. What if a subscriber unsubscribes early? There is only one rule:
前面描述的 Rx 序列的基本规则只考虑了决定何时(或是否)停止源。如果订阅者提前取消订阅呢?这里只有一条规则:
Once the call to Dispose
has returned, the source will make no further calls to the relevant observer. If you call Dispose
on the object returned by Subscribe
, then once that call returns you can be certain that the observer you passed in will receive no further calls to any of its three methods (OnNext
, OnError
, or OnComplete
).
一旦Dispose
的调用返回,源将不会再对相关的观察者进行任何调用。如果你对Subscribe
方法返回的对象调用Dispose
,那么一旦这个调用返回,你就可以确信你传入的观察者将不会再收到其三个方法(OnNext
、OnError
或 OnComplete
)中的任何一个的调用。
That might seem clear enough, but it leaves a grey area: what happens when you've called Dispose
but it hasn't returned yet? The rules permit sources to continue to emit events in this case. In fact they couldn't very well require otherwise: it will invariably take some non-zero length of time for the Dispose
implementation to make enough progress to have any effect, so in a multi-threaded world it it's always going to be possible that an event gets delivered in between the call to Dispose
starting, and the call having any effect. The only situation in which you could depend on no further events emerging would be if your call to Dispose
happened inside the OnNext
handler. In this case the source will already have noted a call to OnNext
is in progress so further calls were already blocked before the call to Dispose
started.
这看起来可能已经足够清晰了,但它留下了一个灰色地带:当你已经调用了Dispose
但还没有返回时会发生什么?规则允许源在这种情况下继续发出事件。事实上,它们不能很好地要求其他方面:Dispose
的实现总是需要花费一些时间才能产生任何效果,所以在多线程环境中,在Dispose
调用起始和产生任何效果之间,总是有可能传递事件的。唯一一种你可以信赖没有更多事件产生的情况是,你的Dispose
调用发生在OnNext
处理器内部。在这种情况下,源已经注意到OnNext
的调用正在进行中,因此在Dispose
调用开始之前,进一步的调用已经被阻止。
But assuming that your observer wasn't already in the middle of an OnNext
call, any of the following would be legal:
但是,假设你的观察者尚未处于OnNext
调用的过程中,以下任何情况都是合法的:
- stopping calls to
IObserver<T>
almost immediately afterDispose
begins, even when it takes a relatively long time to bring any relevant underlying processes to a halt, in which case your observer will never receive anOnCompleted
orOnError
在Dispose
开始之后几乎立即停止对IObserver<T>
的调用,即使使相关底层过程停止需要相对较长的时间,在这种情况下,你的观察者将永远不会接收到OnCompleted
或OnError
的调用。 - producing notifications that reflect the process of shutting down (including calling
OnError
if an error occurs while trying to bring things to a neat halt, orOnCompleted
if it halted without problems) 产生反映关闭过程的通知(如果在尝试平稳停止的过程中发生错误,则调用OnError
;如果正常地停止,则调用OnCompleted
) - producing a few more notifications for some time after the call to
Dispose
begins, but cutting them off at some arbitrary point, potentially losing track even of important things like errors that occurred while trying to bring things to a halt 在Dispose
调用开始之后的一段时间内继续产生一些通知,但在某个任意点切断它们,甚至可能丢失在尝试停止时发生的重要事情(如错误)的跟踪
As it happens, Rx has a preference for the first option. If you're using an IObservable<T>
implemented by the System.Reactive
library (e.g., one returned by a LINQ operator) it is highly likely to have this characteristic. This is partly to avoid tricky situations in which observers try to do things to their sources inside their notification callbacks. Re-entrancy tends to be awkward to deal with, and Rx avoids ever having to deal with this particular form of re-entrancy by ensuring that it has already stopped delivering notifications to the observer before it begins the work of shutting down a subscription.
事实上,Rx 更倾向于第一个选项。如果你正在使用由 System.Reactive 库实现的 IObservable<T>
(例如,由 LINQ 运算符返回的一个),那么它很有可能具有这个特性。这部分是为了避免观察者在其通知回调中尝试对其源(即 IObservable<T>
)执行操作的棘手情况(免除了这种必要)。重入往往难以处理,Rx通过确保在订阅关闭的工作开始之前已经停止向观察者发送通知,从而避免了处理这种特定形式的重入。
This sometimes catches people out. If you need to be able to cancel some process that you are observing but you need to be able to observe everything it does up until the point that it stops, then you can't use unsubscription as the shutdown mechanism. As soon as you've called Dispose
, the IObservable<T>
that returned that IDisposable
is no longer under any obligation to tell you anything. This can be frustrating, because the IDisposable
returned by Subscribe
can sometimes seem like such a natural and easy way to shut something down. But basic truth is this: once you've initiated unsubscription, you can't rely on getting any further notifications associated with that subscription. You might receive some—the source is allowed to carry on supplying items until the call to Dispose
returns. But you can't rely on it—the source is also allowed to silence itself immediately, and that's what most Rx-implemented sources will do.
这有时会让人意外。如果你需要能够取消你正在观察的某个过程,但你需要能够观察到它在停止之前的所有行为,那么你不能使用取消订阅作为关闭机制。一旦你调用了Dispose
,返回该IDisposable
的IObservable<T>
将不再有任何义务向你通知任何事情。这可能会令人沮丧,因为Subscribe
返回的IDisposable
有时看起来是如此自然和容易的方式来关闭某个东西。但基本事实是:一旦你启动了取消订阅,你就不能确定你会收到与该订阅相关的任何更多的通知。你可能会收到一些——源被允许继续提供项目直到Dispose
调用返回。但你不能确定——源也允许自己立即停止通知,这是大多数Rx实现的源会做的。
One subtle consequence of this is that if an observable source reports an error after a subscriber has unsubscribed, that error might be lost. A source might call OnError
on its observer, but if that's a wrapper provided by Rx relating to a subscription that has already been disposed, it just ignores the exception. So it's best to think of early unsubscription as inherently messy, a bit like aborting a thread: it can be done but information can be lost, and there are race conditions that will disrupt normal exception handling.
一个微妙的后果是,如果订阅者已经取消订阅后,可观察源报告了一个错误,那么这个错误可能会丢失。一个源可能会对其观察者调用OnError
,但如果这个观察者是由Rx提供的与已经被释放的订阅相关的包装器,那么它就会忽略这个异常。因此,最好将提前取消订阅视为本质上很混乱的行为,有点像中止一个线程:虽然可以做到,但信息可能会丢失,并且存在竞态条件(会破坏正常的异常处理)。
In short, if you unsubscribe, then a source is not obliged to tell you when things stop, and in most cases it definitely won't tell you.
简而言之,如果你取消订阅,那么源没有义务告诉你何时停止,并且在大多数情况下,它肯定不会告诉你。
Subscription Lifetime and Composition 订阅的生命周期与组合
We typically combine multiple LINQ operators to express our processing requirements in Rx. What does this mean for subscription lifetime?
在Rx中,我们通常组合多个LINQ运算符来表达我们的处理需求。这对订阅生命周期意味着什么?
For example, consider this:
例如,考虑以下情况:
IObservable<int> source = GetSource(); IObservable<int> filtered = source.Where(i => i % 2 == 0); IDisposable subscription = filtered.Subscribe( i => Console.WriteLine(i), error => Console.WriteLine($"OnError: {error}"), () => Console.WriteLine("OnCompleted"));
We're calling Subscribe
on the observable returned by Where
. When we do that, it will in turn call Subscribe
on the IObservable<int>
returned by GetSource
(stored in the source
variable). So there is in effect a chain of subscriptions here. (We only have access to the IDisposable
returned by filtered.Subscribe
but the object that returns will be storing the IDisposable
that it received when it called source.Subscribe
.)
我们对Where
返回的可观察对象调用Subscribe
。当我们这样做时,它会转而调用GetSource
返回的IObservable<int>
(存储在source
变量中)的Subscribe
。因此,这里实际上存在一个订阅链。(我们只能访问filtered.Subscribe
返回的IDisposable
,但返回的对象将存储它在调用source.Subscribe
时接收到的IDisposable
。)
If the source comes to an end all by itself (by calling either OnCompleted
or OnError
), this cascades through the chain. So source
will call OnCompleted
on the IObserver<int>
that was supplied by the Where
operator. And that in turn will call OnCompleted
on the IObserver<int>
that was passed to filtered.Subscribe
, and that will have references to the three methods we passed, so it will call our completion handler. So you could look at this by saying that source
completes, it tells filtered
that it has completed, which invokes our completion handler. (In reality this is a very slight oversimplification, because source
doesn't tell filtered
anything; it's actually talking to the IObserver<T>
that filtered
supplied. This distinction matters if you have multiple subscriptions active simultaneously for the same chain of observables. But in this case, the simpler way of describing it is good enough even if it's not absolutely precise.)
如果源自身结束(通过调用OnCompleted
或OnError
),这会在链中传递。因此,源会调用由Where
运算符提供的IObserver<int>
的OnCompleted
。然后,这又会调用传递给filtered.Subscribe
的IObserver<int>
(通过委托定义)的OnCompleted
,并且该IObserver<int>
会引用我们传递的三个方法,所以它会调用我们的结束处理程序。你可以这样看: source
结束了,它告诉filtered
它已经结束了,这会调用我们的结束处理程序。(在现实中,这是一个稍微的简化,因为 source
不会告诉filtered
任何东西;它实际上是在与filtered.Subscribe
提供的IObserver<T>
进行通信。如果你对同一串可观察对象同时有多个订阅活动,这个区别就很重要。但在这种情况下,即使这种描述方式不是绝对精确,也够简单明了了。)
In short, completion bubbles up from the source, through all the operators, and arrives at our handler.
简而言之,结束通知从源开始,通过所有运算符,最后到达我们的处理程序。
What if we unsubscribe early by calling subscription.Dispose()
? In that case it all happens the other way round. The subscription
returned by filtered.Subscribe
is the first to know that we're unsubscribing, but it will then call Dispose
on the object that was returned when it called source.Subscribe
for us.
如果我们通过调用subscription.Dispose()
提前取消订阅,那会发生什么?在这种情况下,一切都会反过来。filtered.Subscribe
返回的 subscription
(订阅)是第一个知道我们正在取消订阅的,但随后它会为我们调用在调用source.Subscribe
时返回的对象上的Dispose
方法。
Either way, everything from the source to the observer, including any operators that were sitting in between, gets shut down.
无论如何,从源到观察者,包括它们之间的任何运算符,都会被关闭。
Now that we understand the relationship between an IObservable<T>
source and the IObserver<T>
interface that received event notifications, we can look at how we might create an IObservable<T>
instance to represent events of interest in our application.
既然我们理解了IObservable<T>
源和接收事件通知的IObserver<T>
接口之间的关系,我们就可以看看如何创建一个IObservable<T>
实例来显示我们应用程序中感兴趣的事件。
