烂翻译系列之Rx.NET介绍第二版——创建可观察对象序列
In the preceding chapter, we saw the two fundamental Rx interfaces, IObservable<T>
and IObserver<T>
. We also saw how to receive events by implementing IObserver<T>
, and also by using implementations supplied by the System.Reactive
package. In this chapter we'll see how to create IObservable<T>
sources to represent source events of interest in your application.
在前一章中,我们看到了两个基本的Rx接口: IObservable<T>
和 IObserver<T>
。我们还看到了如何通过实现 IObserver<T>
接口,以及使用 System.Reactive
包提供的实现来接收事件。在这一章中,我们将看到如何创建 IObservable<T>
源来表示你的应用程序中感兴趣的源事件。
We will begin by implementing IObservable<T>
directly. In practice, it's relatively unusual to do that, so we'll then look at the various ways you can get System.Reactive
to supply an implementation that does most of the work for you.
我们将首先直接实现 IObservable<T>
接口。实际上,直接这样做相对不常见,因此接下来我们将探讨你可以使用 System.Reactive
包提供的各种方式,让它为你完成大部分工作。
A Very Basic IObservable<T>
Implementation 一个非常基础的 IObservable<T>
实现
Here's an implementation of an IObservable<int>
that produces a sequence of numbers:
以下是一个 IObservable<int>
的实现,它生成一个数字序列:
public class MySequenceOfNumbers : IObservable<int> { public IDisposable Subscribe(IObserver<int> observer) { observer.OnNext(1); observer.OnNext(2); observer.OnNext(3); observer.OnCompleted(); return System.Reactive.Disposables.Disposable.Empty; // Handy do-nothing IDisposable } }
We can test this by constructing an instance of it, and then subscribing to it:
我们可以通过创建它的一个实例,然后订阅它来进行测试:
var numbers = new MySequenceOfNumbers(); numbers.Subscribe( number => Console.WriteLine($"Received value: {number}"), () => Console.WriteLine("Sequence terminated"));
This produces the following output:
这会生成以下输出:
Received value 1 Received value 2 Received value 3 Sequence terminated
Although MySequenceOfNumbers
is technically a correct implementation of IObservable<int>
, it is a little too simple to be useful. For one thing, we typically use Rx when there are events of interest, but this is not really reactive at all—it just produces a fixed set of numbers immediately. Moreover, the implementation is blocking—it doesn't even return from Subscribe
until after it has finished producing all of its values. This example illustrates the basics of how a source provides events to a subscriber, but if we just want to represent a predetermined sequence of numbers, we might as well use an IEnumerable<T>
implementation such as List<T>
or an array.
虽然从技术上讲 MySequenceOfNumbers
是 IObservable<int>
的一个正确实现,但它太简单了以至于不太实用。首先,我们通常在有感兴趣的事件时使用Rx,但这里的实现其实并不是真正的响应式——它只是立即生成了一个固定的数字集合。此外,这个实现是阻塞的——它在生成完所有值之后才从Subscribe
方法中返回。这个示例说明了源如何向订阅者提供事件的基本原理,但如果我们只是想表示一个预定义的数字序列,我们完全可以使用IEnumerable<T>
的实现,如List<T>
或数组。
Representing Filesystem Events in Rx 用 Rx 表示文件系统事件
Let's look at something a little more realistic. This is a wrapper around .NET's FileSystemWatcher
, presenting filesystem change notifications as an IObservable<FileSystemEventArgs>
. (Note: this is not necessarily the best design for an Rx FileSystemWatcher
wrapper. The watcher provides events for several different types of change, and one of them, Renamed
, provides details as a RenamedEventArgs
. This derives from FileSystemEventArgs
so collapsing everything down to a single event stream does work, but this would be inconvenient for applications that wanted access to the details of rename events. A more serious design problem is that this is incapable of reporting more than one event from FileSystemWatcher.Error
. Such errors might be transient and recoverable, in which case an application might want to continue operating, but since this class chooses to represent everything with a single IObservable<T>
, it reports errors by invoking the observer's OnError
, at which point the rules of Rx oblige us to stop. It would be possible to work around this with Rx's Retry
operator, which can automatically resubscribe after an error, but it might be better to offer a separate IObservable<ErrorEventArgs>
so that we can report errors in a non-terminating way. However, the additional complication of that won't always be warranted. The simplicity of this design means it will be a good fit for some applications. As is often the way with software design, there isn't a one-size-fits-all approach.)
让我们来看一个稍微更实际一点的例子。这是一个基于.NET的FileSystemWatcher
的包装器,它将文件系统变更通知呈现为一个IObservable<FileSystemEventArgs>
。(注意:这不一定是Rx FileSystemWatcher
包装器的最佳设计。监视器为几种不同类型的变更提供了事件,其中一个是Renamed
事件,它提供了RenamedEventArgs
类型的详细信息。由于RenamedEventArgs
继承自FileSystemEventArgs
,因此将所有事件合并到单个事件流中是可行的,但这对于需要访问 Renamed事件详细信息的应用程序来说可能不太方便。一个更严重的设计问题是,这个包装器无法报告来自FileSystemWatcher.Error
的多个事件。这些错误可能是短暂的且可恢复的,在这种情况下,应用程序可能希望继续运行,但由于这个类选择使用单个IObservable<T>
来表示所有内容,它通过在观察者上调用OnError
来报告错误,此时Rx的规则要求我们停止。虽然可以使用Rx的Retry
操作符在出现错误后自动重新订阅来绕过这个问题,但提供一个单独的IObservable<ErrorEventArgs>
来以非终止方式报告错误可能更好。然而,这种额外的复杂性并不总是必要的。这个设计的简单性意味着它适合一些应用程序。与软件设计通常的情况一样,并没有一种万能的解决方案。)
// Represents filesystem changes as an Rx observable sequence. // NOTE: this is an oversimplified example for illustration purposes. // It does not handle multiple subscribers efficiently, it does not // use IScheduler, and it stops immediately after the first error. public class RxFsEvents : IObservable<FileSystemEventArgs> { private readonly string folder; public RxFsEvents(string folder) { this.folder = folder; } public IDisposable Subscribe(IObserver<FileSystemEventArgs> observer) { // Inefficient if we get multiple subscribers. FileSystemWatcher watcher = new(this.folder); // FileSystemWatcher's documentation says nothing about which thread // it raises events on (unless you use its SynchronizationObject, // which integrates well with Windows Forms, but is inconvenient for // us to use here) nor does it promise to wait until we've // finished handling one event before it delivers the next. The Mac, // Windows, and Linux implementations are all significantly different, // so it would be unwise to rely on anything not guaranteed by the // documentation. (As it happens, the Win32 implementation on .NET 7 // does appear to wait until each event handler returns before // delivering the next event, so we probably would get way with // ignoring this issue. For now. On Windows. And actually the Linux // implementation dedicates a single thread to this job, but there's // a comment in the source code saying that this should probably // change - another reason to rely only on documented behaviour.) // So it's our problem to ensure we obey the rules of IObserver<T>. // First, we need to make sure that we only make one call at a time // into the observer. A more realistic example would use an Rx // IScheduler, but since we've not explained what those are yet, // we're just going to use lock with this object. object sync = new(); // More subtly, the FileSystemWatcher documentation doesn't make it // clear whether we might continue to get a few more change events // after it has reported an error. Since there are no promises about // threads, it's possible that race conditions exist that would lead to // us trying to handle an event from a FileSystemWatcher after it has // reported an error. So we need to remember if we've already called // OnError to make sure we don't break the IObserver<T> rules in that // case. bool onErrorAlreadyCalled = false; void SendToObserver(object _, FileSystemEventArgs e) { lock (sync) { if (!onErrorAlreadyCalled) { observer.OnNext(e); } } } watcher.Created += SendToObserver; watcher.Changed += SendToObserver; watcher.Renamed += SendToObserver; watcher.Deleted += SendToObserver; watcher.Error += (_, e) => { lock (sync) { // The FileSystemWatcher might report multiple errors, but // we're only allowed to report one to IObservable<T>. if (!onErrorAlreadyCalled) { observer.OnError(e.GetException()); onErrorAlreadyCalled = true; watcher.Dispose(); } } }; watcher.EnableRaisingEvents = true; return watcher; } }
That got more complex fast. This illustrates that IObservable<T>
implementations are responsible for obeying the IObserver<T>
rules. This is generally a good thing: it keeps the messy concerns around concurrency contained in a single place. Any IObserver<FileSystemEventArgs>
that I subscribe to this RxFsEvents
doesn't have to worry about concurrency, because it can count on the IObserver<T>
rules, which guarantee that it will only have to handle one thing at a time. If I hadn't been required to enforce these rules in the source, it might have made my RxFsEvents
class simpler, but all of that complexity of dealing with overlapping events would have spread out into the code that handles the events. Concurrency is hard enough to deal with when its effects are contained. Once it starts to spread across multiple types, it can become almost impossible to reason about. Rx's IObserver<T>
rules prevent this from happening.
这很快变得复杂起来。这说明了IObservable<T>
的实现需要遵守IObserver<T>
的规则。这通常是一件好事:它使得与并发相关的复杂问题被限制在一个地方。任何订阅了我这个RxFsEvents
的IObserver<FileSystemEventArgs>
都不必担心并发问题,因为它可以依赖IObserver<T>
的规则,这些规则保证了它一次只需要处理一件事。如果我没有在源代码中强制执行这些规则,我的RxFsEvents
类可能会更简单,但处理重叠事件的复杂性会扩散到处理事件的代码中。当并发的影响被限制在一定范围内时,处理并发问题就已经足够困难了。一旦它开始在多种类型之间扩散,几乎就不可能进行合理解释了。Rx的IObserver<T>
规则防止了这种情况的发生。
(Note: this is a significant feature of Rx. The rules keep things simple for observers. This becomes increasingly important as the complexity of your event sources or event process grows.)
(注意:这是Rx的一个重要特性。这些规则使观察者的工作变得简单。随着您的事件源或事件处理复杂性的增加,这一点变得越来越重要。)
There are a couple of issues with this code (aside from the API design issues already mentioned). One is that when IObservable<T>
implementations produce events modelling real-life asynchronous activity (such as filesystem changes) applications will often want some way to take control over which threads notifications arrive on. For example, UI frameworks tend to have thread affinity requirements. You typically need to be on a particular thread to be allowed to update the user interface. Rx provides mechanisms for redirecting notifications onto different schedulers, so we can work around it, but we would normally expect to be able to provide this sort of observer with an IScheduler
, and for it to deliver notifications through that. We'll discuss schedulers in later chapters.
这段代码有几个问题(除了已经提到的API设计问题之外)。其中之一是,当IObservable<T>
的实现产生模拟现实异步活动(如文件系统更改)的事件时,应用程序通常希望以某种方式控制通知到达的线程。例如,UI框架往往有线程亲和性要求。你通常需要在特定的线程上才能更新用户界面。Rx提供了将通知重定向到不同调度器的机制,因此我们可以绕过这个问题,但我们通常会期望能够为这类观察者提供一个IScheduler
,并通过它来发送通知。我们将在后面的章节中讨论调度器。
The other issue is that this does not deal with multiple subscribers efficiently. You're allowed to call IObservable<T>.Subscribe
multiple times, and if you do that with this code, it will create a new FileSystemWatcher
each time. That could happen more easily than you might think. Suppose we had an instance of this watcher, and wanted to handle different events in different ways. We might use the Where
operator to define observable sources that split events up in the way we want:
另一个问题是这段代码没有高效地处理多个订阅者。你可以多次调用IObservable<T>.Subscribe
,如果你用这段代码这样做,它每次都会创建一个新的FileSystemWatcher
。这可能会比你想象的更容易发生。假设我们有一个这个监视器的实例,并且想要以不同的方式处理不同的事件。我们可能会使用Where
运算符来定义可观察的来源,以便按照我们想要的方式拆分事件:
IObservable<FileSystemEventArgs> configChanges = fs.Where(e => Path.GetExtension(e.Name) == ".config"); IObservable<FileSystemEventArgs> deletions = fs.Where(e => e.ChangeType == WatcherChangeTypes.Deleted);
When you call Subscribe
on the IObservable<T>
returned by the Where
operator, it will call Subscribe
on its input. So in this case, if we call Subscribe
on both configChanges
and deletions
, that will result in two calls to Subscribe
on fs
. So if fs
is an instance of our RxFsEvents
type above, each one will construct its own FileSystemEventWatcher
, which is inefficient.
当你对由Where
运算符返回的IObservable<T>
调用Subscribe
时,它会在其输入上调用Subscribe
。因此,在这种情况下,如果我们同时对configChanges
和deletions
调用Subscribe
,这将会导致对fs
的Subscribe
进行两次调用。所以,如果fs
是上面提到的RxFsEvents
类型的实例,那么每个都会构造自己的FileSystemEventWatcher
,这是低效的。
Rx offers a few ways to deal with this. It provides operators designed specifically to take an IObservable<T>
that does not tolerate multiple subscribers and wrap it in an adapter that can:
Rx 提供了几种处理这种情况的方法。它提供了专门为不接受多个订阅者的 IObservable<T>
设计的运算符,并将它们包装在一个适配器中,该适配器可以:
IObservable<FileSystemEventArgs> fs = new RxFsEvents(@"c:\temp") .Publish() .RefCount();
But this is leaping ahead. (These operators are described in the Publishing Operators chapter.) If you want to build a type that is inherently multi-subscriber-friendly, all you really need to do is keep track of all your subscribers and notify each of them in a loop. Here's a modified version of the filesystem watcher:
但这是我们接下来要讨论的内容。(这些运算符在“发布运算符”章节中描述。)如果你想要构建一个本质上支持多订阅者的类型,你真正需要做的就是跟踪所有的订阅者,并在一个循环中通知他们每一个人。以下是文件系统监视器的一个修订版本:
public class RxFsEventsMultiSubscriber : IObservable<FileSystemEventArgs> { private readonly object sync = new(); private readonly List<Subscription> subscribers = new(); private readonly FileSystemWatcher watcher; public RxFsEventsMultiSubscriber(string folder) { this.watcher = new FileSystemWatcher(folder); watcher.Created += SendEventToObservers; watcher.Changed += SendEventToObservers; watcher.Renamed += SendEventToObservers; watcher.Deleted += SendEventToObservers; watcher.Error += SendErrorToObservers; } public IDisposable Subscribe(IObserver<FileSystemEventArgs> observer) { Subscription sub = new(this, observer); lock (this.sync) { this.subscribers.Add(sub); if (this.subscribers.Count == 1) { // We had no subscribers before, but now we've got one so we need // to start up the FileSystemWatcher. watcher.EnableRaisingEvents = true; } } return sub; } private void Unsubscribe(Subscription sub) { lock (this.sync) { this.subscribers.Remove(sub); if (this.subscribers.Count == 0) { watcher.EnableRaisingEvents = false; } } } void SendEventToObservers(object _, FileSystemEventArgs e) { lock (this.sync) { foreach (var subscription in this.subscribers) { subscription.Observer.OnNext(e); } } } void SendErrorToObservers(object _, ErrorEventArgs e) { Exception x = e.GetException(); lock (this.sync) { foreach (var subscription in this.subscribers) { subscription.Observer.OnError(x); } this.subscribers.Clear(); } } private class Subscription : IDisposable { private RxFsEventsMultiSubscriber? parent; public Subscription( RxFsEventsMultiSubscriber rxFsEventsMultiSubscriber, IObserver<FileSystemEventArgs> observer) { this.parent = rxFsEventsMultiSubscriber; this.Observer = observer; } public IObserver<FileSystemEventArgs> Observer { get; } public void Dispose() { this.parent?.Unsubscribe(this); this.parent = null; } } }
This creates only a single FileSystemWatcher
instance no matter how many times Subscribe
is called. Notice that I've had to introduce a nested class to provide the IDisposable
that Subscribe
returns. I didn't need that with the very first IObservable<T>
implementation in this chapter because it had already completed the sequence before returning, so it was able to return the Disposable.Empty
property conveniently supplied by Rx. (This is handy in cases where you're obliged to supply an IDisposable
, but you don't actually need to do anything when disposed.) And in my first FileSystemWatcher
wrapper, RxFsEvents
, I just returned the FileSystemWatcher
itself from Dispose
. (This works because FileSystemWatcher.Dispose
shuts down the watcher, and each subscriber was given its own FileSystemWatcher
.) But now that a single FileSystemWatcher
supports multiple observers, we need to do a little more work when an observer unsubscribes.
无论调用Subscribe
多少次,这都只会创建一个FileSystemWatcher
实例。请注意,我引入了一个嵌套类来提供Subscribe
返回的IDisposable
。在本章的第一个IObservable<T>
实现中,我并不需要这样做,因为它在返回之前已经完成了序列,所以它能够简单地返回Rx提供的Disposable.Empty
属性。(在必须提供IDisposable
但实际上在释放时不需要执行任何操作的情况下,这很有用。)在我之前的FileSystemWatcher
包装器RxFsEvents
中,我只是从Dispose
返回FileSystemWatcher
本身。(这可行是因为FileSystemWatcher.Dispose
会关闭监视器,而每个订阅者都有自己的FileSystemWatcher
。)但是现在,一个FileSystemWatcher
支持多个观察者,当观察者取消订阅时,我们需要做一些额外的工作。
When a Subscription
instance that we returned from Subscribe
gets disposed, it removes itself from the list of subscribers, ensuring that it won't receive any more notifications. It also sets the FileSystemWatcher
's EnableRaisingEvents
to false if there are no more subscribers, ensuring that this source does not do unnecessary work if nothing needs notifications right now.
当我们从Subscribe
返回的Subscription
实例被释放时,它会将自己从订阅者列表中移除,确保它不会再收到任何通知。如果没有更多的订阅者,它还会将FileSystemWatcher
的EnableRaisingEvents
设置为false
,确保这个源在目前没有需要通知的订阅者时不进行不必要的工作。
This is looking more realistic than the first example. This is truly a source of events that could occur at any moment (making this exactly the sort of thing well suited to Rx) and it now handles multiple subscribers intelligently. However, we wouldn't often write things this way. We're doing all the work ourselves here—this code doesn't even require a reference to the System.Reactive
package because the only Rx types it refers to are IObservable<T>
and IObserver<T>
, both of which are built into the .NET runtime libraries. In practice we typically defer to helpers in System.Reactive
because they can do a lot of work for us.
这比第一个示例看起来更实际。这确实是一个可能随时发生事件的源(这使得它完全适合Rx),并且现在它能够智能地处理多个订阅者。然而,我们通常不会以这种方式编写代码。在这里,我们自己在做所有的工作——这段代码甚至不需要引用System.Reactive
包,因为它只引用了Rx的两个类型:IObservable<T>
和IObserver<T>
,这两个类型都内置在.NET运行时库中。在实践中,我们通常会依赖于System.Reactive
中的辅助函数,因为它们可以为我们做很多工作。
For example, suppose we only cared about Changed
events. We could write just this:
例如,假设我们只关心“Changed”事件。我们可以这样写:
FileSystemWatcher watcher = new (@"c:\temp"); IObservable<FileSystemEventArgs> changes = Observable .FromEventPattern<FileSystemEventArgs>(watcher, nameof(watcher.Changed)) .Select(ep => ep.EventArgs); watcher.EnableRaisingEvents = true;
Here we're using the FromEventPattern
helper from the System.Reactive
library's Observable
class, which can be used to build an IObservable<T>
from any .NET event that conforms to the normal pattern (in which event handlers take two arguments: a sender of type object
, and then some EventArgs
-derived type containing information about the event). This is not as flexible as the earlier example. It reports only one of the events, and we have to manually start (and, if necessary stop) the FileSystemWatcher
. But for some applications that will be good enough, and this is a lot less code to write. If we were aiming to write a fully-featured wrapper for FileSystemWatcher
suitable for many different scenarios, it might be worth writing a specialized IObservable<T>
implementation as shown earlier. (We could easily extend this last example to watch all of the events. We'd just use the FromEventPattern
once for each event, and then use Observable.Merge
to combine the four resulting observables into one. The only real benefit we're getting from a full custom implementation is that we can automatically start and stop the FileSystemWatcher
depending on whether there are currently any observers.) But if we just need to represent some events as an IObservable<T>
so that we can work with them in our application, we can just use this simpler approach.
这里我们使用了System.Reactive
库中Observable
类的FromEventPattern
辅助函数,该函数可以从任何符合常规模式(事件处理程序接受两个参数:类型为object
的发送者,以及包含事件信息的某个从EventArgs
派生的类型)的.NET事件中构建IObservable<T>
。这没有前面的示例那么灵活。它只报告其中一种事件,我们必须手动启动(并在必要时停止)FileSystemWatcher
。但对于某些应用程序来说,这已经足够了,而且这样编写的代码要少得多。如果我们打算为FileSystemWatcher
编写一个功能齐全、适合多种场景的包装器,那么编写一个前面所示的专用IObservable<T>
实现可能是值得的。(我们可以很容易地将最后一个示例扩展到监视所有事件。我们只需为每个事件使用一次FromEventPattern
,然后使用Observable.Merge
将四个生成的可观察对象合并为一个。我们从完全的自定义实现中获得的唯一真正好处是,我们可以根据当前是否有观察者来自动启动和停止FileSystemWatcher
。)但是,如果我们只是需要将某些事件表示为IObservable<T>
以便在我们的应用程序中使用,那么我们可以直接使用这种更简单的方法。
In practice, we almost always get System.Reactive
to implement IObservable<T>
for us. Even if we want to take control of certain aspects (such as automatically starting up and shutting down the FileSystemWatcher
in these examples) we can almost always find a combination of operators that enable this. The following code uses various methods from System.Reactive
to return an IObservable<FileSystemEventArgs>
that has all the same functionality as the fully-featured hand-written RxFsEventsMultiSubscriber
above, but with considerably less code.
在实践中,我们几乎总是让System.Reactive
为我们实现IObservable<T>
。即使我们想控制某些方面(例如在这些示例中自动启动和关闭FileSystemWatcher
),我们也几乎总是可以找到一系列运算符的组合来实现这一点。以下代码使用了System.Reactive
中的各种方法,返回了一个IObservable<FileSystemEventArgs>
,它具有与上面功能齐全的手动编写的RxFsEventsMultiSubscriber
相同的所有功能,但代码量大大减少。
IObservable<FileSystemEventArgs> ObserveFileSystem(string folder) { return // Observable.Defer enables us to avoid doing any work // until we have a subscriber. Observable.Defer(() => { FileSystemWatcher fsw = new(folder); fsw.EnableRaisingEvents = true; return Observable.Return(fsw); }) // Once the preceding part emits the FileSystemWatcher // (which will happen when someone first subscribes), we // want to wrap all the events as IObservable<T>s, for which // we'll use a projection. To avoid ending up with an // IObservable<IObservable<FileSystemEventArgs>>, we use // SelectMany, which effectively flattens it by one level. .SelectMany(fsw => Observable.Merge(new[] { Observable.FromEventPattern<FileSystemEventHandler, FileSystemEventArgs>( h => fsw.Created += h, h => fsw.Created -= h), Observable.FromEventPattern<FileSystemEventHandler, FileSystemEventArgs>( h => fsw.Changed += h, h => fsw.Changed -= h), Observable.FromEventPattern<RenamedEventHandler, FileSystemEventArgs>( h => fsw.Renamed += h, h => fsw.Renamed -= h), Observable.FromEventPattern<FileSystemEventHandler, FileSystemEventArgs>( h => fsw.Deleted += h, h => fsw.Deleted -= h) }) // FromEventPattern supplies both the sender and the event // args. Extract just the latter. .Select(ep => ep.EventArgs) // The Finally here ensures the watcher gets shut down once // we have no subscribers. .Finally(() => fsw.Dispose())) // This combination of Publish and RefCount means that multiple // subscribers will get to share a single FileSystemWatcher, // but that it gets shut down if all subscribers unsubscribe. .Publish() .RefCount(); }
I've used a lot of methods there, most of which I've not talked about before. For that example to make any sense, I clearly need to start describing the numerous ways in which the System.Reactive
package can implement IObservable<T>
for you.
我在那里使用了很多方法,其中大多数我之前都没有提及过。为了让那个示例有意义,我显然需要开始描述System.Reactive
包可以以多种方式为你实现IObservable<T>
。
Simple factory methods 简单工厂方法
Due to the large number of methods available for creating observable sequences, we will break them down into categories. Our first category of methods create IObservable<T>
sequences that produce at most a single result.
由于创建可观察序列的方法数量众多,我们将它们分为几类。我们的第一类方法创建IObservable<T>
序列,这些序列最多产生单个结果。
Observable.Return 方法
One of the simplest factory methods is Observable.Return<T>(T value)
, which you've already seen in the Quiescent
example in the preceding chapter. This method takes a value of type T
and returns an IObservable<T>
which will produce this single value and then complete. In a sense, this wraps a value in an IObservable<T>
; it's conceptually similar to writing new T[] { value }
, in that it's a sequence containing just one element. You could also think of it as being the Rx equivalent of Task.FromResult
, which you can use when you have a value of some type T
, and need to pass it to something that wants a Task<T>
.
最简单的工厂方法之一是Observable.Return<T>(T value)
,你已经在前一章的Quiescent
示例中见过这个方法。这个方法接受一个类型为T
的值,并返回一个IObservable<T>
,该序列将产生这个单一值然后完成。从某种意义上说,这是将一个值包装在IObservable<T>
中;它在概念上与编写new T[] { value }
类似,因为这是一个仅包含一个元素的序列。你也可以将其视为Rx中与Task.FromResult
等效的方法,当你有一个类型为T
的值,并需要将其传递给期望一个Task<T>
的东西时使用。
IObservable<string> singleValue = Observable.Return<string>("Value");
I specified the type parameter for clarity, but this is not necessary as the compiler can infer the type from argument provided:
我指定了类型参数以明确说明,但这不是必需的,因为编译器可以从提供的参数中推断出类型:
IObservable<string> singleValue = Observable.Return("Value");
Return
produces a cold observable: each subscriber will receive the value immediately upon subscription. (Hot and cold observables were described in the preceding chapter.)
Return
生成的是一个冷可观察对象(cold observable):每个订阅者都会在订阅时立即接收到该值。(冷可观察对象和热可观察对象在前一章中已经描述过了。)
Observable.Empty 方法
Sometimes it can be useful to have an empty sequence. .NET's Enumerable.Empty<T>()
does this for IEnumerable<T>
, and Rx has a direct equivalent in the form of Observable.Empty<T>()
, which returns an empty IObservable<T>
. We need to provide the type argument because there's no value from which the compiler can infer the type.
有时拥有一个空序列会很有用。在.NET中,Enumerable.Empty<T>()
为 IEnumerable<T>
提供了这个功能,而Rx有一个直接等效的方法,即 Observable.Empty<T>()
,它返回一个空的 IObservable<T>
。我们需要提供类型参数,因为编译器无法从任何值中推断出类型。
IObservable<string> empty = Observable.Empty<string>();
In practice, an empty sequence is one that immediately calls OnCompleted
on any subscriber.
在实际应用中,一个空序列会立即对任何订阅者调用 OnCompleted
方法。
In comparison with IEnumerable<T>
, this is just the Rx equivalent of an empty list, but there's another way to look at it. Rx is a powerful way to model asynchronous processes, so you could think of this as being similar to a task that completes immediately without producing any result—so it has a conceptual resemblance to Task.CompletedTask
. (This is not as close an analogy as that between Observable.Return
and Task.FromResult
, because in that case we're comparing an IObservable<T>
with a Task<T>
, whereas here we're comparing an IObservable<T>
with a Task
—the only way for a task to complete without producing anything is if we use the non-generic version of Task
.)
与 IEnumerable<T>
相比,这就是 Rx 中空列表的等效物,但还有另一种看待它的方式。Rx 是模拟异步过程的有力工具,所以你可以将其视为类似于立即完成且不产生任何结果的任务——因此它与 Task.CompletedTask
在概念上相似。(这与 Observable.Return
和 Task.FromResult
之间的类比不太接近,因为在那里我们比较的是 IObservable<T>
与 Task<T>
,而在这里我们比较的是 IObservable<T>
与 Task
——任务不产生任何东西而完成的唯一方式是使用非泛型的 Task
版本。)
Observable.Never 方法
The Observable.Never<T>()
method returns a sequence which, like Empty
, does not produce any values, but unlike Empty
, it never ends. In practice, that means that it never invokes any method (neither OnNext
, OnCompleted
, nor OnError
) on subscribers. Whereas Observable.Empty<T>()
completes immediately, Observable.Never<T>
has infinite duration.
Observable.Never<T>()
方法返回一个序列,就像 Empty
一样,它不产生任何值,但与 Empty
不同,它永远不会结束。在实际应用中,这意味着它永远不会对订阅者调用任何方法(既不是 OnNext
,也不是 OnCompleted
,也不是 OnError
)。而 Observable.Empty<T>()
会立即完成,Observable.Never<T>
的持续时间是无限的。
IObservable<string> never = Observable.Never<string>();
It might not seem obvious why this could be useful. I gave one possible use in the last chapter: you could use this in a test to simulate a source that wasn't producing any values, perhaps to enable your test to validate timeout logic.
这个方法为什么会有用可能看起来并不明显。我在上一章中给出了一个可能的应用场景:你可以在测试中使用它来模拟一个不产生任何值的源,可能是为了让你的测试能够验证超时逻辑。
It can also be used in places where we use observables to represent time-based information. Sometimes we don't actually care what emerges from an observable; we might care only when something (anything) happens. (We saw an example of this "observable sequence used purely for timing purposes" concept in the preceding chapter, although Never
wouldn't make sense in that particular scenario. The Quiescent
example used the Buffer
operator, which works over two observable sequences: the first contains the items of interest, and the second is used purely to determine how to cut the first into chunks. Buffer
doesn't do anything with the values produced by the second observable: it pays attention only to when values emerge, completing the previous chunk each time the second observable produces a value. And if we're representing temporal information it can sometimes be useful to have a way to represent the idea that some event never occurs.)
它还可以在我们使用可观察对象来表示基于时间的信息的地方使用。有时我们并不真正关心从可观察对象中发出什么值;我们可能只关心何时发生某事(任何事)。(我们在前一章中看到了这种“仅用于定时目的的可观察序列”概念的示例,尽管在那种特定场景中,Never
并没有实际意义。Quiescent 示例使用了 Buffer 运算符,它在两个可观察序列上工作:第一个包含感兴趣的项,第二个仅用于确定如何将第一个分割成块。Buffer 不会对第二个可观察对象产生的值进行任何处理:它只关注值何时出现,每次第二个可观察对象产生一个值时,都会完成前一个块。如果我们表示时间信息,有时用一种方式来表示某些事件永远不会发生是有用的。)
As an example of where you might want to use Never
for timing purposes, suppose you were using some Rx-based library that offered a timeout mechanism, where an operation would be cancelled when some timeout occurs, and the timeout is itself modelled as an observable sequence. If for some reason you didn't want a timeout, and just want to wait indefinitely, you could specify a timeout of Observable.Never
.
作为你可能想要使用 Never
来表示时间的一个示例,假设你正在使用一些基于 Rx 的库,该库提供了超时机制,当发生超时时会取消某个操作,而超时本身被建模为一个可观察序列。如果出于某种原因,你不希望设置超时,而是想无限期地等待,你可以将超时设置为 Observable.Never
。
Observable.Throw 方法
Observable.Throw<T>(Exception)
returns a sequence that immediately reports an error to any subscriber. As with Empty
and Never
, we don't supply a value to this method (just an exception) so we need to provide a type parameter so that it knows what T
to use in the IObservable<T>
that it returns. (It will never actually a produce a T
, but you can't have an instance of IObservable<T>
without picking some particular type for T
.)
Observable.Throw<T>(Exception)
方法返回一个序列,该序列会立即向任何订阅者报告一个错误。与 Empty
和 Never
类似,我们不需要向这个方法提供一个值(只需要一个异常),因此我们需要提供一个类型参数,以便它知道在返回的 IObservable<T>
中使用什么类型的 T
。(它实际上永远不会产生一个 T
类型的值,但如果不为 T
选择一个特定的类型,就无法获得 IObservable<T>
的实例。)
IObservable<string> throws = Observable.Throw<string>(new Exception());
Observable.Create 方法
The Create
factory method is more powerful than the other creation methods because it can be used to create any kind of sequence. You could implement any of the preceding four methods with Observable.Create
. The method signature itself may seem more complex than necessary at first, but becomes quite natural once you are used to it.
Create
工厂方法比其他创建方法更强大,因为它可以用来创建任何类型的序列。你可以使用 Observable.Create
来实现前面提到的四种方法中的任何一种。这个方法签名本身可能看起来比必要的要复杂一些,但一旦你习惯了它,就会变得非常自然。
// Creates an observable sequence from a specified Subscribe method implementation. public static IObservable<TSource> Create<TSource>( Func<IObserver<TSource>, IDisposable> subscribe) {...} public static IObservable<TSource> Create<TSource>( Func<IObserver<TSource>, Action> subscribe) {...}
You provide this with a delegate that will be executed each time a subscription is made. Your delegate will be passed an IObserver<T>
. Logically speaking, this represents the observer passed to the Subscribe
method, although in practice Rx puts a wrapper around that for various reasons. You can call the OnNext
/OnError
/OnCompleted
methods as you need. This is one of the few scenarios where you will work directly with the IObserver<T>
interface. Here's a simple example that produces three items:
你提供一个委托,这个委托将在每次发生订阅时被执行。你的委托将被传递一个 IObserver<T>
。从逻辑上讲,这代表了传递给 Subscribe
方法的观察者,尽管实际上 Rx 出于各种原因会对其进行包装。你可以根据需要调用 OnNext/OnError/OnCompleted
方法。这是少数几个你将直接与 IObserver<T>
接口一起工作的场景之一。下面是一个简单的例子,它生成了三个项:
private IObservable<int> SomeNumbers() { return Observable.Create<int>( (IObserver<int> observer) => { observer.OnNext(1); observer.OnNext(2); observer.OnNext(3); observer.OnCompleted(); return Disposable.Empty; }); }
Your delegate must return either an IDisposable
or an Action
to enable unsubscription. When the subscriber disposes their subscription in order to unsubscribe, Rx will invoke Dispose()
on the IDisposable
you returned, or in the case where you returned an Action
, it will invoke that.
你的委托必须返回一个 IDisposable
或一个 Action
以实现退订。当订阅者为了退订而释放他们的订阅时,Rx 将调用你返回的 IDisposable
上的 Dispose()
方法,或者在你返回一个 Action
的情况下,Rx 将调用该 Action
。
This example is reminiscent of the MySequenceOfNumbers
example from the start of this chapter, in that it immediately produces a few fixed values. The main difference in this case is that Rx adds some wrappers that can handle awkward situations such as re-entrancy. Rx will sometimes automatically defer work to prevent deadlocks, so it's possible that code consuming the IObservable<int>
returned by this method will see a call to Subscribe
return before the callback in the code above runs, in which case it would be possible for them to unsubscribe inside their OnNext
handler.
这个示例让人回想起本章开头部分的 MySequenceOfNumbers
示例,因为它立即产生了几个固定值。然而,在这个例子中,主要区别在于 Rx 添加了一些可以处理诸如重入等棘手情况的包装器。Rx 有时会自动延迟工作以防止死锁,因此,有可能调用 IObservable<int>
(Create方法返回的)的代码会在上述代码中的回调运行之前看到 Subscribe
方法的返回,在这种情况下,它们有可能在 OnNext
处理程序中取消订阅。
The following sequence diagram shows how this could occur in practice. Suppose the IObservable<int>
returned by SomeNumbers
has been wrapped by Rx in a way that ensures that subscription occurs in some different execution context. We'd typically determine the context by using a suitable scheduler. (The SubscribeOn
operator creates such a wrapper.) We might use the TaskPoolScheduler
in order to ensure that the subscription occurs on some task pool thread. So when our application code calls Subscribe
, the wrapper IObservable<int>
doesn't immediately subscribe to the underlying observable. Instead it queues up a work item with the scheduler to do that, and then immediately returns without waiting for that work to run. This is how our subscriber can be in possession of an IDisposable
representing the subscription before Observable.Create
invokes our callback. The diagram shows the subscriber then making this available to the observer.
以下序列图展示了这种情况在实际中是如何发生的。假设 SomeNumbers
返回的 IObservable<int>
被 Rx 以某种方式封装,以确保订阅发生在某个不同的执行上下文中。我们通常通过使用合适的调度器来确定这个上下文。(SubscribeOn
运算符创建了这样的封装。)我们可能会使用 TaskPoolScheduler
来确保订阅发生在某个任务池线程上。因此,当我们的应用程序代码调用 Subscribe
时,封装的 IObservable<int>
(Rx IObservable Wrapper)并不会立即订阅底层可观察对象(由Observable.Create创建)。相反,它会将一项工作调度到调度器中以便稍后执行,并立即返回而不等待该工作运行。这就是我们的subscriber(订阅者)如何在 Observable.Create
调用我们的回调之前拥有一个代表订阅的 IDisposable
的原因。序列图展示了subscriber(订阅者)随后将这个 IDisposable
提供给观察者。
The diagram shows the scheduler call Subscribe
on the underlying observable after this, and that will mean the call back we passed to Observable.Create<int>
will now run. Our callback calls OnNext
, but it is not passed the real observer: instead it is passed another Rx-generated wrapper. That wrapper initially forwards calls directly onto the real observer, but our diagram shows that when the real observer (all the way over on the right) receives the its second call (OnNext(2)
) it unsubscribes by calling Dispose
on the IDisposable
that was returned when we subscribed to the Rx IObservable
wrapper. The two wrappers here—the IObservable
and IObserver
wrappers—are connected, so when we unsubscribe from the IObservable
wrapper, it tells the IObserver
wrapper that the subscription is being shut down. This means that when our Observable.Create<int>
callback calls OnNext(3)
on the IObserver
wrapper, that wrapper does not forward it to the real observer, because it knows that that observer has already unsubscribed. (It also doesn't forward the OnCompleted
, for the same reason.)
该序列图展示了调度器(scheduler)在此之后对底层可观察对象(observable)调用Subscribe
方法,这意味着我们传递给Observable.Create<int>
的回调现在将会执行。我们的回调调用了OnNext
,但它并没有将调用传递给真正的观察者(observer),而是传递给了另一个由Rx生成的包装器(Rx IObserver wrapper)。这个包装器最初会直接将调用转发给真正的观察者,但序列图显示,当真正的观察者(位于最右侧)接收到它的第二次调用(OnNext(2)
)时,它会通过调用我们在订阅Rx的IObservable wrapper时返回的IDisposable
的Dispose
方法来取消订阅。这里的两个包装器——IObservable wrapper(
包装器)和IObserver wrapper(
包装器)——是相互连接的,因此,当我们从IObservable wrapper取消订阅时,它会通知IObserver wrapper包装器订阅正在被关闭。这意味着,当我们的Observable.Create<int>
回调在IObserver wrapper上调用OnNext(3)
时,该 IObserver wrapper不会将其转发给真正的观察者,因为它知道该观察者已经取消了订阅。(出于同样的原因,它也不会转发OnCompleted
。)
You might be wondering how the IDisposable
we return to Observable.Create
can ever do anything useful. It's the return value of the callback, so we can only return it to Rx as the last thing our callback does. Won't we always have finished our work by the time we return, meaning there's nothing to cancel? Not necessarily—we might kick off some work that continues to run after we return. This next example does that, meaning that the unsubscription action it returns is able to do something useful: it sets a cancellation token that is being observed by the loop that generates our observable's output. (This returns a callback instead of an IDisposable
—Observable.Create
offers overloads that let you do either. In this case, Rx will invoke our callback when the subscription is terminated early.)
您可能会好奇,我们返回给 Observable.Create
的 IDisposable
究竟如何发挥实际作用。因为它是回调函数的返回值,所以我们只能在回调函数的最后一步将其返回给 Rx(响应式扩展)。在我们返回 IDisposable
时,我们的工作不是已经完成了吗?这意味着没有什么可以取消的了吗?未必如此——我们可能会启动一些在我们返回后继续运行的工作。接下来的例子就展示了这一点,这意味着它返回的取消订阅操作能够执行一些有用的操作:它设置了一个取消令牌,该令牌正被生成我们可观察对象输出的循环所监视。(这里返回的是一个回调函数而不是 IDisposable
——Observable.Create
提供了重载,允许你选择其中一种方式。在这种情况下,当订阅提前终止时,Rx 会调用我们的回调函数。)
IObservable<char> KeyPresses() => Observable.Create<char>(observer => { CancellationTokenSource cts = new(); Task.Run(() => { while (!cts.IsCancellationRequested) { ConsoleKeyInfo ki = Console.ReadKey(); observer.OnNext(ki.KeyChar); } }); return () => cts.Cancel(); });
This illustrates how cancellation won't necessarily take effect immediately. The Console.ReadKey
API does not offer an overload accepting a CancellationToken
, so this observable won't be able to detect that cancellation is requested until the user next presses a key, causing ReadKey
to return.
这说明了取消操作不一定会立即生效。Console.ReadKey
API 并没有提供接受 CancellationToken
的重载,因此这个可观察对象在用户下次按下键,导致 ReadKey
返回之前,无法检测到取消请求。
Bearing in mind that cancellation might have been requested while we were waiting for ReadKey
to return, you might think we should check for that after ReadKey
returns and before calling OnNext
. In fact it doesn't matter if we don't. Rx has a rule that says an observable source must not call into an observer after a call to Dispose
on that observer's subscription returns. To enforce that rule, if the callback you pass to Observable.Create
continues to call methods on its IObserver<T>
after a request to unsubscribe, Rx just ignores the call. This is one reason why the IObserver<T>
it passes to you is a wrapper: it can intercept the calls before they are passed to the underlying observer. However, that convenience means there are two important things to be aware of
考虑到在等待 ReadKey
返回时可能已经请求了取消操作,您可能会认为我们应该在 ReadKey
返回后、调用 OnNext
之前检查这一点。事实上,如果我们不这样做也没关系。Rx 有一条规则,即一个可观察源在调用该观察者的订阅的 Dispose
方法返回后,不得再调用该观察者。为了强制执行这一规则,如果您传递给 Observable.Create
的回调在请求取消订阅后继续调用其 IObserver<T>
的方法,Rx 将忽略该调用。这是它传递给您的 IObserver<T>
是一个包装器的原因之一:它可以在调用传递给底层观察者之前拦截这些调用。然而,这种便利性意味着有两件重要的事情需要注意:
- if you do ignore attempts to unsubscribe and continue to do work to produce items, you are just wasting time because nothing will receive those items 如果你忽略退订请求并继续工作以产生项目,那么你只是在浪费时间,因为这些项目不会被接收。
- if you call
OnError
it's possible that nothing is listening and that the error will be completely ignored. 如果你调用OnError
,可能会没有监听者,那么错误将被完全忽略。
There are overloads of Create
designed to support async
methods. This next method exploits this to be able to use the asynchronous ReadLineAsync
method to present lines of text from a file as an observable source.
Create
方法有重载版本,旨在支持异步方法。下面的这个方法就利用了这一点,以便能够使用异步的 ReadLineAsync
方法将文件中的行作为可观察源呈现出来。
IObservable<string> ReadFileLines(string path) => Observable.Create<string>(async (observer, cancellationToken) => { using (StreamReader reader = File.OpenText(path)) { while (cancellationToken.IsCancellationRequested) { string? line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false); if (line is null) { break; } observer.OnNext(line); } observer.OnCompleted(); } });
Reading data from a storage device typically doesn't happen instantaneously (unless it happens to be in the filesystem cache already), so this source will provide data as quickly as it can be read from storage.
从存储设备读取数据通常不会立即发生(除非它碰巧已经在文件系统缓存中),所以这个数据源将以尽可能快的速度从存储设备中提供数据。
Notice that because this is an async
method, it will typically return to its caller before it completes. (The first await
that actually has to wait returns, and the remainder of the method runs via a callback when the work completes.) That means that subscribers will typically be in possession of the IDisposable
representing their subscription before this method finishes, so we're using a different mechanism to handle unsubscription here. This particular overload of Create
passes its callback not just an IObserver<T>
but also a CancellationToken
, with which it will request cancellation when unsubscription occurs.
请注意,由于这是一个异步方法,它通常会在完成之前返回给调用者。(第一个实际需要等待的 await
返回后,方法的其余部分会在工作完成时通过回调运行。)这意味着订阅者通常会在此方法完成之前就拥有代表其订阅的 IDisposable
,因此我们在这里使用了一种不同的机制来处理取消订阅。Create
的这个特定重载不仅向其回调传递了一个 IObserver<T>
,还传递了一个 CancellationToken
,当发生取消订阅时,它将使用该令牌请求取消。
File IO can encounter errors. The file we're looking for might not exist, or we might be unable to open it due to security restrictions, or because some other application is using it. The file might be on a remote storage server, and we could lose network connectivity. For this reason, we must expect exceptions from such code. This example has done nothing to detect exceptions, and yet the IObservable<string>
that this ReadFileLines
method returns will in fact report any exceptions that occur. This is because the Create
method will catch any exception that emerges from our callback and report it with OnError
. (If our code already called OnComplete
on the observer, Rx won't call OnError
because that would violate the rules. Instead it will silently drop the exception, so it's best not to attempt to do any work after you call OnCompleted
.)
文件 I/O 可能会遇到错误。我们想要查找的文件可能不存在,或者我们可能因为安全限制无法打开它,或者因为其他应用程序正在使用它。文件可能位于远程存储服务器上,我们可能会失去网络连接。因此,我们必须预期此类代码会产生异常。这个示例并没有做任何检测异常的事情,但是 ReadFileLines
方法返回的 IObservable<string>
实际上会报告发生的任何异常。这是因为 Create
方法会捕获从我们的回调函数中抛出的任何异常,并通过 OnError
报告它。(如果我们的代码已经对观察者调用了 OnCompleted
,Rx 不会调用 OnError
,因为这将违反规则。相反,它会静默地丢弃异常,所以最好不要在调用 OnCompleted
后尝试进行任何工作。)
This automatic exception delivery is another example of why the Create
factory method is the preferred way to implement custom observable sequences. It is almost always a better option than creating custom types that implement the IObservable<T>
interface. This is not just because it saves you some time. It's also that Rx tackles the intricacies that you may not think of such as thread safety of notifications and disposal of subscriptions.
这种自动异常传递是 Create
工厂方法成为实现自定义可观察序列的首选方式的另一个原因。它几乎总是比创建实现 IObservable<T>
接口的自定义类型更好的选择。这不仅是因为它节省了你一些时间。还因为 Rx 解决了你可能没有考虑到的复杂问题,例如通知的线程安全性和订阅的处置。
The Create
method entails lazy evaluation, which is a very important part of Rx. It opens doors to other powerful features such as scheduling and combination of sequences that we will see later. The delegate will only be invoked when a subscription is made. So in the ReadFileLines
example, it won't attempt to open the file until you subscribe to the IObservable<string>
that is returned. If you subscribe multiple times, it will execute the callback each time. (So if the file has changed, you can retrieve the latest contents by calling Subscribe
again.)
Create
方法涉及到惰性求值,这是 Rx 非常重要的一个部分。它为其他强大的功能(如调度和序列组合)打开了大门,我们将在后面看到这些功能。只有当发生订阅时,委托才会被调用。所以在 ReadFileLines
示例中,它不会尝试打开文件,直到你订阅返回的 IObservable<string>
。如果你多次订阅,它每次都会执行回调。(因此,如果文件已经更改,你可以通过再次调用 Subscribe
来检索最新内容。)
As an exercise, try to build the Empty
, Return
, Never
& Throw
extension methods yourself using the Create
method. If you have Visual Studio or LINQPad available to you right now, code it up as quickly as you can, or if you have Visual Studio Code, you could create a new Polyglot Notebook. (Polyglot Notebooks make Rx available automatically, so you can just write a C# cell with a suitable using
directive, and you're up and running.) If you don't (perhaps you are on the train on the way to work), try to conceptualize how you would solve this problem.
作为练习,尝试使用 Create
方法自己构建 Empty
、Return
、Never
和 Throw
扩展方法。如果你现在有 Visual Studio 或 LINQPad 可用,尽快编写代码,或者如果你有 Visual Studio Code,你可以创建一个新的 Polyglot 笔记本。(Polyglot 笔记本会自动提供 Rx,所以你可以只编写一个带有适当 using
指令的 C# 单元格,然后你就可以开始运行了。)如果你没有这些工具(也许你在上班的路上),试着构思你将如何解决这个问题。
You completed that last step before moving onto this paragraph, right? Because you can now compare your versions with these examples of Empty
, Return
, Never
and Throw
recreated with Observable.Create
:
你在继续阅读本段之前已经完成了上一步,对吧?因为你现在可以将你的版本与这些使用 Observable.Create
重新创建的 Empty
、Return
、Never
和 Throw
示例进行比较了:
public static IObservable<T> Empty<T>() { return Observable.Create<T>(o => { o.OnCompleted(); return Disposable.Empty; }); } public static IObservable<T> Return<T>(T value) { return Observable.Create<T>(o => { o.OnNext(value); o.OnCompleted(); return Disposable.Empty; }); } public static IObservable<T> Never<T>() { return Observable.Create<T>(o => { return Disposable.Empty; }); } public static IObservable<T> Throws<T>(Exception exception) { return Observable.Create<T>(o => { o.OnError(exception); return Disposable.Empty; }); }
You can see that Observable.Create
provides the power to build our own factory methods if we wish.
你可以看到,Observable.Create
提供了如果我们愿意,就可以构建我们自己的工厂方法的能力。
Observable.Defer 方法
One very useful aspect of Observable.Create
is that it provides a place to put code that should run only when subscription occurs. Often, libraries will make IObservable<T>
properties available that won't necessarily be used by all applications, so it can be useful to defer the work involved until you know you will really need it. This deferred initialization is inherent to how Observable.Create
works, but what if the nature of our source means that Observable.Create
is not a good fit? How can we perform deferred initialization in that case? Rx providers Observable.Defer
for this purpose.
Observable.Create
的一个非常有用的方面是,它提供了一个位置来放置仅在订阅发生时才应运行的代码。通常,库会提供 IObservable<T>
属性,但这些属性并不一定会被所有应用程序使用,因此,将相关的工作推迟到确实需要它的时候再进行会很有用。这种延迟初始化是 Observable.Create
工作原理的固有特性,但如果我们的数据源的性质意味着 Observable.Create
不太适合,那该怎么办?在那种情况下,我们如何进行延迟初始化?为此,Rx提供了 Observable.Defer
。
I've already used Defer
once. The ObserveFileSystem
method returned an IObservable<FileSystemEventArgs>
reporting changes in a folder. It was not a good candidate for Observable.Create
because it provided all the notifications we wanted as .NET events, so it made sense to use Rx's event adaptation features. But we still wanted to defer the creation of the FileSystemWatcher
until the moment of subscription, which is why that example used Observable.Defer
.
我已经使用过一次 Defer
。ObserveFileSystem
方法返回了一个 IObservable<FileSystemEventArgs>
,用于报告文件夹中的更改。由于它提供了我们想要的所有通知(以.NET 事件的方式),因此使用 Rx 的事件适配功能是有意义的。但我们仍然希望在订阅时刻才创建 FileSystemWatcher
,这就是为什么那个例子使用了 Observable.Defer
。
Observable.Defer
takes a callback that returns an IObservable<T>
, and Defer
wraps this with an IObservable<T>
that invokes that callback upon subscription. To show the effect, I'm first going to show an example that does not use Defer
:
Observable.Defer
接受一个返回 IObservable<T>
的回调函数,并用一个 IObservable<T>
包装它,这个 IObservable<T>
会在订阅时调用那个回调函数。为了展示这个效果,我首先会展示一个不使用 Defer
的例子:
static IObservable<int> WithoutDeferal() { Console.WriteLine("Doing some startup work..."); return Observable.Range(1, 3); } Console.WriteLine("Calling factory method"); IObservable<int> s = WithoutDeferal(); Console.WriteLine("First subscription"); s.Subscribe(Console.WriteLine); Console.WriteLine("Second subscription"); s.Subscribe(Console.WriteLine);
This produces the following output:
这会产生以下输出:
Calling factory method Doing some startup work... First subscription 1 2 3 Second subscription 1 2 3
As you can see, the "Doing some startup work...
message appears when we call the factory method, and before we've subscribed. So if nothing ever subscribed to the IObservable<int>
that method returns, the work would be done anyway, wasting time and energy. Here's the Defer
version:
如你所见,当我们调用工厂方法时,“Doing some startup work...”消息就出现了,而这时我们还没有进行订阅。因此,如果从来没有任何东西订阅那个方法返回的 IObservable<int>
,这项工作仍然会被执行,浪费了时间和资源。下面是使用 Defer
的版本:
static IObservable<int> WithDeferal() { return Observable.Defer(() => { Console.WriteLine("Doing some startup work..."); return Observable.Range(1, 3); }); }
If we were to use this with similar code to the first example, we'd see this output:
如果我们用类似于第一个例子的代码来使用这个,我们会看到这样的输出:
Calling factory method First subscription Doing some startup work... 1 2 3 Second subscription Doing some startup work... 1 2 3
There are two important differences. First, the "Doing some startup work..."
message does not appear until we first subscribe, illustrating that Defer
has done what we wanted. However, notice that the message now appears twice: it will do this work each time we subscribe. If you want this deferred initialization but you'd also like once-only execution, you should look at the operators in the Publishing Operators chapter, which provide various ways to enable multiple subscribers to share a single subscription to an underlying source.
有两个重要的不同点。首先,“Doing some startup work...”消息直到我们首次订阅时才出现,这表明 Defer
已经实现了我们想要的效果。但是,请注意,现在消息出现了两次:每次我们订阅时它都会执行这项工作。如果你想要这种延迟初始化,但又希望只执行一次,你应该查看“发布运算符”章节中的运算符,它们提供了多种方法使多个订阅者共享对底层源的单个订阅。
Sequence Generators 序列生成器
The creation methods we've looked at so far are straightforward in that they either produce very simple sequences (such as single-element, or empty sequences), or they rely on our code to tell them exactly what to produce. Now we'll look at some methods that can produce longer sequences.
到目前为止,我们查看的创建方法都很直接,它们要么产生非常简单的序列(如单个元素或空序列),要么依赖于我们的代码来明确告诉它们要产生什么。现在,我们将查看一些可以产生更长序列的方法。
Observable.Range 方法
Observable.Range(int, int)
returns an IObservable<int>
that produces a range of integers. The first integer is the initial value and the second is the number of values to yield. This example will write the values '10' through to '24' and then complete.
Observable.Range(int, int)
返回一个 IObservable<int>
,该对象产生一系列整数。第一个整数是初始值,第二个整数是要产生的值的数量。这个示例将输出值 '10' 到 '24',然后完成。
IObservable<int> range = Observable.Range(10, 15); range.Subscribe(Console.WriteLine, () => Console.WriteLine("Completed"));
Observable.Generate 方法
Suppose you wanted to emulate the Range
factory method using Observable.Create
. You might try this:
假设你想要使用 Observable.Create
来模拟 Range
工厂方法。你可能会尝试这样做:
// Not the best way to do it! IObservable<int> Range(int start, int count) => Observable.Create<int>(observer => { for (int i = 0; i < count; ++i) { observer.OnNext(start + i); } return Disposable.Empty; });
This will work, but it does not respect request to unsubscribe. That won't cause direct harm, because Rx detects unsubscription, and will simply ignore any further values we produce. However, it's a waste of CPU time (and therefore energy, with consequent battery lifetime and/or environmental impact) to carry on generating numbers after nobody is listening. How bad that is depends on how long a range was requested. But imagine you wanted an infinite sequence? Perhaps it's useful to you to have an IObservable<BigInteger>
that produces value from the Fibonacci sequence, or prime numbers. How would you write that with Create
? You'd certainly want some means of handling unsubscription in that case. We need our callback to return if we are to be notified of unsubscription (or we could supply an async
method, but that doesn't really seem suitable here).
这样做可以工作,但它不尊重取消订阅的请求。这不会造成直接伤害,因为 Rx 会检测到取消订阅,并简单地忽略我们产生的任何进一步的值。然而,在没有人监听的情况下继续生成数字是浪费 CPU 时间(因此也是浪费能量,从而影响电池寿命和/或环境影响)。这有多糟糕取决于请求的范围有多长。但想象一下,如果你想要一个无限序列呢?也许拥有一个产生 Fibonacci 序列或质数值的 IObservable<BigInteger>
对你来说很有用。在这种情况下,你将如何使用 Create
来编写它?你当然希望在这种情况下有一些处理取消订阅的方法。我们需要我们的回调函数在取消订阅时返回(或者我们可以提供一个异步方法,但在这里似乎并不合适)。
There's a different approach that can work better here: Observable.Generate
. The simple version of Observable.Generate
takes the following parameters:
这里有一种不同的方法可能效果更好:Observable.Generate
。Observable.Generate
的简单版本接受以下参数:
- an initial state 一个初始状态
- a predicate that defines when the sequence should terminate 一个定义序列何时终止的断言(条件)
- a function to apply to the current state to produce the next state 一个应用于当前状态以产生下一个状态的函数
- a function to transform the state to the desired output 一个将状态转换为所需输出的函数
public static IObservable<TResult> Generate<TState, TResult>( TState initialState, Func<TState, bool> condition, Func<TState, TState> iterate, Func<TState, TResult> resultSelector)
This shows how you could use Observable.Generate
to construct a Range
method:
这展示了如何使用 Observable.Generate
来构造一个 Range
方法:
// Example code only public static IObservable<int> Range(int start, int count) { int max = start + count; return Observable.Generate( start, value => value < max, value => value + 1, value => value); }
The Generate
method calls us back repeatedly until either our condition
callback says we're done, or the observer unsubscribes. We can define an infinite sequence simply by never saying we are done:
Generate
方法会反复调用我们的回调函数,直到我们的条件回调函数说我们已经完成了,或者观察者取消了订阅。我们可以通过永远不说我们完成了来定义一个无限序列:
IObservable<BigInteger> Fibonacci() { return Observable.Generate( (v1: new BigInteger(1), v2: new BigInteger(1)), value => true, // It never ends! value => (value.v2, value.v1 + value.v2), value => value.v1); }
Timed Sequence Generators 定时序列生成器
Most of the methods we've looked at so far have returned sequences that produce all of their values immediately. (The only exception is where we called Observable.Create
and produced values when we were ready to.) However, Rx is able to generate sequences on a schedule.
到目前为止,我们查看的大多数方法都返回了立即产生所有值的序列。(唯一的例外是我们调用 Observable.Create
并在我们准备好时产生值的情况。)然而,Rx 能够按照计划生成序列。
As we'll see, operators that schedule their work do so through an abstraction called a scheduler. If you don't specify one, they will pick a default scheduler, but sometimes the timer mechanism is significant. For example, there are timers that integrate with UI frameworks, delivering notifications on the same thread that mouse clicks and other input are delivered on, and we might want Rx's time-based operators to use these. For testing purposes it can be useful to virtualize timings, so we can verify what happens in timing-sensitive code without necessarily waiting for tests to execute in real time.
正如我们将看到的,那些调度其工作的运算符是通过一个名为调度器(scheduler)的抽象概念来实现的。如果你不指定一个调度器,它们将选择一个默认的调度器,但有时计时机制非常重要。例如,有些计时器与UI框架集成,能够在处理鼠标点击和其他输入的同一个线程(即UI线程)上发送通知,而我们可能希望Rx的基于时间的运算符能够使用这些计时器。出于测试目的,将时间虚拟化可能是有益的,这样我们就可以在不必等待测试实时执行的情况下,验证对时间敏感的代码中的行为。
Schedulers are a complex subject that is out of scope for this chapter, but they are covered in detail in the later chapter on Scheduling and threading.
调度器是一个复杂的主题,超出了本章的范围,但它们在后续的“调度和线程”章节中有详细讨论。
There are three ways of producing timed events.
有三种方式可以产生定时事件。
Observable.Interval 方法
The first is Observable.Interval(TimeSpan)
which will publish incremental values starting from zero, based on a frequency of your choosing.
第一种是 Observable.Interval(TimeSpan)
,它会根据你选择的频率发布从零开始的递增值。
This example publishes values every 250 milliseconds.
这个示例每 250 毫秒发布一个值。
IObservable<long> interval = Observable.Interval(TimeSpan.FromMilliseconds(250)); interval.Subscribe( Console.WriteLine, () => Console.WriteLine("completed"));
Output:
输出:
0 1 2 3 4 5
Once subscribed, you must dispose of your subscription to stop the sequence, because Interval
returns an infinite sequence. Rx presumes that you might have considerable patience, because the sequences returned by Interval
are of type IObservable<long>
(long
, not int
) meaning you won't hit problems if you produce more than a paltry 2.1475 billion event (i.e. more than int.MaxValue
).
一旦订阅,你必须释放你的订阅以停止序列,因为 Interval
返回一个无限序列。Rx 假定你可能有足够的耐心,因为 Interval
返回的序列是 IObservable<long>
类型(long,不是 int),意味着如果你产生超过可怜的 21 亿 4750 万个事件(即超过 int.MaxValue
),你也不会遇到问题。
Observable.Timer 方法
The second factory method for producing constant time based sequences is Observable.Timer
. It has several overloads. The most basic one takes just a TimeSpan
as Observable.Interval
does. But unlike Observable.Interval
, Observable.Timer
will publish exactly one value (the number 0) after the period of time has elapsed, and then it will complete.
产生基于时间的常数列的第二种工厂方法是 Observable.Timer
。它有几个重载。最基本的重载只接受一个 TimeSpan
,就像 Observable.Interval
一样。但与 Observable.Interval
不同,Observable.Timer
会在指定的时间过去后恰好发布一个值(数字 0),然后它就会完成。
var timer = Observable.Timer(TimeSpan.FromSeconds(1)); timer.Subscribe( Console.WriteLine, () => Console.WriteLine("completed"));
Output:
输出:
0 completed
Alternatively, you can provide a DateTimeOffset
for the dueTime
parameter. This will produce the value 0 and complete at the specified time.
另外,你可以为 dueTime
参数提供一个 DateTimeOffset
。这将在指定的时间产生值 0 并完成。
A further set of overloads adds a TimeSpan
that indicates the period at which to produce subsequent values. This allows us to produce infinite sequences. It also shows how Observable.Interval
is really just a special case of Observable.Timer
. Interval
could be implemented like this:
另一组重载增加了一个 TimeSpan
,表示生成后续值的时间间隔。这允许我们产生无限序列。这也展示了 Observable.Interval
实际上只是 Observable.Timer
的一个特例。Interval
可以像这样实现:
public static IObservable<long> Interval(TimeSpan period) { return Observable.Timer(period, period); }
While Observable.Interval
will always wait the given period before producing the first value, this Observable.Timer
overload gives the ability to start the sequence when you choose. With Observable.Timer
you can write the following to have an interval sequence that starts immediately.
虽然 Observable.Interval
总是在生成第一个值之前等待指定的时间段,但这个 Observable.Timer
的重载版本允许你在选择的时间点开始序列。使用 Observable.Timer
,你可以编写以下代码来创建一个立即开始的间隔序列。
Observable.Timer(TimeSpan.Zero, period);
This takes us to our third way and most general way for producing timer related sequences, back to Observable.Generate
.
这就引出了我们第三种也是最常见的方法来产生与计时器相关的序列,即回到 Observable.Generate
。
Timed Observable.Generate 定时的Observable.Generate方法
There's a more complex overload of Observable.Generate
that allows you to provide a function that specifies the due time for the next value.
Observable.Generate
有一个更复杂的重载,允许你提供一个函数来指定下一个值的到期时间。
public static IObservable<TResult> Generate<TState, TResult>( TState initialState, Func<TState, bool> condition, Func<TState, TState> iterate, Func<TState, TResult> resultSelector, Func<TState, TimeSpan> timeSelector)
The extra timeSelector
argument lets us tell Generate
when to produce the next item. We can use this to write our own implementation of Observable.Timer
(and as you've already seen, this in turn enables us to write our own Observable.Interval
).
额外的 timeSelector
参数允许我们告诉 Generate
何时产生下一个项。我们可以使用它来写我们自己的 Observable.Timer
实现(而且正如你已经看到的,这反过来又使我们能够写我们自己的 Observable.Interval
)。
public static IObservable<long> Timer(TimeSpan dueTime) { return Observable.Generate( 0l, i => i < 1, i => i + 1, i => i, i => dueTime); } public static IObservable<long> Timer(TimeSpan dueTime, TimeSpan period) { return Observable.Generate( 0l, i => true, i => i + 1, i => i, i => i == 0 ? dueTime : period); } public static IObservable<long> Interval(TimeSpan period) { return Observable.Generate( 0l, i => true, i => i + 1, i => i, i => period); }
This shows how you can use Observable.Generate
to produce infinite sequences. I will leave it up to you the reader, as an exercise using Observable.Generate
, to produce values at variable rates.
这展示了如何使用 Observable.Generate
来产生无限序列。我将把它留给你,读者,作为一个使用 Observable.Generate
的练习,以可变速率产生值。
Observable sequences and state 可观察序列和状态
As Observable.Generate
makes particularly clear, observable sequences may need to maintain state. With that operator it is explicit—we pass in initial state, and we supply a callback to update it on each iteration. Plenty of other operators maintain internal state. The Timer
remembers its tick count, and more subtly, has to somehow keep track of when it last raised an event and when the next one is due. And as you'll see in forthcoming chapters, plenty of other operators need to remember information about what they've already seen.
正如Observable.Generate所明确指出的那样,可观察序列可能需要维持状态。使用该运算符时,状态是明确的——我们传入初始状态,并提供一个回调函数以便在每次迭代时更新它。许多其他运算符也维护内部状态。Timer会记住它的滴答计数,而且更微妙的是,它必须以某种方式跟踪上次触发事件的时间和下一个事件应该触发的时间。正如你将在接下来的章节中看到的那样,许多其他运算符也需要记住它们已经看到的信息。
This raises an interesting question: what happens if a process shuts down? Is there a way to preserve that state, and reconstitute it in a new process.
这引发了一个有趣的问题:如果一个进程关闭了,会发生什么?是否有一种方法来保存该状态,并在新进程中重新构建它?
With ordinary Rx.NET, the answer is no: all such state is held entirely in memory and there is no way to get hold of that state, or to ask running subscriptions to serialize their current state. This means that if you are dealing with particularly long-running operations you need to work out how you would restart and you can't rely on System.Reactive
to help you. However, there is a related Rx-based set of libraries known collectively as the Reaqtive libraries. These provide implementations of most of the same operators as System.Reactive
, but in a form where you can collect the current state, and recreate new subscriptions from previously preserved state. These libraries also include a component called Reaqtor, which is a hosting technology that can manage automatic checkpointing, and post-crash recovery, making it possible to support very long-running Rx logic, by making subscriptions persistent and reliable. Be aware that this is not currently in any productised form, so you will need to do a fair amount of work to use it, but if you need a persistable version of Rx, be aware that it exists.
使用普通的Rx.NET,答案是否定的:所有这样的状态都完全保存在内存中,无法获取这些状态,也无法要求正在运行的订阅序列化其当前状态。这意味着如果你正在处理特别耗时的操作,你需要自己想出如何重启,而不能依赖System.Reactive来帮助你。然而,存在一组基于Rx的相关库,统称为Reaqtive库。这些库提供了与System.Reactive中大多数相同操作符的实现,但以一种可以收集当前状态并从先前保存的状态重新创建新订阅的形式。这些库还包括一个名为Reaqtor的组件,它是一种托管技术,可以管理自动检查点(checkpointing)和崩溃后恢复,通过使订阅持久化和可靠化,来支持非常耗时的Rx逻辑。请注意,目前这些库尚未以任何产品化的形式存在,因此你需要做相当多的工作才能使用它,但如果你需要一个可持久化的Rx版本,要知道它确实是存在的。
Adapting Common Types to IObservable<T>
将常见类型转换为IObservable<T>
Although we've now seen two very general ways to produce arbitrary sequences—Create
and Generate
—what if you already have an existing source of information in some other form that you'd like to make available as an IObservable<T>
? Rx provides a few adapters for common source types.
虽然我们已经看到了两种非常通用的方式来生成任意序列—— Create
和 Generate
——但如果你已经以其他形式拥有了一个现有的信息源,并且希望将其作为 IObservable<T>
提供,怎么办呢?Rx 为常见的源类型提供了一些适配器。
From delegates 从委托转换IObservable<T>
The Observable.Start
method allows you to turn a long running Func<T>
or Action
into a single value observable sequence. By default, the processing will be done asynchronously on a ThreadPool thread. If the overload you use is a Func<T>
then the return type will be IObservable<T>
. When the function returns its value, that value will be published and then the sequence completed. If you use the overload that takes an Action
, then the returned sequence will be of type IObservable<Unit>
. The Unit
type represents the absence of information, so it's somewhat analogous to void
, except you can have an instance of the Unit
type. It's particularly useful in Rx because we often care only about when something has happened, and there might not be any information besides timing. In these cases, we often use an IObservable<Unit>
so that it's possible to produce definite events even though there's no meaningful data in them. (The name comes from the world of functional programming, where this kind of construct is used a lot.) In this case, Unit
is used to publish an acknowledgement that the Action
is complete, because an Action
does not return any information. The Unit
type itself has no value; it just serves as an empty payload for the OnNext
notification. Below is an example of using both overloads.
Observable.Start
方法允许你将一个长时间运行的 Func<T>
或 Action
转换为一个单值可观察序列。默认情况下,处理将在 ThreadPool 线程上异步进行。如果你使用的重载是 Func<T>
,那么返回类型将是 IObservable<T>
。当函数返回其值时,该值将被发布,然后序列完成。如果你使用接受 Action
的重载,那么返回的序列将是 IObservable<Unit>
类型。Unit
类型表示信息的缺失,因此它与 void
有些类似,但你可以拥有 Unit
类型的实例。在 Rx 中,它特别有用,因为我们经常只关心何时发生了某件事,除了时间之外可能没有其他信息。在这些情况下,我们经常使用 IObservable<Unit>
,以便即使它们中没有有意义的数据,也能产生确定的事件。(这个名字来自函数式编程的世界,在那里这种构造被广泛使用。)在这种情况下,Unit
用于发布一个确认 Action
已完成的通知,因为 Action
不返回任何信息。Unit
类型本身没有值;它只是作为 OnNext
通知的空负载。以下是使用这两种重载的示例。
static void StartAction() { var start = Observable.Start(() => { Console.Write("Working away"); for (int i = 0; i < 10; i++) { Thread.Sleep(100); Console.Write("."); } }); start.Subscribe( unit => Console.WriteLine("Unit published"), () => Console.WriteLine("Action completed")); } static void StartFunc() { var start = Observable.Start(() => { Console.Write("Working away"); for (int i = 0; i < 10; i++) { Thread.Sleep(100); Console.Write("."); } return "Published value"; }); start.Subscribe( Console.WriteLine, () => Console.WriteLine("Action completed")); }
Note the difference between Observable.Start
and Observable.Return
. The Start
method invokes our callback only upon subscription, so it is an example of a 'lazy' operation. Conversely, Return
requires us to supply the value up front.
请注意 Observable.Start
和 Observable.Return
之间的区别。Start
方法仅在订阅时调用我们的回调函数,因此它是一个“惰性”运算的示例。
The observable returned by Start
may seem to have a superficial resemblance to Task
or Task<T>
(depending on whether you use the Action
or Func<T>
overload). Each represents work that may take some time before eventually completing, perhaps producing a result. However, there's a significant difference: Start
doesn't begin the work until you subscribe to it. Moreover, it will re-execute the callback every time you subscribe to it. So it is more like a factory for a task-like entity.
Start
返回的可观察对象(observable)在表面上似乎与 Task
或 Task<T>
(取决于你使用的是 Action
还是 Func<T>
的重载)有些相似。它们都代表了一项可能需要一些时间才能完成的工作,并可能产生一个结果。然而,有一个重要的区别:Start
并不会在你订阅它之前开始工作。而且,每次你订阅它时,它都会重新执行回调函数。因此,它更像是一个任务类实体的工厂。
From events 从事件转换到IObservable<T>
As we discussed early in the book, .NET has a model for events that is baked into its type system. This predates Rx (not least because Rx wasn't feasible until .NET got generics in .NET 2.0) so it's common for types to support events but not Rx. To be able to integrate with the existing event model, Rx provides methods to take an event and turn it into an observable sequence. I showed this briefly in the file system watcher example earlier, but let's examine this in a bit more detail. There are several different varieties you can use. This show the most succinct form:
正如我们在本书早期所讨论的,.NET 在其类型系统中内置了一个事件模型。这个模型早于 Rx(尤其是因为在 .NET 2.0 引入泛型之前,Rx 是不可能实现的),所以类型支持事件但不支持 Rx 是很常见的。为了能够与现有的事件模型集成,Rx 提供了将事件转换为可观察序列的方法。我之前在文件系统监视器示例中简要地展示了这一点,但让我们更详细地研究一下。你可以使用几种不同的形式。这里展示的是最简洁的形式:
FileSystemWatcher watcher = new (@"c:\incoming"); IObservable<EventPattern<FileSystemEventArgs>> changeEvents = Observable .FromEventPattern<FileSystemEventArgs>(watcher, nameof(watcher.Changed));
If you have an object that provides an event, you can use this overload of FromEventPattern
, passing in the object and the name of the event that you'd like to use with Rx. Although this is the simplest way to adapt events into Rx's world, it has a few problems.
如果你有一个提供事件的对象,你可以使用 FromEventPattern
的这个重载,传入该对象和你想要与 Rx 一起使用的事件的名称。尽管这是将事件适配到 Rx 世界中的最简单方法,但它存在一些问题。
Firstly, why do I need to pass the event name as a string? Identifying members with strings is an error-prone technique. The compiler won't notice if there's a mismatch between the first and second argument (e.g., if I passed the arguments (somethingElse, nameof(watcher.Changed))
by mistake). Couldn't I just pass watcher.Changed
itself? Unfortunately not—this is an example of the issue I mentioned in the first chapter: .NET events are not first class citizens. We can't use them in the way we can use other objects or values. For example, we can't pass an event as an argument to a method. In fact the only thing you can do with a .NET event is attach and remove event handlers. If I want to get some other method to attach handlers to the event of my choosing (e.g., here I want Rx to handle the events), then the only way to do that is to specify the event's name so that the method (FromEventPattern
) can then use reflection to attach its own handlers.
首先,为什么我需要将事件名称作为字符串传递?使用字符串来识别成员是一种容易出错的技术。如果第一个和第二个参数之间存在不匹配(例如,如果我错误地传递了参数 (somethingElse, nameof(watcher.Changed))
),编译器不会注意到。难道我不能直接传递 watcher.Changed
本身吗?不幸的是,不能这样做——这是我在第一章中提到的问题的一个例子:.NET 事件不是一等公民。我们不能像使用其他对象或值那样使用它们。例如,我们不能将事件作为参数传递给方法。实际上,你唯一可以对 .NET 事件做的事情就是附加和移除事件处理程序。如果我想让其他方法(例如,这里我想让 Rx 处理事件)附加处理程序到我选择的事件上,那么唯一的方法是指定事件的名称,以便该方法(FromEventPattern
)可以使用反射来附加它自己的处理程序。
This is a problem for some deployment scenarios. It is increasingly common in .NET to do extra work at build time to optimize runtime behaviour, and reliance on reflection can compromise these techniques. For example, instead of relying on Just In Time (JIT) compilation of code, we might use Ahead of Time (AOT) mechanisms. .NET's Ready to Run (R2R) system enables you to include pre-compiled code targeting specific CPU types alongside the normal IL, avoiding having to wait for .NET to compile the IL into runnable code. This can have a significant effect on startup times. In client side applications, it can fix problems where applications are sluggish when they first start up. It can also be important in server-side applications, especially in environments where code may be moved from one compute node to another fairly frequently, making it important to minimize cold start costs. There are also scenarios where JIT compilation is not even an option, in which case AOT compilation isn't merely an optimization: it's the only means by which code can run at all.
这对于某些部署场景来说是个问题。在.NET中,为了优化运行时行为而在构建时做额外的工作变得越来越普遍,而依赖反射可能会损害这些技术。例如,我们可能使用提前编译(Ahead of Time,AOT)机制,而不是依赖代码的即时编译(Just In Time,JIT)。.NET的Ready to Run(R2R)系统允许你在正常的中间语言(Intermediate Language,IL)旁边包含针对特定CPU类型预编译的代码,从而避免了等待.NET将IL编译成可运行的代码。这可以显著影响启动时间。在客户端应用程序中,它可以解决应用程序在首次启动时运行缓慢的问题。在服务器端应用程序中,它也很重要,特别是在代码可能频繁地从一个计算节点移动到另一个计算节点的环境中,此时最小化冷启动成本变得尤为重要。还有一些场景甚至不能使用JIT编译,在这种情况下,AOT编译不仅仅是一种优化:它是代码能够运行的唯一手段。
The problem with reflection is that it makes it difficult for the build tools to work out what code will execute at runtime. When they inspect this call to FromEventPattern
they will just see arguments of type object
and string
. It's not self-evident that this is going to result in reflection-driven calls to the add
and remove
methods for FileSystemWatcher.Changed
at runtime. There are attributes that can be used to provide hints, but there are limits to how well these can work. Sometimes the build tools will be unable to determine what code would need to be AOT compiled to enable this method to execute without relying on runtime JIT.
反射的问题在于,它使得构建工具难以确定哪些代码将在运行时执行。当它们检查对FromEventPattern
的调用时,它们只能看到类型为object
和string
的参数。这并不明显表明这将在运行时导致对FileSystemWatcher.Changed
的add
和remove
方法进行反射驱动的调用。虽然可以使用特性来提供提示,但这些特性的作用有限。有时,构建工具无法确定需要哪些代码进行AOT编译,以使该方法能够在不依赖运行时JIT的情况下执行。
There's another, related problem. The .NET build tools support a feature called 'trimming', in which they remove unused code. The System.Reactive.dll
file is about 1.3MB in size, but it would be a very unusual application that used every member of every type in that component. Basic use of Rx might need only a few tens of kilobytes. The idea with trimming is to work out which bits are actually in use, and produce a copy of the DLL that contains only that code. This can dramatically reduce the volume of code that needs to be deployed for an executable to run. This can be especially important in client-side Blazor applications, where .NET components end up being downloaded by the browser. Having to download an entire 1.3MB component might make you think twice about using it. But if trimming means that basic usage requires only a few tens of KB, and that the size would increase only if you were making more extensive use of the component, that can make it reasonable to use a component that would, without trimming, have imposed too large a penalty to justify its inclusion. But as with AOT compilation, trimming can only work if the tools can determine which code is in use. If they can't do that, it's not just a case of falling back to a slower path, waiting while the relevant code gets JIT compiler. If code has been trimmed, it will be unavailable at runtime, and your application might crash with a MissingMethodException
.
还有一个与之相关的问题。.NET构建工具支持一个名为“修剪”(trimming)的功能,该功能可以删除未使用的代码。System.Reactive.dll
文件的大小约为1.3MB,但使用该组件中每个类型的每个成员的应用程序将非常罕见。Rx的基本使用可能只需要几十KB。修剪的目的是确定哪些部分实际上在使用中,并生成一个仅包含这些代码的DLL副本。这可以显著减少可执行文件运行所需部署的代码量。这在客户端Blazor应用程序中尤为重要,因为.NET组件最终将由浏览器下载。如果必须下载一个完整的1.3MB组件,您可能会对其使用三思而后行。但是,如果修剪意味着基本使用只需要几十KB,并且只有在更广泛使用该组件时大小才会增加,那么使用未修剪时将带来过大负担而不值得包含的组件就变得合理了。但是,与AOT编译一样,修剪只有在工具能够确定哪些代码在使用中时才能工作。如果它们无法做到这一点,那么问题就不仅仅是退回到更慢的路径,等待相关代码进行JIT编译。如果代码已被修剪,它将在运行时不可用,并且您的应用程序可能会因MissingMethodException
异常而崩溃。
So reflection-based APIs can be problematic if you're using any of these techniques. Fortunately, there's an alternative. We can use an overload that takes a couple of delegates, and Rx will invoke these when it wants to add or remove handlers for the event:
因此,如果你使用这些技术中的任何一种,基于反射的 API 可能会成为问题。幸运的是,有一种替代方案。我们可以使用接受几个委托的重载,当 Rx 想要添加或移除事件处理程序时,它会调用这些委托:
IObservable<EventPattern<FileSystemEventArgs>> changeEvents = Observable .FromEventPattern<FileSystemEventHandler, FileSystemEventArgs>( h => watcher.Changed += h, h => watcher.Changed -= h);
This is code that AOT and trimming tools can understand easily. We've written methods that explicitly add and remove handlers for the FileSystemWatcher.Changed
event, so AOT tools can pre-compile those two methods, and trimming tools know that they cannot remove the add and remove handlers for those events.
这是AOT(提前编译)和修剪工具能够轻松理解的代码。我们编写了明确添加和删除FileSystemWatcher.Changed
事件处理程序的方法,因此AOT工具可以预编译这两个方法,而修剪工具也知道它们不能删除这些事件的add和remove处理程序。
The downside is that this is a pretty cumbersome bit of code to write. If you've not already bought into the idea of using Rx, this might well be enough to make you think "I'll just stick with ordinary .NET events, thanks." But the cumbersome nature is a symptom of what is wrong with .NET events. We wouldn't have had to write anything so ugly if events had been first class citizens in the first place.
不利之处在于,编写这样的代码相当繁琐。如果你还没有接受使用 Rx 的想法,这可能会让你觉得“我还是继续使用普通的 .NET 事件吧,谢谢”。但这种繁琐的写法是 .NET 事件本身存在的问题的一个症状。如果事件从一开始就是一等公民(first class citizens),我们就不必编写如此丑陋的代码了。
Not only has that second-class status meant we couldn't just pass the event itself as an argument, it has also meant that we've had to state type arguments explicitly. The relationship between an event's delegate type (FileSystemEventHandler
in this example) and its event argument type (FileSystemEventArgs
here) is, in general, not something that C#'s type inference can determine automatically, which is why we've had to specify both types explicitly. (Events that use the generic EventHandler<T>
type are more amenable to type inference, and can use a slightly less verbose version of FromEventPattern
. Unfortunately, relatively few events actually use that. Some events provide no information besides the fact that something just happened, and use the base EventHandler
type, and for those kinds of events, you can in fact omit the type arguments completely, making the code slightly less ugly. You still need to provide the add and remove callbacks though.)
这种二等公民的地位不仅意味着我们不能直接传递事件本身作为参数,还意味着我们必须明确声明类型参数。一个事件的委托类型(在此示例中为 FileSystemEventHandler
)与其事件参数类型(在此处为 FileSystemEventArgs
)之间的关系,通常不是 C# 的类型推断可以自动确定的,这就是为什么我们必须明确指定这两种类型的原因。(使用泛型 EventHandler<T>
类型的事件更容易进行类型推断,并可以使用稍微不那么冗长的 FromEventPattern
版本。不幸的是,实际使用这种方式的事件相对较少。有些事件除了某事刚刚发生之外不提供任何信息,并使用基础的 EventHandler
类型,对于这类事件,你实际上可以完全省略类型参数,使代码稍微不那么难看。不过,你仍然需要提供add和remove的回调函数。)
Notice that the return type of FromEventPattern
in this example is:
请注意,在这个例子中,FromEventPattern
的返回类型是:
IObservable<EventPattern<FileSystemEventArgs>>
.
The EventPattern<T>
type encapsulates the information that the event passes to handlers. Most .NET events follow a common pattern in which handler methods take two arguments: an object sender
, which just tells you which object raised the event (useful if you attach one event handler to multiple objects) and then a second argument of some type derived from EventArgs
that provides information about the event. EventPattern<T>
just packages these two arguments into a single object that offers Sender
and EventArgs
properties. In cases where you don't in fact want to attach one handler to multiple sources, you only really need that EventArgs
property, which is why the earlier FileSystemWatcher
examples went on to extract just that, to get a simpler result of type IObservable<FileSystemEventArgs>
. It did this with the Select
operator, which we'll get to in more detail later:
EventPattern<T>
类型封装了事件传递给处理程序的信息。大多数 .NET 事件遵循一个通用模式,即处理程序方法接受两个参数:一个 object sender
,它只是告诉你哪个对象引发了事件(如果你将一个事件处理程序附加到多个对象上,这会很有用),然后是某个从 EventArgs
派生的类型的第二个参数,它提供了关于事件的信息。EventPattern<T>
只是将这两个参数打包成一个提供 Sender
和 EventArgs
属性的单一对象。在你不希望将一个处理程序附加到多个源的情况下,你实际上只需要 EventArgs
属性,这就是为什么前面的 FileSystemWatcher
示例接着只提取了该属性,以获得类型为 IObservable<FileSystemEventArgs>
的更简单结果。这是通过 Select
操作符实现的,我们稍后会详细讨论这个运算符:
IObservable<FileSystemEventArgs> changes = changeEvents.Select(ep => ep.EventArgs);
It is very common to want to expose property changed events as observable sequences. The .NET runtime libraries define a .NET-event-based interface for advertising property changes, INotifyPropertyChanged
, and some user interface frameworks have more specialized systems for this, such as WPF's DependencyProperty
. If you are contemplating writing your own wrappers to do this sort of thing, I would strongly suggest looking at the Reactive UI libraries first. It has a set of features for wrapping properties as IObservable<T>
.
将属性更改事件公开为可观察序列是非常常见的做法。.NET 运行时库为公告属性更改定义了一个基于 .NET 事件的接口,即 INotifyPropertyChanged
,而一些用户界面框架为此提供了更专门的系统,如 WPF 的 DependencyProperty
。如果你正在考虑编写自己的包装器来执行此类操作,我强烈建议先查看 Reactive UI 库。它有一组功能用于将属性包装为 IObservable<T>
。
From Task 从Task转换为IObservable<T>
The Task
and Task<T>
types are very widely used in .NET. Mainstream .NET languages have built-in support for working with them (e.g., C#'s async
and await
keywords). There's some conceptual overlap between tasks and IObservable<T>
: both represent some sort of work that might take a while to complete. There is a sense in which an IObservable<T>
is a generalization of a Task<T>
: both represent potentially long-running work, but an IObservable<T>
can produce multiple results whereas Task<T>
can produce just one.
Task
和 Task<T>
类型在 .NET 中被广泛使用。主流 .NET 语言内置了对它们的支持(例如,C# 的 async
和 await
关键字)。任务和 IObservable<T>
之间存在一些概念上的重叠:它们都代表可能需要一段时间才能完成的某种工作。从某种意义上说,IObservable<T>
是 Task<T>
的一种泛化:两者都表示可能运行时间较长的工作,但 IObservable<T>
可以产生多个结果,而 Task<T>
只能产生一个结果。
Since IObservable<T>
is the more general abstraction, we should be able to represent a Task<T>
as an IObservable<T>
. Rx defines various extension methods for Task
and Task<T>
to do this. These methods are all called ToObservable()
, and it offers various overloads offering control of the details where required, and simplicity for the most common scenarios.
由于 IObservable<T>
是一个更为通用的抽象概念,我们应该能够将 Task<T>
表示为 IObservable<T>
。Reactive Extensions (Rx) 为 Task
和 Task<T>
定义了各种扩展方法来实现这一点。这些方法统称为 ToObservable()
,并且提供了多种重载,以便在需要时控制细节,同时为最常见的场景提供简便性。
Although they are conceptually similar, Task<T>
does a few things differently in the details. For example, you can retrieve its Status
property, which might report that it is in a cancelled or faulted state. IObservable<T>
doesn't provide a way to ask a source for its state; it just tells you things. So ToObservable
makes some decisions about how to present status in a way that makes makes sense in an Rx world:
尽管它们在概念上相似,但 Task<T>
在一些细节上的处理略有不同。例如,你可以检索其 Status
属性,该属性可能会报告它处于已取消或错误状态。IObservable<T>
没有提供一种方式来询问源的状态;它只是告诉你发生的事情。因此,ToObservable
在如何以在 Rx 环境中有意义的方式呈现状态方面做了一些决策:
- if the task is Cancelled,
IObservable<T>
invokes a subscriber'sOnError
passing aTaskCanceledException
如果任务被取消,IObservable<T>
会调用订阅者的OnError
方法,并传递一个TaskCanceledException
异常。 - if the task is Faulted
IObservable<T>
invokes a subscriber'sOnError
passing the task's inner exception 如果任务出错,IObservable<T>
会调用订阅者的OnError
方法,并传递任务的内部异常。 - if the task is not yet in a final state (neither Cancelled, Faulted, or RanToCompletion), the
IObservable<T>
will not produce any notifications until such time as the task does enter one of these final states 如果任务尚未进入最终状态(既不是已取消、出错,也不是已完成),那么IObservable<T>
将不会生成任何通知,直到任务进入这些最终状态之一。
It does not matter whether the task is already in a final state at the moment that you call ToObservable
. If it has finished, ToObservable
will just return a sequence representing that state. (In fact, it uses either the Return
or Throw
creation methods you saw earlier.) If the task has not yet finished, ToObservable
will attach a continuation to the task to detect the outcome once it does complete.
当你调用 ToObservable
时,无论该任务是否已处于最终状态都无关紧要。如果它已经完成,ToObservable
将只返回一个表示该状态的序列。(实际上,它会使用你之前看到的 Return
或 Throw
创建方法。)如果任务尚未完成,ToObservable
将在任务上附加一个延续,以便在任务完成时检测其结果。
Tasks come in two forms: Task<T>
, which produces a result, and Task
, which does not. But in Rx, there is only IObservable<T>
—there isn't a no-result form. We've already seen this problem once before, when the Observable.Start
method needed to be able to adapt a delegate as an IObservable<T>
even when the delegate was an Action
that produced no result. The solution was to return an IObservable<Unit>
, and that's also exactly what you get when you call ToObservable
on a plain Task
.
任务有两种形式:Task<T>
,它会产生一个结果,和Task
,它不产生结果。但在Rx中,只有IObservable<T>
——没有不产生结果的形式。我们之前已经遇到过这个问题,当Observable.Start
方法需要能够将一个委托适配为IObservable<T>
时,即使这个委托是一个不产生结果的Action
。解决方案是返回一个IObservable<Unit>
,这也是当你对普通的Task
调用ToObservable
时得到的。
The extension method is simple to use:
这个扩展方法的使用很简单:
Task<string> t = Task.Run(() => { Console.WriteLine("Task running..."); return "Test"; }); IObservable<string> source = t.ToObservable(); source.Subscribe( Console.WriteLine, () => Console.WriteLine("completed")); source.Subscribe( Console.WriteLine, () => Console.WriteLine("completed"));
Here's the output.
这是输出。
Task running...
Test
completed
Test
completed
Notice that even with two subscribers, the task runs only once. That shouldn't be surprising since we only created a single task. If the task has not yet finished, then all subscribers will receive the result when it does. If the task has finished, the IObservable<T>
effectively becomes a single-value cold observable.
请注意,即使有两个订阅者,任务也只运行一次。这并不应该令人惊讶,因为我们只创建了一个任务。如果任务尚未完成,那么当任务完成时,所有订阅者都将收到结果。如果任务已经完成,那么IObservable<T>
实际上就变成了只发出单个值的冷可观察对象。
One Task per subscription 每个订阅一个任务
There's a different way to get an IObservable<T>
for a source. I can replace the first statement in the preceding example with this:
有一种不同的方式可以从一个源获取 IObservable<T>
。我可以将前面示例中的第一个语句替换为这个:
IObservable<string> source = Observable.FromAsync(() => Task.Run(() => { Console.WriteLine("Task running..."); return "Test"; }));
Subscribing twice to this produces slightly different output:
对这个进行两次订阅会产生稍微不同的输出:
Task running...
Task running...
Test
Test
completed
completed
Notice that this executes the task twice, once for each call to Subscribe
. FromAsync
can do this because instead of passing a Task<T>
we pass a callback that returns a Task<T>
. It calls that when we call Subscribe
, so each subscriber essentially gets their own task.
请注意,这会执行两次任务,每次调用 Subscribe
都会执行一次。FromAsync
能够这样做是因为我们传递的是一个返回 Task<T>
的回调,而不是直接传递 Task<T>
。当我们调用 Subscribe
时,它会调用那个回调,所以每个订阅者实际上都获得了他们自己的任务。
If I want to use async
and await
to define my task, then I don't need to bother with the Task.Run
because an async
lambda creates a Func<Task<T>>
, which is exactly the type FromAsync
wants:
如果我想要使用 async
和 await
来定义我的任务,那么我就不需要使用 Task.Run
了,因为异步 lambda 表达式会创建一个 Func<Task<T>>
,这正是 FromAsync
所需要的类型:
IObservable<string> source = Observable.FromAsync(async () => { Console.WriteLine("Task running..."); await Task.Delay(50); return "Test"; });
This produces exactly the same output as before. There is a subtle difference with this though. When I used Task.Run
the lambda ran on a task pool thread from the start. But when I write it this way, the lambda will begin to run on whatever thread calls Subscribe
. It's only when it hits the first await
that it returns (and the call to Subscribe
will then return), with the remainder of the method running on the thread pool.
这会产生与之前完全相同的输出。但这里有一个微妙的区别。当我使用 Task.Run
时,lambda 表达式从一开始就在任务池线程上运行。但是当我以这种方式编写时,lambda 表达式将在调用 Subscribe
的线程上开始运行。只有当它遇到第一个 await
时,它才会返回(此时对 Subscribe
的调用也会返回),而方法的其余部分将在任务池线程上运行。
From IEnumerable<T> 从
IEnumerable<T>
转换到IObservable<T>
Rx defines another extension method called ToObservable
, this time for IEnumerable<T>
. In earlier chapters I described how IObservable<T>
was designed to represent the same basic abstraction as IEnumerable<T>
, with the only difference being the mechanism we use to obtain the elements in the sequence: with IEnumerable<T>
, we write code that pulls values out of the collection (e.g., a foreach
loop), whereas IObservable<T>
pushes values to us by invoking OnNext
on our IObserver<T>
.
Rx 定义了一个名为 ToObservable
的另一个扩展方法,这次是用于 IEnumerable<T>
。在前面的章节中,我描述了 IObservable<T>
是如何被设计用来表示与 IEnumerable<T>
相同的基本抽象概念的,唯一的区别是我们用来获取序列中元素的机制:对于 IEnumerable<T>
,我们编写代码从集合中拉取值(例如,使用 foreach
循环),而 IObservable<T>
则通过在我们的 IObserver<T>
上调用 OnNext
方法来推送值给我们。
We could write code that bridges from pull to push:
我们可以编写代码来实现从拉取到推送的桥接:
// Example code only - do not use! public static IObservable<T> ToObservableOversimplified<T>(this IEnumerable<T> source) { return Observable.Create<T>(o => { foreach (var item in source) { o.OnNext(item); } o.OnComplete(); // Incorrectly ignoring unsubscription. return Disposable.Empty; }); }
This crude implementation conveys the basic idea, but it is naive. It does not attempt to handle unsubscription, and it's not easy to fix that when using Observable.Create
for this particular scenario. And as we will see later in the book, Rx sources that might try to deliver large numbers of events in quick succession should integrate with Rx's concurrency model. The implementation that Rx supplies does of course cater for all of these tricky details. That makes it rather more complex, but that's Rx's problem; you can think of it as being logically equivalent to the code shown above, but without the shortcomings.
这个简单的实现传达了基本思想,但它是很原始的。它并没有尝试处理退订(unsubscribe)的情况,而且在这种特定场景下使用 Observable.Create
来修复这个问题并不容易。正如我们将在本书后面看到的,可能会在短时间内尝试传递大量事件的 Rx 源应该与 Rx 的并发模型集成。Rx 提供的实现当然考虑了所有这些棘手的细节。这使得它相当复杂,但这是 Rx 的问题;你可以将其视为逻辑上等价于上面显示的代码,但没有这些缺点。
In fact this is a recurring theme throughout Rx.NET. Many of the built-in operators are useful not because they do something particularly complicated, but because they deal with many subtle and tricky issues for you. You should always try to find something built into Rx.NET that does what you need before considering rolling your own solution.
事实上,这是 Rx.NET 中反复出现的主题。许多内置的运算符之所以有用,并不是因为它们执行了特别复杂的操作,而是因为它们为你处理了许多微妙且棘手的问题。在考虑自己实现解决方案之前,你应该始终尝试在 Rx.NET 中找到满足你需求的东西。
When transitioning from IEnumerable<T>
to IObservable<T>
, you should carefully consider what you are really trying to achieve. Consider that the blocking synchronous (pull) nature of IEnumerable<T>
does always not mix well with the asynchronous (push) nature of IObservable<T>
. As soon as something subscribes to an IObservable<T>
created in this way, it is effectively asking to iterate over the IEnumerable<T>
, immediately producing all of the values. The call to Subscribe
might not return until it has reached the end of the IEnumerable<T>
, making it similar to the very simple example shown at the start of this chapter. (I say "might" because as we'll see when we get to schedulers, the exact behaviour depends on the context.) ToObservable
can't work magic—something somewhere has to execute what amounts to a foreach
loop.
在从IEnumerable<T>
转换为IObservable<T>
时,你应该仔细考虑你真正想要实现什么。考虑到IEnumerable<T>
的阻塞同步(拉取)特性并不总是能与IObservable<T>
的异步(推送)特性很好地融合。一旦有事物订阅了以这种方式创建的IObservable<T>
,它实际上就是在请求遍历IEnumerable<T>
,并立即生成所有值。Subscribe
调用可能会一直等到遍历完IEnumerable<T>
的末尾才返回,这与本章开头所示的一个非常简单的例子类似。(我说“可能”是因为,当我们讲到调度器时,我们会发现具体行为取决于上下文。)ToObservable
并不能创造奇迹——在某个地方必须有相当于执行foreach
循环的操作。
So although this can be a convenient way to bring sequences of data into an Rx world, you should carefully test and measure the performance impact.
因此,尽管这是一种将数据序列带入 Rx 世界的方便方式,但你应该仔细测试和测量性能影响。
From APM 从APM创建IObservable<T>
Rx provides support for the ancient .NET Asynchronous Programming Model (APM). Back in .NET 1.0, this was the only pattern for representing asynchronous operations. It was superseded in 2010 when .NET 4.0 introduced the Task-based Asynchronous Pattern (TAP). The old APM offers no benefits over the TAP. Moreover, C#'s async
and await
keywords (and equivalents in other .NET languages) only support the TAP, meaning that the APM is best avoided. However, the TAP was fairly new back in 2011 when Rx 1.0 was released, so it offered adapters for presenting an APM implementation as an IObservable<T>
.
Rx(响应式扩展)为古老的.NET异步编程模型(APM)提供了支持。在.NET 1.0时代,这是表示异步操作的唯一模式。然而,当.NET 4.0在2010年引入基于任务的异步模式(TAP)时,旧的APM就被取代了。旧的APM在TAP面前没有任何优势。而且,C#的async
和await
关键字(以及其他.NET语言中的等效关键字)仅支持TAP,这意味着最好避免使用APM。不过,当Rx 1.0在2011年发布时,TAP还是一个相对较新的概念,因此它提供了适配器,用于将APM实现呈现为IObservable<T>
。
Nobody should be using the APM today, but for completeness (and just in case you have to use an ancient library that only offers the APM) I will provide a very brief explanation of Rx's support for it.
如今没有人应该再使用APM(异步编程模型)了,但为了完整性(以及以防万一你不得不使用一个仅提供APM的古老库),我将对Rx对其的支持提供一个非常简短的解释。
The result of the call to Observable.FromAsyncPattern
does not return an observable sequence. It returns a delegate that returns an observable sequence. (So it is essentially a factory factory.) The signature for this delegate will match the generic arguments of the call to FromAsyncPattern
, except that the return type will be wrapped in an observable sequence. The following example wraps the Stream
class's BeginRead
/EndRead
methods (which are an implementation of the APM).
调用Observable.FromAsyncPattern
的结果并不直接返回一个可观察序列。它返回一个委托,该委托返回一个可观察序列。(所以,它本质上是一个“工厂工厂”)。这个委托的签名将与FromAsyncPattern
的泛型参数相匹配,除了返回类型将被封装在一个可观察序列中。以下示例包装了Stream
类的BeginRead/EndRead
方法(这是APM的一个实现)。
Note: this is purely to illustrate how to wrap the APM. You would never do this in practice because Stream
has supported the TAP for years.
注意:这纯粹是为了说明如何封装APM。在实际应用中,你永远不会这样做,因为Stream
类多年来一直支持TAP。
Stream stream = GetStreamFromSomewhere(); var fileLength = (int) stream.Length; Func<byte[], int, int, IObservable<int>> read = Observable.FromAsyncPattern<byte[], int, int, int>( stream.BeginRead, stream.EndRead); var buffer = new byte[fileLength]; IObservable<int> bytesReadStream = read(buffer, 0, fileLength); bytesReadStream.Subscribe(byteCount => { Console.WriteLine( "Number of bytes read={0}, buffer should be populated with data now.", byteCount); });
Subjects 主题
So far, this chapter has explored various factory methods that return IObservable<T>
implementations. There is another way though: System.Reactive
defines various types that implement IObservable<T>
that we can instantiate directly. But how do we determine what values these types produce? We're able to do that because they also implement IObserver<T>
, enabling us to push values into them, and those very same values we push in will be the ones seen by observers.
到目前为止,本章已经探讨了返回IObservable<T>
实现的各种工厂方法。不过,还有另一种方式:System.Reactive 定义了可以直接实例化的实现IObservable<T>
的各种类型。但是,我们如何确定这些类型生成什么值呢?我们之所以能够做到这一点,是因为它们还实现了IObserver<T>
接口,这使得我们能够向它们推送值,而我们推送的这些值就是观察者将看到的值。
Types that implement both IObservable<T>
and IObserver<T>
are called subjects in Rx. There's an ISubject<T>
to represent this. (This is in the System.Reactive
NuGet package, unlike IObservable<T>
and IObserver<T>
, which are both built into the .NET runtime libraries.) ISubject<T>
looks like this:
在Rx中,既实现IObservable<T>
又实现IObserver<T>
的类型被称为subject。有一个ISubject<T>
接口来表示这个类型。(这个接口在System.Reactive NuGet包中,不同于IObservable<T>
和IObserver<T>
,这两者都是.NET运行时库中的内置接口。)ISubject<T>
的定义如下:
public interface ISubject<T> : ISubject<T, T> { }
So it turns out there's also a two-argument ISubject<TSource, TResult>
to accommodate the fact that something that is both an observer and an observable might transform the data that flows through it in some way, meaning that the input and output types are not necessarily the same. Here's the two-type-argument definition:
所以,还有一个带有两个类型参数的ISubject<TSource, TResult>
接口,以适应既是观察者又是可观察对象的东西可能会以某种方式转换流经它的数据这一事实,这意味着输入和输出类型不一定相同。以下是带有两个类型参数的定义:
public interface ISubject<in TSource, out TResult> : IObserver<TSource>, IObservable<TResult> { }
As you can see the ISubject
interfaces don't define any members of their own. They just inherit from IObserver<T>
and IObservable<T>
—these interfaces are nothing more than a direct expression of the fact that a subject is both an observer and an observable.
如你所见,ISubject
接口并没有定义自己的任何成员。它们只是从IObserver<T>
和IObservable<T>
继承——这些接口仅仅是直接表达了subject既是观察者又是可观察对象这一事实。
But what is this for? You can think of IObserver<T>
and the IObservable<T>
as the 'consumer' and 'publisher' interfaces respectively. A subject, then is both a consumer and a publisher. Data flows both into and out of a subject.
但这有什么用处呢?你可以将IObserver<T>
和IObservable<T>
分别视为“消费者”和“发布者”接口。那么,一个subject既是消费者又是发布者。数据既可以流入也可以流出subject。
Rx offers a few subject implementations that can occasionally be useful in code that wants to make an IObservable<T>
available. Although Observable.Create
is usually the preferred way to do this, there's one important case where a subject might make more sense: if you have some code that discovers events of interest (e.g., by using the client API for some messaging technology) and wants to make them available through an IObservable<T>
, subjects can sometimes provide a more convenient way to to this than with Observable.Create
or a custom implementation.
Rx 提供了一些 subject 的实现,这些实现在需要将一个 IObservable<T>
暴露给代码时可能会很有用。虽然 Observable.Create
通常是首选方式,但在一种重要的情况下,使用 subject 可能会更有意义:如果你有一些代码用于发现感兴趣的事件(例如,通过使用某种消息传递技术的客户端 API),并且希望通过 IObservable<T>
提供这些事件,subject 有时比使用 Observable.Create
或自定义实现提供更方便的方式来实现这一点。
Rx offers a few subject types. We'll start with the most straightforward one to understand.
Rx 提供了几种 subject 类型。我们将从最容易理解的那种开始介绍。
Subject<T>
The Subject<T>
type immediately forwards any calls made to its IObserver<T>
methods on to all of the observers currently subscribed to it. This example shows its basic operation:
Subject<T>
类型会立即将其 IObserver<T>
方法上的任何调用转发给当前已订阅的所有观察者。以下示例展示了其基本操作:
Subject<int> s = new(); s.Subscribe(x => Console.WriteLine($"Sub1: {x}")); s.Subscribe(x => Console.WriteLine($"Sub2: {x}")); s.OnNext(1); s.OnNext(2); s.OnNext(3);
I've created a Subject<int>
. I've subscribed to it twice, and then called its OnNext
method repeatedly. This produces the following output, illustrating that the Subject<int>
forwards each OnNext
call onto both subscribers:
我创建了一个 Subject<int>
。我订阅了它两次,然后反复调用其 OnNext
方法。这产生了以下输出,说明了 Subject<int>
将每个 OnNext
调用转发给两个订阅者:
Sub1: 1 Sub2: 1 Sub1: 2 Sub2: 2 Sub1: 3 Sub2: 3
We could use this as a way to bridge between some API from which we receive data into the world of Rx. You could imagine writing something of this kind:
我们可以使用这种方式来连接一些我们从中接收数据的 API 到 Rx 的世界。你可以想象写出这样的东西:
public class MessageQueueToRx : IDisposable { private readonly Subject<string> messages = new(); public IObservable<string> Messages => messages; public void Run() { while (true) { // Receive a message from some hypothetical message queuing service string message = MqLibrary.ReceiveMessage(); messages.OnNext(message); } } public void Dispose() { message.Dispose(); } }
It wouldn't be too hard to modify this to use Observable.Create
instead. But where this approach can become easier is if you need to provide multiple different IObservable<T>
sources. Imagine we distinguish between different message types based on their content, and publish them through different observables. That's hard to arrange with Observable.Create
if we still want a single loop pulling messages off the queue.
使用 Observable.Create
替代这种方法并不太难。但是,当你需要提供多个不同的 IObservable<T>
源时,这种方法可能会更简单。想象一下,我们根据消息的内容区分不同的消息类型,并通过不同的可观察对象发布它们。如果我们仍然想要使用一个单独的循环从队列中拉取消息,那么使用 Observable.Create
来实现这一点会很困难。
Subject<T>
also distributes calls to either OnCompleted
or OnError
to all subscribers. Of course, the rules of Rx require that once you have called either of these methods on an IObserver<T>
(and any ISubject<T>
is an IObserver<T>
, so this rule applies to Subject<T>
) you must not call OnNext
, OnError
, or OnComplete
on that observer ever again. In fact, Subject<T>
will tolerate calls that break this rule—it just ignores them, so even if your code doesn't quite stick to these rules internally, the IObservable<T>
you present to the outside world will behave correctly, because Rx enforces this.
Subject<T>
还会将所有对 OnCompleted
或 OnError
的调用分发给所有订阅者。当然,Rx 的规则要求一旦你在一个 IObserver<T>
上调用了这两个方法中的任何一个(因为任何 ISubject<T>
都是 IObserver<T>
,所以这个规则适用于 Subject<T>
),你就不能再在那个观察者上调用 OnNext
、OnError
或 OnComplete
了。实际上,Subject<T>
会容忍违反这一规则的调用——它只是忽略它们,因此即使你的代码在内部没有严格遵循这些规则,你向外部世界展示的 IObservable<T>
也会表现出正确的行为,因为 Rx 强制执行了这一规则。
Subject<T>
implements IDisposable
. Disposing a Subject<T>
puts it into a state where it will throw an exception if you call any of its methods. The documentation also describes it as unsubscribing all observers, but since a disposed Subject<T>
isn't capable of producing any further notifications in any case, this doesn't really mean much. (Note that it does not call OnCompleted
on its observers when you Dispose
it.) The one practical effect is that its internal field that keeps track of observers is reset to a special sentinel value indicating that it has been disposed, meaning that the one externally observable effect of "unsubscribing" the observers is that if, for some reason, your code held onto a reference to a Subject<T>
after disposing it, that would no longer keep all the subscribers reachable for GC purposes. If a Subject<T>
remains reachable indefinitely after it is no longer in use, that in itself is effectively a memory leak, but disposal would at least limit the effects: only the Subject<T>
itself would remain reachable, and not all of its subscribers.
Subject<T>
实现了 IDisposable
接口。释放(Dispose)一个 Subject<T>
会将其置于一种状态,在这种状态下,如果你调用它的任何方法,它将抛出异常。文档还将其描述为取消订阅所有观察者,但由于已释放的 Subject<T>
在任何情况下都无法产生进一步的通知,因此这实际上并没有太大意义。(注意,当你释放它时,它不会对其观察者调用 OnCompleted
方法。)一个实际的效果是,它用于跟踪观察者的内部字段被重置为一个特殊的哨兵值,表明它已被释放,这意味着“取消订阅”观察者的唯一外部可观察效果是,如果出于某种原因,你的代码在释放 Subject<T>
后仍然持有对它的引用,那么这不会再使所有订阅者对于垃圾回收(GC)而言都是可达的。如果一个 Subject<T>
在不再使用后仍然无限期地保持可达,那么这本身实际上就是一种内存泄漏,但释放操作至少会限制其影响:只有 Subject<T>
本身会保持可达,而不是它的所有订阅者。
Subject<T>
is the most straightforward subject, but there are other, more specialized ones.
Subject<T>
是最直接的主题类型,但还有其他更专门化的主题类型。
ReplaySubject<T>
Subject<T>
does not remember anything: it immediately distributes incoming values to subscribers. If new subscribers come along, they will only see events that occur after they subscribe. ReplaySubject<T>
, on the other hand, can remember every value it has ever seen. If a new subject comes along, it will receive the complete history of events so far.
Subject<T>
不会记住任何东西:它会立即将传入的值分发给订阅者。如果新的订阅者出现,它们只会看到在它们订阅之后发生的事件。而 ReplaySubject<T>
,则可以记住它曾经看到过的每一个值。如果有新的订阅者加入,它将接收到目前为止的所有事件历史记录。
This is a variation on the first example in the preceding Subject<T>
section. It creates a ReplaySubject<int>
instead of a Subject<int>
. And instead of immediately subscribing twice, it creates an initial subscription, and then a second one only after a couple of values have been emitted.
这是前面 Subject<T>
部分中第一个示例的一个变体。它创建了一个 ReplaySubject<int>
而不是 Subject<int>
。并且,它并没有立即订阅两次,而是首先创建了一个初始订阅,然后在发出几个值之后才进行了第二次订阅。
ReplaySubject<int> s = new(); s.Subscribe(x => Console.WriteLine($"Sub1: {x}")); s.OnNext(1); s.OnNext(2); s.Subscribe(x => Console.WriteLine($"Sub2: {x}")); s.OnNext(3);
This produces the following output:
这会产生以下输出:
Sub1: 1 Sub1: 2 Sub2: 1 Sub2: 2 Sub1: 3 Sub2: 3
As you'd expect, we initially see output only from Sub1
. But when we make the second call to subscribe, we can see that Sub2
also received the first two values. And then when we report the third value, both see it. If this example had used Subject<int>
instead, we would have seen just this output:
正如你所期望的,我们最初只从 Sub1 看到输出。但是,当我们进行第二次订阅调用时,我们可以看到 Sub2 也接收到了前两个值。然后,当我们报告第三个值时,两者都看到了它。如果这个例子使用了 Subject<int>
而不是 ReplaySubject<int>
,我们只会看到这样的输出:
Sub1: 1 Sub1: 2 Sub1: 3 Sub2: 3
There's an obvious potential problem here: if ReplaySubject<T>
remembers every value published to it, we mustn't use it with endless event sources, because it will eventually cause us to run out of memory.
这里有一个明显的潜在问题:如果 ReplaySubject<T>
记住了发布给它的每一个值,那么我们不应该将它用于无限的事件源,因为它最终会导致我们耗尽内存。
ReplaySubject<T>
offers constructors that accept simple cache expiry settings that can limit memory consumption. One option is to specify the maximum number of item to remember. This next example creates a ReplaySubject<T>
with a buffer size of 2:
ReplaySubject<T>
提供了接受简单缓存过期设置的构造函数,这些设置可以限制内存消耗。一个选项是指定要记住的最大项目数。下面的示例创建了一个缓冲区大小为 2 的 ReplaySubject<T>
:
ReplaySubject<int> s = new(2); s.Subscribe(x => Console.WriteLine($"Sub1: {x}")); s.OnNext(1); s.OnNext(2); s.OnNext(3); s.Subscribe(x => Console.WriteLine($"Sub2: {x}")); s.OnNext(4);
Since the second subscription only comes along after we've already produced 3 values, it no longer sees all of them. It only receives the last two values published prior to subscription (but the first subscription continues to see everything of course):
由于第二个订阅仅在我们产生 3 个值之后才出现,因此它不再看到所有这些值。它只接收在订阅之前发布的最后两个值(但第一个订阅当然一直看到所有内容):
Sub1: 1 Sub1: 2 Sub1: 3 Sub2: 2 Sub2: 3 Sub1: 4 Sub2: 4
Alternatively, you can specify a time-based limit by passing a TimeSpan
to the ReplaySubject<T>
constructor.
或者,你也可以通过将 TimeSpan
传递给 ReplaySubject<T>
构造函数来指定基于时间的限制。
BehaviorSubject<T>
Like ReplaySubject<T>
, BehaviorSubject<T>
also has a memory, but it remembers exactly one value. However, it's not quite the same as a ReplaySubject<T>
with a buffer size of 1. Whereas a ReplaySubject<T>
starts off in a state where it has nothing in its memory, BehaviorSubject<T>
always remembers exactly one item. How can that work before we've made our first call to OnNext
? BehaviorSubject<T>
enforces this by requiring us to supply the initial value when we construct it.
与ReplaySubject<T>
类似,BehaviorSubject<T>
也具有记忆功能,但它只记住一个确切的值。然而,它与缓冲区大小为1的ReplaySubject<T>
并不完全相同。ReplaySubject<T>
在其内存中没有任何内容的状态下开始,而BehaviorSubject<T>
则始终记住一个确切的项目。在我们首次调用OnNext
之前,这是如何工作的呢?BehaviorSubject<T>
通过要求我们在构造时提供初始值来确保这一点。
So you can think of BehaviorSubject<T>
as a subject that always has a value available. If you subscribe to a BehaviorSubject<T>
it will instantly produce a single value. (It may then go on to produce more values, but it always produces one right away.) As it happens, it also makes that value available through a property called Value
, so you don't need to subscribe an IObserver<T>
to it just to retrieve the value.
因此,你可以将BehaviorSubject<T>
视为一个总是有一个可用值的主题。如果你订阅了一个BehaviorSubject<T>
,它会立即产生一个单一的值。(之后它可能会继续产生更多的值,但它总是会立即产生一个值。)巧合的是,它还通过名为Value
的属性使该值可访问,因此你无需仅为了检索该值而向其订阅一个IObserver<T>
。
A BehaviorSubject<T>
could be thought of an as observable property. Like a normal property, it can immediately supply a value whenever you ask it. The difference is that it can then go on to notify you every time its value changes. If you're using the ReactiveUI framework (an Rx-based framework for building user interfaces), BehaviourSubject<T>
can make sense as the implementation type for a property in a view model (the type that mediates between your underlying domain model and your user interface). It has property-like behaviour, enabling you to retrieve a value at any time, but it also provides change notifications, which ReactiveUI can handle in order to keep the UI up to date.
BehaviorSubject<T>
可以被看作是一个可观察的属性。就像普通属性一样,当你请求它时,它可以立即提供一个值。不同的是,它随后可以在每次值改变时通知你。如果你在使用 ReactiveUI 框架(一个基于 Rx 的用于构建用户界面的框架),BehaviorSubject<T>
可以作为视图模型中属性的实现类型(在底层域模型和用户界面之间起中介作用的类型)来使用。它具有类似属性的行为,允许你随时检索值,但它还提供了更改通知,ReactiveUI 可以处理这些通知以保持 UI 的更新。
This analogy falls down slightly when it comes to completion. If you call OnCompleted
, it immediately calls OnCompleted
on all of its observers, and if any new observers subscribe, they will also immediately be completed—it does not first supply the last value. (So this is another way in which it is different from a ReplaySubject<T>
with a buffer size of 1.)
当涉及到完成时,这个类比稍微有些站不住脚。如果你调用 OnCompleted
,它会立即对所有观察者调用 OnCompleted
,并且如果任何新的观察者订阅,它们也会立即被标记为已完成——它不会首先提供最后一个值。(所以这是它与缓冲区大小为 1 的 ReplaySubject<T>
之间的另一个不同点。)
Similarly, if you call OnError
, all current observers will receive an OnError
call, and any subsequent subscribers will also receive nothing but an OnError
call.
类似地,如果你调用 OnError
,所有当前的观察者都将收到一个 OnError
调用,并且任何后续的订阅者也将只收到一个 OnError
调用,而不会再收到其他任何内容。
AsyncSubject<T>
AsyncSubject<T>
provides all observers with the final value it receives. Since it can't know which is the final value until OnCompleted
is called, it will not invoke any methods on any of its subscribers until either its OnCompleted
or OnError
method is called. (If OnError
is called, it just forwards that to all current and future subscribers.) You will often use this subject indirectly, because it is the basis of Rx's integration with the await
keyword. (When you await
an observable sequence, the await
returns the final value emitted by the source.)
AsyncSubject<T>
会为所有观察者提供它接收到的最后一个值。由于它不知道哪个是最后一个值,直到 OnCompleted
被调用,所以在它的 OnCompleted
或 OnError
方法被调用之前,它不会在其任何订阅者上调用任何方法。(如果调用 OnError
,它会将其转发给所有当前和未来的订阅者。)你通常会间接使用这个主题,因为它是 Rx 与 await
关键字集成的基础。(当你对一个可观察序列使用 await
时,await
返回的是源发出的最后一个值。)
If no calls were made to OnNext
before OnCompleted
then there was no final value, so it will just complete any observers without providing a value.
如果在OnCompleted
之前没有对OnNext
进行任何调用,那么就没有最终值,因此它将仅在不提供值的情况下完成所有观察者。
In this example no values will be published as the sequence never completes. No values will be written to the console.
在这个例子中,由于序列从未完成,因此不会发布任何值。也不会有任何值被写入到控制台。
AsyncSubject<string> subject = new(); subject.OnNext("a"); subject.Subscribe(x => Console.WriteLine($"Sub1: {x}")); subject.OnNext("b"); subject.OnNext("c");
In this example we invoke the OnCompleted
method so there will be a final value ('c') for the subject to produce:
在这个例子中,我们调用了 OnCompleted
方法,因此主题将产生一个最终值('c'):
AsyncSubject<string> subject = new(); subject.OnNext("a"); subject.Subscribe(x => Console.WriteLine($"Sub1: {x}")); subject.OnNext("b"); subject.OnNext("c"); subject.OnCompleted(); subject.Subscribe(x => Console.WriteLine($"Sub2: {x}"));
This produces the following output:
这会产生以下输出:
Sub1: c
Sub2: c
If you have some potentially slow work that needs to be done when your application starts up, and which needs to be done just once, you might choose an AsyncSubject<T>
to make the results of that work available. Code requiring those results can subscribe to the subject. If the work is not yet complete, they will receive the results as soon as they are available. And if the work has already completed, they will receive it immediately.
如果你的应用程序在启动时需要进行一些可能很慢且只需要执行一次的工作,你可以选择使用 AsyncSubject<T>
来提供这些工作的结果。需要这些结果的代码可以订阅这个subject。如果工作尚未完成,它们将在结果可用时立即接收结果。如果工作已经完成,它们将立即收到结果。
Subject factory 主题工厂
Finally it is worth making you aware that you can also create a subject via a factory method. Considering that a subject combines the IObservable<T>
and IObserver<T>
interfaces, it seems sensible that there should be a factory that allows you to combine them yourself. The Subject.Create(IObserver<TSource>, IObservable<TResult>)
factory method provides just this.
最后,值得让你了解的是,你也可以通过工厂方法来创建一个主题。考虑到主题结合了IObservable<T>
和IObserver<T>
接口,那么存在一个允许你自己将它们组合起来的工厂似乎是合理的。Subject.Create(IObserver<TSource>, IObservable<TResult>)
工厂方法正是提供了这一功能。
// Creates a subject from the specified observer used to publish messages to the // subject and observable used to subscribe to messages sent from the subject public static ISubject<TSource, TResult> Create<TSource, TResult>( IObserver<TSource> observer, IObservable<TResult> observable) {...}
Note that unlike all of the other subjects just discussed, this creates a subject where there is no inherent relationship between the input and the output. This just takes whatever IObserver<TSource>
and IObserver<TResult>
implementations you supply and wraps them up in a single object. All calls made to the subject's IObserver<TSource>
methods will be passed directly to the observer you supplied. If you want values to emerge to subscribers to the corresponding IObservable<TResult>
, it's up to you to make that happen. This really combines the two objects you supply with the absolute minimum of glue.
请注意,与刚才讨论的所有其他subject不同,这种方法创建的subject的输入和输出之间没有固有的关系。它只是接受你提供的任何 IObserver<TSource>
和 IObservable<TResult>
的实现,并将它们封装在一个单独的对象中。对主题的 IObserver<TSource>
方法的所有调用都将直接传递给你提供的观察者。如果你想让值出现在对应 IObservable<TResult>
的订阅者中,那就需要你自己来确保这一点。这实际上只是将你提供的两个对象以最少的连接代码组合在一起。
Subjects provide a convenient way to poke around Rx, and are occasionally useful in production scenarios, but they are not recommended for most cases. An explanation is in the Usage Guidelines appendix. Instead of using subjects, favour the factory methods shown earlier in this chapter.
主题(Subjects)为在响应式编程(Rx)中进行探索提供了一种便捷的方式,并且在生产场景中偶尔也会很有用,但在大多数情况下并不推荐使用它们。有关原因,请参阅“使用指南”附录。不要使用主题,而是应该优先使用本章前面展示的工厂方法。。
Summary 总结
We have looked at the various eager and lazy ways to create a sequence. We have seen how to produce timer based sequences using the various factory methods. And we've also explored ways to transition from other synchronous and asynchronous representations.
我们已经探讨了创建序列的各种积极(eager)和惰性(lazy)方式。我们已经了解了如何使用各种工厂方法来生成基于计时器的序列。此外,我们还探索了如何从其他同步和异步表示形式进行转换的方法。
As a quick recap:
简要概括一下:
-
Factory Methods 工厂方法
- Observable.Return
- Observable.Empty
- Observable.Never
- Observable.Throw
- Observable.Create
- Observable.Defer
-
Generative methods 生成性方法
- Observable.Range
- Observable.Generate
- Observable.Interval
- Observable.Timer
-
Adaptation 适配器
- Observable.Start
- Observable.FromEventPattern
- Task.ToObservable
- Task<T>.ToObservable
- IEnumerable<T>.ToObservable
- Observable.FromAsyncPattern
Creating an observable sequence is our first step to practical application of Rx: create the sequence and then expose it for consumption. Now that we have a firm grasp on how to create an observable sequence, we can look in more detail at the operators that allow us to describe processing to be applied, to build up more complex observable sequences.
