烂翻译系列之Rx.NET介绍第二版——为什么使用Rx.NET
Rx is a .NET library for processing event streams. Why might you want that?
Rx 是一个用于处理事件流的.NET 库。为什么需要它?
Why Rx? 为什么选择Rx?
Users want timely information. If you're waiting for a parcel to arrive, live reports of the delivery van's progress give you more freedom than a suspect 2 hour delivery window. Financial applications depend on continuous streams of up-to-date data. We expect our phones and computers to provide us with all sorts of important notifications. And some applications simply can't work without live information. Online collaboration tools and multiplayer games absolutely depend on the rapid distribution and delivery of data.
用户需要及时的信息。如果你正在等待一个包裹到达,与让人疑惑的两小时内送达相比,送货车行进的实时报告给你更多的自主性。金融应用程序依赖于连续的最新数据流。我们希望我们的手机和电脑提供给我们各种各样的重要通知。而且有些应用程序在没有实时信息的情况下根本无法工作。在线协作工具和多人游戏绝对依赖于数据的快速分发和传递。
In short, our systems need to react when interesting things happen.
简而言之,当有趣的事情发生时,我们的系统需要做出反应。
Live information streams are a basic, ubiquitous element of computer systems. Despite this, they are often a second class citizen in programming languages. Most languages support sequences of data through something like an array, which presumes that the data is sitting in memory ready for our code to read at its leisure. If your application deals with events, arrays might work for historical data, but they aren't a good way to represent events that occur while the application is running. And although streamed data is a pretty venerable concept in computing, it tends to be clunky, with the abstractions often surfaced through APIs that are poorly integrated with our programming language's type system.
实时信息流是计算机系统中一个基本的、普遍存在的元素。尽管如此,在编程语言方面,他们通常是二等公民。大多数语言通过类似数组的东西来支持数据序列,数组假定数据位于内存中,等待我们的代码在空闲时读取。如果应用程序与事件打交道,数组可能适用于历史数据,但它们不是表示应用程序运行期间发生的事件的好方法。尽管流式数据在计算领域是一个相当古老的概念,但它往往显得笨拙,其抽象层通常通过API呈现,而这些API与我们的编程语言类型系统的集成度很低。
This is bad. Live data is critical to a wide range of applications. It should be as easy to work with as lists, dictionaries, and other collections.
这不好。实时数据对于众多应用至关重要。它应该像列表、字典和其他集合一样易于操作。
The Reactive Extensions for .NET (Rx.NET or Rx for short, available as the System.Reactive
NuGet package) elevate live data sources to first class citizens. Rx does not require any special programming language support. It exploits .NET's type system to represent streams of data in a way that .NET languages such as C#, F#, and VB.NET can all work with as naturally as they use collection types.
.NET的响应式扩展(简称Rx.NET或Rx,可作为 System.Reactive NuGet 软件包使用)向一等公民提供实时数据源。Rx 不需要任何特殊的编程语言支持。它利用.NET 的类型系统,来表示数据流,.NET 语言,如 C# 、 F# 和VB.NET,在某种程度上,可以像使用集合类型一样自然地使用它。
(A brief grammatical aside: although the phrase "Reactive Extensions" is plural, when we reduce it to just Rx.NET or Rx, we treat it as a singular noun. This is inconsistent, but saying "Rx are..." sounds plain weird.)
(一个简短的语法问题: 虽然短语“Reactive Extensions”是复数形式,但是当我们把它简化为 Rx.NET 或 Rx 时,我们把它当作一个单数名词。这是不一致的,但是说“ Rx are...”听起来很奇怪。)
For example, C# offers integrated query features that we might use to find all of the entries in a list that meet some criteria. If we have some List<Trade> trades
variable, we might write this:
例如,C # 提供了集成的查询特性,我们可以使用这些特性来查找满足某些条件的列表中的所有条目。如果我们有一个 List<Trade> trades
变量,我们可以这样写:
var bigTrades = from trade in trades where trade.Volume > 1_000_000;
With Rx, we could use this exact same code with live data. Instead of being a List<Trade>
, the trades
variable could be an IObservable<Trade>
. IObservable<T>
is the fundamental abstraction in Rx. It is essentially a live version of IEnumerable<T>
. In this case, bigTrades
would also be an IObservable<Trade>
, a live data source able to notify us of all trades whose Volume
exceeds one million. Crucially, it can report each such trade immediately—this is what we mean by a 'live' data source.
使用 Rx,我们可以对实时数据使用完全相同的代码。这个 trades
变量不是 List<Trade>
,而是 IObservable<Trade>
。 IObservable<T>
是 Rx 中的基础抽象,本质上是 IEnumerable<T>
的实时版本。在这种情况下,bigTrades 也将是一个 IObservable<Trade>
,一个能够通知我们所有交易量超过一百万的实时数据源。至关重要的是,它可以立即报告每笔此类交易——这就是我们所说的“实时”数据源。
Rx is a powerfully productive development tool. It enables developers to work with live event streams using language features familiar to all .NET developers. It enables a declarative approach that often allows us to express complex behaviour more elegantly and with less code than would be possible without Rx.
Rx 是一个强大的高效开发工具。它使开发人员能够使用所有.NET 开发人员都熟悉的语言特性来处理实时事件流。它支持一种声明性方法,这种方法通常使得我们可以使用比没有 Rx 时更优雅、更少的代码来表达复杂的行为。
Rx builds on LINQ (Language Integrated Query). This enables us to use the query syntax shown above (or you can use the explicit function call approach that some .NET developers prefer). LINQ is widely used in .NET both for data access (e.g., in Entity Framework Core), but also for working with in-memory collections (with LINQ to Objects), meaning that experienced .NET developers will tend to feel at home with Rx. Crucially, LINQ is a highly composable design: you can connect operators together in any combination you like, expressing potentially complex processing in a straightforward way. This composability arises from the mathematical foundations of its design, but although you can learn about this aspect of LINQ if you want, it's not a prerequisite: developers who aren't interested in the mathematics behind it can just enjoy the fact that LINQ providers such as Rx provide a set of building blocks that can be plugged together in endless different ways, and it all just works.
Rx 建立在 LINQ (语言集成查询)的基础上。这使我们能够使用上面所示的查询语法(或者您可以使用一些.NET 开发人员更喜欢的显式函数调用方法)。LINQ 广泛应用于.NET ,既用于数据访问(例如,在Entity Framework Core) ,也用于处理内存中的集合(使用 LINQ to Objects) ,这意味着经验丰富的.NET 开发人员在使用 Rx 时会感到宾至如归。至关重要的是,LINQ 是一种高度可组合的设计: 您可以将运算符以任何您喜欢的组合连接在一起,以一种直接的方式表达潜在的复杂处理。这种组合性源自其设计的数学基础,但尽管你可以(如果你愿意的话)了解LINQ的这一方面,但它并不是先决条件:对LINQ背后的数学不感兴趣的开发者可以只享受这样一个事实,即像Rx这样的LINQ提供程序提供了一套可以以无数种不同方式组合在一起的构建块,而且它们都能正常工作。
LINQ has proven track record of handling high very high volumes of data. Microsoft has used it extensively in the internal implementation of some of their systems, including services that support tens of millions of active users.
LINQ在处理极高数据量的方面有着良好的历史记录。微软在其一些系统的内部实现中广泛使用了LINQ,包括支持数千万活跃用户的服务。
When is Rx appropriate? 何时适用Rx?
Rx is designed for processing sequences of events, meaning that it suits some scenarios better than others. The next sections describe some of these scenarios, and also cases in which it is a less obvious match but still worth considering. Finally, we describe some cases in which it is possible to use Rx but where alternatives are likely to be better.
Rx旨在处理事件序列,这意味着它更适合某些场景而非其他场景。接下来的部分将描述其中的一些场景,以及Rx虽然不是最合适的选择但仍值得考虑的情况。最后,我们将描述一些可以使用Rx但其他替代方案可能更优的情况。
Good Fit with Rx 很适合Rx的场景
Rx is well suited to representing events that originate from outside of your code, and which your application needs to respond to, such as: Rx 非常适合表示来源于您代码之外并且应用程序需要响应的事件,例如:
- Integration events like a broadcast from a message bus, or a push event from WebSockets API, or a message received via MQTT or other low latency middleware like Azure Event Grid, Azure Event Hubs and Azure Service Bus, or a non-vendor specific representation such as cloudevents 集成事件,如来自消息总线的广播、来自WebSockets API的推送事件、通过MQTT或其他低延迟中间件(如Azure Event Grid、Azure Event Hubs和Azure Service Bus)接收的消息,或如cloudevents这样的非供应商特定表示
- Telemetry from monitoring devices such as a flow sensor in a water utility's infrastructure, or the monitoring and diagnostic features in a broadband provider's networking equipment 来自监测设备的遥测技术,如供水公司基础设施中的流量传感器,或宽带供应商网络设备中的监测和诊断功能
- Location data from mobile systems such as AIS messages from ships, or automotive telemetry 来自移动系统的位置数据,例如来自船舶的 AIS 消息,或者汽车遥测
- Operating system events such as filesystem activity, or WMI events 操作系统事件,如文件系统活动或 WMI 事件
- Road traffic information, such as notifications of accidents or changes in average speed 道路交通信息,例如意外通知或平均车速变动
- Integration with a Complex Event Processing (CEP) engine 与复杂事件处理(CEP)引擎的集成
- UI events such as mouse movement or button clicks UI 事件,如鼠标移动或按钮点击
Rx is also good way to model domain events. These may occur as a result of some of the events just described, but after processing them to produce events that more directly represent application concepts. These might include: Rx也是建模领域事件的一种好方法。这些领域事件可能是由前面描述的一些事件引发的,但在处理这些事件后,会产生更直接代表应用程序概念的事件。这些事件可能包括:
- Property or state changes on domain objects such as "Order Status Updated", or "Registration Accepted" 领域对象上的属性或状态的更改,如“订单状态更新”或“注册已接受”
- Changes to collections of domain objects, such as "New Registration Created" 对领域对象集合的更改,如“新注册已创建”
Events might also represent insights derived from incoming events (or historical data being analyzed at a later date) such as: 事件也可能代表从传入事件(或日后分析的历史数据)中得出的见解,例如:
- A broadband customer might have become an unwitting participant in a DDoS attack 宽带用户可能在不知情的情况下参与了 DDoS 攻击
- Two ocean-going vessels have engaged in a pattern of movement often associated with illegal activity (e.g., travelling closely alongside one another for an extended period, long enough to transfer cargo or people, while far out at sea) 两艘远洋船舶呈现出一种通常与非法活动相关的活动模式(例如,在远离海岸的广阔海域长时间近距离并排航行,时间足够长,可以进行货物或人员转运)
- CNC Milling Machine MFZH12's number 4 axis bearing is exhibiting signs of wear at a significantly higher rate than the nominal profile CNC铣床MFZH12的第4轴轴承的磨损速度明显快于标称磨损速度
- If the user wants to arrive on time at their meeting half way across town, the current traffic conditions suggest they should leave in the next 10 minutes 如果用户想准时到达位于城市另一端的会议地点,根据当前的交通状况,建议他们在接下来的10分钟内出发
These three sets of examples show how applications might progressively increase the value of the information as they process events. We start with raw events, which we then enhance to produce domain-specific events, and we then perform analysis to produce notifications that the application's users will really care about. Each stage of processing increases the value of the messages that emerge. Each stage will typically also reduce the volume of messages. If we presented the raw events in the first category directly to users, they might be overwhelmed by the volume of messages, making it impossible to spot the important events. But if we only present them with notifications when our processing has detected something important, this will enable them to work more efficiently and accurately, because we have dramatically improved the signal to noise ratio.
这三组示例展示了应用程序在处理事件时如何逐步提升信息的价值。我们从原始事件开始,然后对其进行增强以产生特定领域的事件,接着进行分析以生成应用程序用户真正关心的通知。每个处理阶段都会提升所生成消息的价值。每个阶段通常还会减少消息的数量。如果我们直接将第一类的原始事件呈现给用户,他们可能会被大量的消息淹没,从而无法发现重要事件。但是,如果我们的处理检测到重要内容时才向用户发送通知,这将使他们能够更高效、更准确地工作,因为我们显著提高了信噪比。
The System.Reactive
library provides tools for building exactly this kind of value-adding process, in which we tame high-volume raw event sources to produce high-value, live, actionable insights. It provides a suite of operators that enable our code to express this kind of processing declaratively, as you'll see in subsequent chapters.
System.Reactive库提供了构建这种增值过程的工具,在这个过程中,我们使高容量的原始事件源变得可控,从而生成高价值、实时、可操作的见解。它提供了一系列操作符,使我们的代码能够以一种声明性的方式表达这种处理过程,您将在后续章节中看到这一点。
Rx is also well suited for introducing and managing concurrency for the purpose of offloading. That is, performing a given set of work concurrently, so that the thread that detected an event doesn't also have to be the thread that handles that event. A very popular use of this is maintaining a responsive UI. (UI event handling has become such a popular use of Rx—both in .NET. but also in RxJS, which originated as an offshoot of Rx.NET—that it would be easy to think that this is what it's for. But its success there should not blind us to its wider applicability.)
Rx也非常适合为了卸载的目的而引入和管理并发。也就是说,并发执行给定的一组工作,这样检测到事件的线程就不必是处理该事件的线程。这种用途的一个非常流行的例子是保持用户界面的响应性。(UI 事件处理已经成为 Rx 的一种非常流行的用法。在.NET中如此,在 RxJS 中也是如此,RxJS是起源于 Rx.NET 的一个分支——很容易认为这就是它的用途。但它在那里取得的成功不应使我们对其更广泛的适用性视而不见。)
You should consider using Rx if you have an existing IEnumerable<T>
that is attempting to model live events. While IEnumerable<T>
can model data in motion (by using lazy evaluation like yield return
), there's a problem. If the code consuming the collection has reached the point where it wants the next item (e.g., because a foreach
loop has just completed an iteration) but no item is yet available, the IEnumerable<T>
implementation would have no choice but to block the calling thread in its MoveNext
until such time as data is available, which can cause scalability problems in some applications. Even in cases where thread blocking is acceptable (or if you use the newer IAsyncEnumerable<T>
, which can take advantage of C#'s await foreach
feature to avoid blocking a thread in these cases) IEnumerable<T>
and IAsyncEnumerable<T>
are misleading types for representing live information sources. These interfaces represent a 'pull' programming model: code asks for the next item in the sequence. Rx is a more natural choice for modelling information sources that naturally produce information on their own schedule.
如果你有一个现有的IEnumerable<T>
试图模拟实时事件,你应该考虑使用Rx。虽然IEnumerable<T>
可以通过使用惰性求值(如yield return
)来模拟动态数据,但存在一个问题。如果消费集合的代码已经到达需要下一个项目的点(例如,因为foreach
循环刚刚完成了一次迭代),但还没有项目可用,那么IEnumerable<T>
实现将别无选择,只能在MoveNext
中阻塞调用线程,直到数据可用为止,这可能会导致一些应用程序出现可扩展性问题。即使在可以接受线程阻塞的情况下(或者如果你使用更新的IAsyncEnumerable<T>
,它可以利用C#的await foreach
特性来避免在这些情况下阻塞线程),IEnumerable<T>
和IAsyncEnumerable<T>
作为表示实时信息源的类型也是具有误导性的。这些接口表示一种“拉取”编程模型:代码请求序列中的下一个项目。对于自然按照自身调度产生信息的信息源建模而言,Rx是一个更自然的选择。
Possible Fit with Rx 可能适用于Rx的情形
Rx can be used to represent asynchronous operations. .NET's Task
or Task<T>
effectively represent a single event, and IObservable<T>
can be thought if as a generalization of this to a sequence of events. (The relationship between, say, Task<int>
and IObservable<int>
is similar to the relationship between int
and IEnumerable<int>
.)
Rx 可被用来表示异步操作。NET 的 Task
或 Task<T>
有效地表示单个事件, 而IObservable<T>
可被认为一系列事件的泛化。(例如Task<int>
和 IObservable<int>
之间的关系类似于 int
和 IEnumerable<int>
之间的关系。)
This means that there are some scenarios that can be dealt with either using tasks and the async
keyword or through Rx. If at any point in your processing you need to deal with multiple values as well as single ones, Rx can do both; tasks don't handle multiple items so well. You can have a Task<IEnumerable<int>>
, which enables you to await
for a collection, and that's fine if all the items in the collection can be collected in a single step. The limitation with this is that once the task has produced its IEnumerable<int>
result, your await
has completed, and you're back to non-asynchronous iteration over that IEnumerable<int>
. If the data can't be fetched in a single step—perhaps the IEnumerable<int>
represents data from an API in which results are fetched in batches of 100 items at a time—its MoveNext
will have to block your thread every time it needs to wait.
这意味着有些场景可以使用Task和async
关键字来处理,也可以通过Rx来处理。如果在你的处理过程中,你需要处理多个值以及单个值,Rx可以同时处理这两者;而处理Task多个项目的效果就不太好了。你可以有一个Task<IEnumerable<int>>
,它允许你等待一个集合,如果集合中的所有项目都可以在一次操作中收集完,那这是可以的。但这种方法的限制是,一旦任务产生了IEnumerable<int>
结果,你的await
就完成了,然后你又要回到对这个IEnumerable<int>
的非异步迭代。如果数据不能一次获取完——也许IEnumerable<int>
表示的是来自API的数据,这些数据是每次以100个项目为一批获取的——那么它的MoveNext
每次需要等待时都会阻塞你的线程。
For the first 5 years of its existence, Rx was arguably the best way to represent collections that wouldn't necessarily have all the items available immediately. However, the introduction of IAsyncEnumerable<T>
in .NET Core 3.0 and C# 8 provided a way to handle sequences while remaining in the world of async
/await
(and the Microsoft.Bcl.AsyncInterfaces
NuGet package makes this available on .NET Framework and .NET Standard 2.0). So the choice to use Rx to now tends to boil down to whether a 'pull' model (exemplified by foreach
or await foreach
) or a 'push' model (in which code supplies callbacks to be invoked by the event source when items become available) is a better fit for the concepts being modelled.
在Rx出现的头五年里,它可以说是表示那些不一定会立即包含所有项目的集合的最佳方式。然而,.NET Core 3.0和C# 8中引入了IAsyncEnumerable<T>
,提供了一种在处理序列时仍然保持在async/await世界中的方法(而Microsoft.Bcl.AsyncInterfaces
NuGet包使这一功能在.NET Framework和.NET Standard 2.0上也可用)。因此,现在选择是否使用Rx往往归结为哪种模型更适合所建模的概念:“拉取”模型(以foreach
或await foreach
为例)还是“推送”模型(在这种模型中,代码提供回调,当项目可用时由事件源调用)。
Another related feature that was added .NET since Rx first appears is channels. These allow a source to produce object and a consumer to process them, so there's an obvious superficial similarity to Rx. However, a distinguishing feature of Rx is its support for composition with an extensive set of operators, something with no direct equivalent in channels. Channels on the other hand provide more options for adapting to variations in production and consumption rates.
自从 Rx 首次出现以来,.NET 增加的另一个相关特性是 channels。它们允许源产生对象,消费者处理它们,因此在表面上与 Rx 有明显的相似之处。然而,Rx 的一个显著特征是它支持使用一组大量的运算符进行组合,这些运算符在channels中没有直接等价物。另一方面,Channels 为适应生产率和消费率的变化提供了更多的选择。
Earlier, I mentioned offloading: using Rx to push work onto other threads. Although this technique can enable Rx to introduce and manage concurrency for the purposes of scaling or performing parallel computations, other dedicated frameworks like TPL (Task Parallel Library) Dataflow or PLINQ are more appropriate for performing parallel compute intensive work. However, TPL Dataflow offers some integration with Rx through its AsObserver
and AsObservable
extension methods. So it is common to use Rx to integrate TPL Dataflow with the rest of your application.
前面,我提到了卸载:使用Rx将工作推送到其他线程上。虽然这种技术可以使Rx为了扩展或执行并行计算而引入和管理并发,但其他专用框架,如TPL(任务并行库)Dataflow或PLINQ,更适合执行并行计算密集型工作。然而,TPL Dataflow通过其AsObserver
和AsObservable
扩展方法与Rx提供了一些集成。因此,使用Rx将TPL Dataflow与应用程序的其他部分集成起来是很常见的做法。
Poor Fit with Rx 不适合使用RX的场景
Rx's IObservable<T>
is not a replacement for IEnumerable<T>
or IAsyncEnumerable<T>
. It would be a mistake to take something that is naturally pull based and force it to be push based.
Rx的IObservable<T>
不是IEnumerable<T>
或IAsyncEnumerable<T>
的替代品。将原本基于拉取(pull-based)的东西强行改为基于推送(push-based)是错误的。
Also, there are some situations in which the simplicity of Rx's programming model can work against you. For example, some message queuing technologies such as MSMQ are by definition sequential, and thus might look like a good fit for Rx. However, they are often chosen for their transaction handling support. Rx does not have any direct way to surface transaction semantics, so in scenarios that require this you might be better off just working directly with the relevant technology's API. (That said, Reaqtor adds durability and persistence to Rx, so you might be able to use that to integrate these kinds of queueing systems with Rx.)
此外,在某些情况下,Rx编程模型的简单性可能会对你产生不利影响。例如,一些消息队列技术,如MSMQ,本质上是顺序的,因此看似非常适合Rx。然而,通常选择MSMQ 是因为它们的事务处理支持。Rx没有直接的方式来展现事务语义,因此,在需要这种事务语义的场景中,直接使用相关技术的API可能会更好。(话说回来,Reaqtor 为 Rx 增加了稳定性和持久性,因此您可以使用它来将这些类型的队列系统与 Rx 集成。)
By choosing the best tool for the job your code should be easier to maintain, it will likely provide better performance and you will probably get better support.
通过为任务选择最佳工具,你的代码将更容易维护,可能会提供更好的性能,并且你可能会获得更好的支持。
Rx in action Rx实践
You can get up and running with a simple Rx example very quickly. If you have the .NET SDK installed, you can run the following at a command line:
您可以非常快速地启动并运行一个简单的 Rx 示例。如果你已经安装了.NET SDK,可以在命令行上运行以下命令:
mkdir TryRx
cd TryRx
dotnet new console
dotnet add package System.Reactive
Alternatively, if you have Visual Studio installed, create a new .NET Console project, and then use the NuGet package manager to add a reference to System.Reactive
.
或者,如果安装了 Visual Studio,则创建一个新的.NET 控制台项目,然后使用 NuGet 包管理器添加对 System.Reactive
的引用。
This code creates an observable source (ticks
) that produces an event once every second. The code also passes a handler to that source that writes a message to the console for each event:
这段代码创建一个可观察的源(ticks),每秒钟产生一次事件。该代码还向该源传递一个处理程序,该处理程序为每个事件向控制台写入一条消息:
using System.Reactive.Linq; IObservable<long> ticks = Observable.Timer( dueTime: TimeSpan.Zero, period: TimeSpan.FromSeconds(1)); ticks.Subscribe( tick => Console.WriteLine($"Tick {tick}")); Console.ReadLine();
If this doesn't seem very exciting, it's because it's about as basic an example as it's possible to create, and at its heart, Rx has a very simple programming model. The power comes from composition—we can use the building blocks in the System.Reactive
library to describe the processing that will takes us from raw, low-level events to high-value insights. But to do that, we must first understand Rx's key types, IObservable<T>
and IObserver<T>
.
如果这看起来不是很令人兴奋,那是因为它是一个最基础的示例,而 Rx 的核心是一个非常简单的编程模型。能量来自于组合——我们可以使用 System.Reactive
中的构件,它描述的处理过程将把我们从原始的、低层次的事件带到高价值的洞察。
如果这看起来并不那么令人兴奋,那是因为它是一个最基础的示例,而 Rx 的核心是一个非常简单的编程模型。它的强大之处在于组合——我们可以使用 System.Reactive
库中的构建块,它描述的处理过程将把我们从原始的、低层次的事件带到高价值的见解。但要做到这一点,我们首先需要了解Rx的两个关键类型:IObservable<T>和IObserver<T>。
