烂翻译系列之Rx.NET介绍第二版——序列转换
The values from the sequences we consume are not always in the format we need. Sometimes there is more information than we need, and we need to pick out just the values of interest. Sometimes each value needs to be expanded either into a richer object or into more values.
我们使用的序列中的值并不总是以我们需要的格式出现。有时包含的信息比我们需要的多,我们需要挑选出仅我们感兴趣的值。有时,每个值都需要扩展成一个更丰富的对象或更多的值。
Up until now, we have looked at creation of sequences, transition into sequences, and, the reduction of sequences by filtering. In this chapter we will look at transforming sequences.
到目前为止,我们已经探讨了序列的创建、转换为序列,以及通过过滤对序列进行缩减。在本章中,我们将探讨序列的转换。
Select 操作
The most straightforward transformation method is Select
. It allows you provide a function that takes a value of TSource
and return a value of TResult
. The signature for Select
reflects its ability to transform a sequence's elements from one type to another type, i.e. IObservable<TSource>
to IObservable<TResult>
.
最直接的转换方法是 Select
。它允许你提供一个函数,该函数接受一个 TSource
类型的值并返回一个 TResult
类型的值。 Select
的签名反映了它将序列的元素从一种类型转换为另一种类型的能力,即 IObservable<TSource>
转换为 IObservable<TResult>
。
IObservable<TResult> Select<TSource, TResult>( this IObservable<TSource> source, Func<TSource, TResult> selector)
You don't have to change the type—TSource
and TResult
can be the same if you want. This first example transforms a sequence of integers by adding 3, resulting in another sequence of integers.
你不需要改变类型——如果你愿意,TSource
和 TResult
可以是相同的。第一个示例通过给整数序列的每个元素加3来进行转换,从而得到另一个整数序列。
IObservable<int> source = Observable.Range(0, 5); source.Select(i => i+3) .Dump("+3")
This uses the Dump
extension method we defined at the start of the Filtering chapter. It produces the following output:
这使用了我们在“过滤”章节开头定义的 Dump
扩展方法。它产生以下输出:
+3 --> 3 +3 --> 4 +3 --> 5 +3 --> 6 +3 --> 7 +3 completed
This next example transforms values in a way that changes their type. It converts integer values to characters.
接下来的这个示例通过改变类型的方式来转换值。它将整数值转换为字符。
Observable.Range(1, 5); .Select(i => (char)(i + 64)) .Dump("char");
Output:
输出:
char --> A char --> B char --> C char --> D char --> E char completed
This example transforms our sequence of integers to a sequence where the elements have an anonymous type:
此示例将整数序列转换为一个元素为匿名类型的序列:
Observable.Range(1, 5) .Select(i => new { Number = i, Character = (char)(i + 64) }) .Dump("anon");
Output:
输出:
anon --> { Number = 1, Character = A } anon --> { Number = 2, Character = B } anon --> { Number = 3, Character = C } anon --> { Number = 4, Character = D } anon --> { Number = 5, Character = E } anon completed
Select
is one of the standard LINQ operators supported by C#'s query expression syntax, so we could have written that last example like this:
Select
是 C# 查询表达式语法支持的标准 LINQ 运算符之一,因此我们也可以将上一个示例写成这样:
var query = from i in Observable.Range(1, 5) select new {Number = i, Character = (char) (i + 64)}; query.Dump("anon");
In Rx, Select
has another overload, in which the selector
function takes two values. The additional argument is the element's index in the sequence. Use this method if the index of the element in the sequence is important to your selector function.
在 Rx 中,Select
还有一个重载,其中selector
函数接受两个值。额外的参数是序列中元素的索引。如果序列中元素的索引对 selector
函数很重要,请使用此方法。
SelectMany 操作
Whereas Select
produces one output for each input, SelectMany
enables each input element to be transformed into any number of outputs. To see how this can work, let's first look at an example that uses just Select
:
虽然 Select
为每个输入产生一个输出,但 SelectMany
允许每个输入元素被转换成任意数量的输出。为了了解这是如何工作的,让我们首先看一个仅使用 Select
的示例:
Observable .Range(1, 5) .Select(i => new string((char)(i+64), i)) .Dump("strings");
which produces this output:
这将产生以下输出:
strings-->A strings-->BB strings-->CCC strings-->DDDD strings-->EEEEE strings completed
As you can see, for each of the numbers produced by Range
, our output contains a string whose length is that many characters. What if, instead of transforming each number into a string, we transformed it into an IObservable<char>
. We can do that just by adding .ToObservable()
after constructing the string:
如您所见,对于 Range
生成的每个数字,我们的输出都包含一个字符串,其长度与这些数字相对应。但是,如果我们不是将每个数字转换为字符串,而是将其转换为 IObservable<char>
应该怎样做呢?我们只需在构造字符串后增加 .ToObservable()
即可实现:
Observable .Range(1, 5) .Select(i => new string((char)(i+64), i).ToObservable()) .Dump("sequences");
(Alternatively, we could have replaced the selection expression with i => Observable.Repeat((char)(i+64), i)
. Either has exactly the same effect.) The output isn't terribly useful:
(或者,我们也可以用 i => Observable.Repeat((char)(i+64), i)
来替换 selection 表达式。两种方式效果完全相同。)但输出结果并不是特别有用:
strings-->System.Reactive.Linq.ObservableImpl.ToObservableRecursive`1[System.Char] strings-->System.Reactive.Linq.ObservableImpl.ToObservableRecursive`1[System.Char] strings-->System.Reactive.Linq.ObservableImpl.ToObservableRecursive`1[System.Char] strings-->System.Reactive.Linq.ObservableImpl.ToObservableRecursive`1[System.Char] strings-->System.Reactive.Linq.ObservableImpl.ToObservableRecursive`1[System.Char] strings completed
We have an observable sequence of observable sequences. But look at what happens if we now replace that Select
with a SelectMany
:
我们得到一个可观察序列的可观察序列。但是,如果我们现在用 SelectMany
来替换 Select
,看看会发生什么:
Observable .Range(1, 5) .SelectMany(i => new string((char)(i+64), i).ToObservable()) .Dump("chars");
This gives us an IObservable<char>
, with this output:
这给我们一个 IObservable<char>
,输出如下:
chars-->A chars-->B chars-->B chars-->C chars-->C chars-->D chars-->C chars-->D chars-->E chars-->D chars-->E chars-->D chars-->E chars-->E chars-->E chars completed
The order has become a little scrambled, but if you look carefully you'll see that the number of occurrences of each letter is the same as when we were emitting strings. There is just one A
, for example, but C
appears three times, and E
five times.
顺序有点混乱,但如果你仔细看,你会发现每个字母的出现次数与我们发出字符串时相同。例如,只有一个A
,但 C
出现了三次, E
出现了五次。
SelectMany
expects the transformation function to return an IObservable<T>
for each input, and it then combines the result of those back into a single result. The LINQ to Objects equivalent is a little less chaotic. If you were to run this:
SelectMany
要求转换函数为每个输入返回一个 IObservable<T>
,随后将这些结果合并为单个输出流。LINQ to Objects 中的等效实现则更为直观有序。若运行以下代码:
Enumerable .Range(1, 5) .SelectMany(i => new string((char)(i+64), i)) .ToList()
it would produce a list with these elements:
它会生成一个包含这些元素的列表:
[ A, B, B, C, C, C, D, D, D, D, E, E, E, E, E ]
The order is less odd. It's worth exploring the reasons for this in a little more detail.
顺序看起来不那么奇怪了。值得更详细地探讨一下其中的原因。
IEnumerable<T>
vs. IObservable<T>
SelectMany
IEnumerable<T>
对比IObservable<T>
SelectMany
IEnumerable<T>
is pull based—sequences produce elements only when asked. Enumerable.SelectMany
pulls items from its sources in a very particular order. It begins by asking its source IEnumerable<int>
(the one returned by Range
in the preceding example) for the first value. SelectMany
then invokes our callback, passing this first item, and then enumerates everything in the IEnumerable<char>
our callback returns. Only when it has exhausted this does it ask the source (Range
) for a second item. Again, it passes that second item to our callback and then fully enumerates the IEnumerable<char>
, we return, and so on. So we get everything from the first nested sequence first, then everything from the second, etc.
IEnumerable<T>
是基于拉取(pull-based)的模型——序列只有在被请求时才会生成元素。Enumerable.SelectMany
以一种非常特定的顺序从源中拉取元素进行处理。其工作流程如下:
-
获取首个源元素:它首先从源
IEnumerable<int>
(例如前例中Range
返回的序列)中拉取第一个值。 -
执行回调并遍历嵌套序列:将第一个元素传递给
SelectMany
的回调函数,然后完全遍历回调返回的IEnumerable<char>
中的所有元素。 -
逐次处理后续元素:只有当第一个元素的嵌套序列完全遍历完毕后,才会从源(
Range
)中拉取第二个元素,重复上述过程:传递该元素给回调,并完全遍历其返回的IEnumerable<char>
。 -
结果顺序:因此,输出序列会先包含第一个嵌套序列的所有元素,接着是第二个嵌套序列的所有元素,依此类推。
Enumerable.SelectMany
is able to proceed in this way for two reasons. First, the pull-based nature of IEnumerable<T>
enables it to decide on the order in which it processes things. Second, with IEnumerable<T>
it is normal for operations to block, i.e., not to return until they have something for us. When the preceding example calls ToList
, it won't return until it has fully populated a List<T>
with all of the results.
Enumerable.SelectMany
能够以这种方式进行有两个原因。首先,IEnumerable<T>
的基于拉取的特性使其能够决定处理元素的顺序。其次,对于 IEnumerable<T>
来说,操作通常是阻塞式的,即操作必须完成所有工作后才返回结果。当前面的示例调用 ToList
时,它会阻塞当前线程,直到遍历完整个序列并完全填充 List<T>
后才会返回。
Rx is not like that. First, consumers don't get to tell sources when to produce each item—sources emit items when they are ready to. Second, Rx typically models ongoing processes, so we don't expect method calls to block until they are done. There are some cases where Rx sequences will naturally produce all of their items very quickly and complete as soon as they can, but the kinds of information sources that we tend to want model with Rx typically don't behave that way. So most operations in Rx do not block—they immediately return something (such as an IObservable<T>
, or an IDisposable
representing a subscription) and will then produce values later.
Rx并不是这样的。首先,消费者无法控制数据源何时生成每个项——数据源会在它们准备好时主动发出项。其次,Rx通常用于对持续进行的过程建模,因此我们不会期望方法调用会一直阻塞直到操作完成。虽然有些情况下Rx序列可能会很快生成所有项并立即完成,但通常我们使用Rx建模的那些信息源类型并不会以这种方式运作。因此,Rx中的大多数操作都不会阻塞——它们会立即返回某些内容(例如一个 IObservable<T>
,或表示订阅关系的 IDisposable
),然后在后续阶段才会产生值。
The Rx version of the example we're currently examining is in fact one of these unusual cases where each of the sequences emits items as soon as it can. Logically speaking, all of the nested IObservable<char>
sequences are in progress concurrently. The result is a mess because each of the observable sources here attempts to produce every element as quickly as the source can consume them. The fact that they end up being interleaved has to do with the way these kinds of observable sources use Rx's scheduler system, which we will describe in the Scheduling and Threading chapter. Schedulers ensure that even when we are modelling logically concurrent processes, the rules of Rx are maintained, and observers of the output of SelectMany
will only be given one item at a time. The following marble diagram shows the events that lead to the scrambled output we see:
我们当前正在研究的这个示例的Rx版本,实际上属于一种特殊情况:每个嵌套的 IObservable<char>
序列都会在就绪后立即发射元素。从逻辑上看,所有这些嵌套的可观察序列都在并发执行。最终结果显得混乱,因为这里的每个被观察源都试图以最快速度产生元素(只要消费者能够处理)。这些元素之所以最终会交错出现,与这类可观察源使用Rx调度器系统的方式有关(我们将在《调度与线程》章节详细讨论)。调度器的存在确保了即使我们在建模逻辑上并发的进程时,仍能维护Rx的核心规则—— SelectMany
运算符的输出,观察者每次只会收到一个元素。下图所示的弹珠图展示了导致我们所见乱序输出的具体事件流:
We can make a small tweak to prevent the child sequences all from trying to run at the same time. (This also uses Observable.Repeat
instead of the rather indirect route of constructing a string
and then calling ToObservable
on that. I did that in earlier examples to emphasize the similarity with the LINQ to Objects example, but you wouldn't really do it that way in Rx.)
我们可以稍作调整,防止所有嵌套的子序列同时运行。(这里还使用了 Observable.Repeat
方法,而不是通过先构造字符串再对其调用 ToObservable
的间接方式。我在之前的示例中采用后者是为了强调与LINQ to Objects示例的相似性,但在实际使用Rx时,通常不会采用这种写法。)
Observable .Range(1, 5) .SelectMany(i => Observable.Repeat((char)(i+64), i) .Delay(TimeSpan.FromMilliseconds(i * 100))) .Dump("chars");
Now we get output consistent with the IEnumerable<T>
version:
现在我们得到的输出与 IEnumerable<T>
版本的输出一致:
chars-->A chars-->B chars-->B chars-->C chars-->C chars-->C chars-->D chars-->D chars-->D chars-->D chars-->E chars-->E chars-->E chars-->E chars-->E chars completed
This clarifies that SelectMany
lets you produce a sequence for each item that the source produces, and to have all of the items from all of those new sequences flattened back out into one sequence that contains everything. While that might make it easier to understand, you wouldn't want to introduce this sort of delay in reality purely for the goal of making it easier to understand. These delays mean it will take about a second and a half for all the elements to emerge. This marble diagram shows that the code above produces a sensible-looking ordering by making each child observable produce a little bunch of items, and we've just introduced dead time to get the separation:
这说明, SelectMany
允许你为源序列生成的每个元素生成一个新的序列,并将所有这些新序列中的元素全部扁平化合并成一个包含所有内容的单一序列。虽然这种设计可能更易于理解,但在实际应用中,你通常不会纯粹为了提升可读性而引入此类延迟。这些延迟意味着所有元素需要大约一秒半的时间才能全部呈现出来。此弹珠图展示了上述代码通过让每个子可观察对象生成一小批元素(并刻意引入空档时间来区隔不同批次),最终形成了一个看似合理的顺序结构。
I introduced these gaps purely to provide a slightly less confusing example, but if you really wanted this sort of strictly-in-order handling, you wouldn't use SelectMany
in this way in practice. For one thing, it's not completely guaranteed to work. (If you try this example, but modify it to use shorter and shorter timespans, eventually you reach a point where the items start getting jumbled up again. And since .NET is not a real-time programming system, there's actually no safe timespan you can specific here that guarantees the ordering.) If you absolutely need all the items from the first child sequence before seeing any from the second, there's actually a robust way to ask for that:
我引入这些间隔纯粹是为了提供一个稍微不那么令人困惑的示例,但如果你确实需要这种严格按顺序处理的方式,那么在实践中实际上不应以这种方式使用 SelectMany
。首先,这种方式并不能完全保证有效。(如果你尝试这个示例,但修改它以使用越来越短的时间间隔,最终会达到某个临界点,元素会再次开始混乱。由于 .NET 并非实时编程系统,实际上你无法指定任何安全的时间间隔来保证顺序。)如果你必须确保在接收到第二个子序列的任何元素之前,先完整获取第一个子序列的所有元素,实际上存在一种可靠的方法可以实现这一点:
Observable .Range(1, 5) .Select(i => Observable.Repeat((char)(i+64), i)) .Concat()) .Dump("chars");
However, that would not have been a good way to show what SelectMany
does, since this no longer uses it. (It uses Concat
, which will be discussed in the Combining Sequences chapter.) We use SelectMany
either when we know we're unwrapping a single-valued sequence, or when we don't have specific ordering requirements, and want to take elements as and when they emerge from child observables.
然而,这并不能很好地展示 SelectMany
的作用,因为这种方法不再使用它(这里使用的是 Concat
,该运算符将在《组合序列》章节中讨论)。我们使用 SelectMany
通常有两种情况:当明确知道需要解包一个单值序列时,或者当没有特定顺序要求、希望按子可观察对象产生元素的自然顺序获取元素时。
The Significance of SelectMany
SelectMany的意义
If you've been reading this book's chapters in order, you had already seen two examples of SelectMany
in earlier chapters. The first example in the LINQ Operators and Composition section of chapter 2 used it. Here's the relevant code:
如果你已经按顺序阅读了这本书的章节,那么你应该已经在前面的章节中看到过 SelectMany
的两个例子。第二章“LINQ 运算符和组合”部分中的第一个例子就使用了它。以下是相关代码:
IObservable<int> onoffs = from _ in src from delta in Observable.Return(1, scheduler) .Concat(Observable.Return(-1, scheduler) .Delay(minimumInactivityPeriod, scheduler)) select delta;
(If you're wondering where the call to SelectMany
is in that, remember that if a Query Expression contains two from
clauses, the C# compiler turns those into a call to SelectMany
.) This illustrates a common pattern in Rx, which might be described as fanning out, and then back in again.
(如果你想知道其中对 SelectMany
的调用在哪里,请记住,如果查询表达式包含两个 from
子句,C#编译器会将它们转换为对 SelectMany
的调用。)这说明了Rx中的一个常见模式,可以描述为先展开再合并。
As you may recall, this example worked by creating a new, short-lived IObservable<int>
for each item produced by src
. (These child sequences, represented by the delta
range variable in the example, produce the value 1
, and then after the specified minimumActivityPeriod
, they produce -1
. This enabled us to keep count of the number of recent events emitted.) This is the fanning out part, where items in a source sequence produce new observable sequences. SelectMany
is crucial in these scenarios because it enables all of those new sequences to be flattened back out into a single output sequence.
正如你可能还记得的,这个示例的工作原理是为源序列( src
)生成的每个项创建一个新的、短暂存在的IObservable<int>
。(这些子序列在示例中由delta 范围变量表示,它们会先产生值1,然后在指定的 minimumActivityPeriod
时间后产生-1。这使我们能够持续统计最近发出的事件数量。)这就是所谓的"展开"(fanning out)部分——源序列中的每个项都会生成新的可观测序列。 SelectMany
在这些场景中至关重要,因为它能够将所有新生成的序列重新"扁平化",合并回单一的输出序列中。
The second place I used SelectMany
was slightly different: it was the final example of the Representing Filesystem Events in Rx section in chapter 3. Although that example also combined multiple observable sources into a single observable, that list of observables was fixed: there was one for each of the different events from FileSystemWatcher
. It used a different operator Merge
(which we'll get to in Combining Sequences), which was simpler to use in that scenario because you just pass it the list of all the observables you'd like to combine. However, because of a few other things this code wanted to do (including deferred startup, automated disposal, and sharing a single source when multiple subscribers were active), the particular combination of operators used to achieve this meant our merging code that returned an IObservable<FileSystemEventArgs>
, needed to be invoked as a transforming step. If we'd just used Select
, the result would have been an IObservable<IObservable<FileSystemEventArgs>>
. The structure of the code meant that it would only ever produce a single IObservable<FileSystemEventArgs>
, so the double-wrapped type would be rather inconvenient. SelectMany
is very useful in these scenarios. If composition of operators has introduced an extra layer of observables-in-observables that you don't want, SelectMany
can unwrap one layer for you.
我使用 SelectMany
的第二个场景略有不同:它出现在第3章"用Rx表示文件系统事件"部分的最后一个示例中。虽然那个示例同样需要将多个可观察源合并为单个可观察序列,但那里的可观察列表是固定的——每个FileSystemWatcher
的不同事件都对应一个可观察对象。该示例使用了不同的运算符 Merge
(我们将在“合并序列”章节详细讨论),由于只需要传入所有待合并的可观察列表,这在当时场景中使用更为简单。不过,由于这段代码还需要实现其他功能(包括延迟启动、自动释放资源,以及在有多个订阅者时共享单一事件源),为实现这些功能而特别组合的运算符,意味着我们需要将返回 IObservable<FileSystemEventArgs>
的合并代码作为转换步骤来调用。如果直接使用 Select
,结果将会是 IObservable<IObservable<FileSystemEventArgs>>
这种嵌套结构。由于代码逻辑设计上只会生成单个 IObservable<FileSystemEventArgs>
,这种双重包装的类型会带来诸多不便。此时SelectMany
就展现出它的优势——当操作符的组合导致产生多余的可观察对象嵌套层级时,SelectMany
能自动为你解包一层嵌套结构。
These two cases—fanning out then back in, and removing or avoiding a layer of observables of observables—come up quite often, which makes SelectMany
an important method. (It's not surprising that I was unable to avoid using it in earlier examples.)
这两个案例——先展开再合并,以及移除或避免可观察对象的多层嵌套——在实践中非常常见,这使得 SelectMany 成为一个至关重要的方法。(难怪在之前的示例中我始终无法避免使用它。)
As it happens, SelectMany
is also a particularly important operator in the mathematical theory that Rx is based on. It is a fundamental operator, in the sense that it is possible to build many other Rx operators with it. Section 'Recreating other operators with SelectMany
' in Appendix D shows how you can implement Select
and Where
using SelectMany
.
碰巧的是,SelectMany
也是 Rx 所基于的数学理论中一个特别重要的运算符。从某种意义上说,它是一个基础运算符,因为你可以用它来构建许多其他的 Rx 运算符。附录 D 中的“使用 SelectMany 重新创建其他运算符”部分展示了如何使用 SelectMany
来实现 Select
和 Where
。
Cast 运算
C#'s type system is not omniscient. Sometimes we might know something about the type of the values emerging from an observable source that is not reflected in that source's type. This might be based on domain-specific knowledge. For example, with the AIS messages broadcast by ships, we might know that if the message type is 3, it will contain navigation information. That means we could write this:
C#的类型系统并非全知全能。有时,我们可能知道从某个可观察源产生的值的类型相关的某些信息,但这些信息并未在该源的类型中体现。这可能基于特定领域的知识。例如,对于船舶广播的AIS消息,我们可能知道如果消息类型为3,则该消息将包含导航信息。这意味着我们可以编写这样的代码:
IObservable<IVesselNavigation> type3 = receiverHost.Messages.Where(v => v.MessageType == 3) .Cast<IVesselNavigation>();
This uses Cast
, a standard LINQ operator that we can use whenever we know that the items in some collection are of some more specific type than the type system has been able to deduce.
这使用了 Cast
运算符(标准 LINQ 操作符),当我们明确知道某个集合中的元素实际类型比类型系统推断的类型更为具体时,就可以使用它进行类型转换。
The difference between Cast
and the OfType
operator shown in chapter 5 is the way in which they handle items that are not of the specified type. OfType
is a filtering operator, so it just filters out any items that are not of the specified type. But with Cast
(as with a normal C# cast expression) we are asserting that we expect the source items to be of the specified type, so the observable returned by Cast
will invoke its subscriber's OnError
if its source produces an item that is not compatible with the specified type.
Cast
和第5章中介绍的 OfType
运算符之间的区别在于它们处理不属于指定类型的元素的方式。OfType
是一个过滤运算符,因此它只会过滤掉所有不属于指定类型的元素。但使用 Cast
时(就像普通的C#强制转换表达式),我们是在断言我们期望源元素都属于指定类型,因此如果 Cast
运算符的源产生与指定类型不兼容的元素,它返回的可观察序列将会调用订阅者的 OnError
方法。
This distinction might be easier to see if we recreate the functionality of Cast
and OfType
using other more fundamental operators.
如果我们使用其他更基础的运算符来重新实现 Cast
和 OfType
的功能,可能会更容易看出它们之间的区别。
// source.Cast<int>(); is equivalent to source.Select(i => (int)i); // source.OfType<int>(); source.Where(i => i is int).Select(i => (int)i);
Materialize and Dematerialize Materialize 和 Dematerialize
The Materialize
operator transforms a source of IObservable<T>
into one of type IObservable<Notification<T>>
. It will provide one Notification<T>
for each item the source produces, and, if the sourced terminates, it will produce one final Notification<T>
indicating whether it completed successfully or with an error.
Materialize
运算符将 IObservable<T>
源转换为 IObservable<Notification<T>>
类型的对象。它会为源产生的每个数据项生成一个 Notification<T>
通知对象,且当源序列终止时(无论是正常完成还是出错),它都会生成一个最终的 Notification<T>
通知对象,指示其终止状态(成功完成或错误终止)。
This can be useful because it produces objects that describe a whole sequence. If you wanted to record the output of an observable in a way that could later be replayed...well you'd probably use a ReplaySubject<T>
because it is designed for precisely that job. But if you wanted to be able to do something other than merely replaying the sequence—inspecting the items or maybe even modifying them before replying, you might want to write your own code to store items. Notification<T>
can be helpful because it enables you to represent everything a source does in a uniform way. You don't need to store information about whether or how the sequence terminates separately—this information is just the final Notification<T>
.
这很有用,因为它能生成描述整个序列的对象。如果您想以可重放的方式记录可观察对象的输出...您很可能会使用 ReplaySubject<T>,因为它正是为此场景设计的。但如果您希望不仅能重放序列,还能在重放前检查数据项甚至进行修改,您可能需要编写自定义代码来存储元素。此时 Notification<T>
将非常有用,因为它能让您以统一方式表示源对象的所有操作——您无需单独存储序列是否终止或如何终止的信息,这些信息都能通过最终的 Notification<T>
来体现。
You could imagine using this in conjunction with ToArray
in a unit test. This would enable you to get an array of type Notification<T>[]
containing a complete description of everything the source did, making it easy to write tests that ask, say, what the third item to emerge from the sequence was. (The Rx.NET source code itself uses Notification<T>
in many of its tests.)
你可以想象在单元测试中将此与 ToArray
结合使用。这将使你能够获得一个类型为 Notification<T>[]
的数组,其中包含对数据源所有操作的完整描述,从而轻松编写诸如"验证序列中第三个元素是什么"这类测试用例。(实际上,Rx.NET源代码本身就在其许多测试中使用了 Notification<T>
。)
If we materialize a sequence, we can see the wrapped values being returned.
若我们对序列进行物化(Materialize)操作,就能观察到被封装后的值被返回。
Observable.Range(1, 3) .Materialize() .Dump("Materialize");
Output:
输出:
Materialize --> OnNext(1) Materialize --> OnNext(2) Materialize --> OnNext(3) Materialize --> OnCompleted() Materialize completed
Note that when the source sequence completes, the materialized sequence produces an 'OnCompleted' notification value and then completes. Notification<T>
is an abstract class with three implementations:
请注意,当源序列完成时,物化后的序列会产生一个“OnCompleted”通知值,然后完成。Notification<T>
是一个抽象类,有三个实现:
- OnNextNotification
- OnErrorNotification
- OnCompletedNotification
Notification<T>
exposes four public properties to help you inspect it: Kind
, HasValue
, Value
and Exception
. Obviously only OnNextNotification
will return true for HasValue
and have a useful implementation of Value
. Similarly, OnErrorNotification
is the only implementation that will have a value for Exception
. The Kind
property returns an enum
which allows you to know which methods are appropriate to use.
Notification<T>
公开了四个公共属性来帮助你检查它:Kind
、HasValue
、Value
和 Exception
。显然,只有 OnNext 类型的通知(OnNextNotification
)会为 HasValue
返回 true
并且其 Value
属性会包含有效值。类似地,OnError类型的通知(OnErrorNotification
)是唯一具有 Exception
值的实现。Kind
属性返回一个 NotificationKind枚举,通过它您可以判断当前通知的类型(例如 OnNext、OnError 或 OnCompleted),从而确定哪些属性和方法是适用的。
public enum NotificationKind { OnNext, OnError, OnCompleted, }
In this next example we produce a faulted sequence. Note that the final value of the materialized sequence is an OnErrorNotification
. Also that the materialized sequence does not error, it completes successfully.
在接下来的示例中,我们会创建一个故障序列(faulted sequence)。需要注意的是,物化后的序列的最后一个元素将是OnError类型的通知( OnErrorNotification
)。同时,尽管原序列发生了错误,物化后的序列本身并不会抛出异常,而是会正常完成。
var source = new Subject<int>(); source.Materialize() .Dump("Materialize"); source.OnNext(1); source.OnNext(2); source.OnNext(3); source.OnError(new Exception("Fail?"));
Output:
输出:
Materialize --> OnNext(1) Materialize --> OnNext(2) Materialize --> OnNext(3) Materialize --> OnError(System.Exception) Materialize completed
Materializing a sequence can be very handy for performing analysis or logging of a sequence. You can unwrap a materialized sequence by applying the Dematerialize
extension method. The Dematerialize
will only work on IObservable<Notification<TSource>>
.
对序列进行物化(Materialize)操作,可以非常方便地对其进行分析或日志记录。若需将物化后的序列还原为原始事件流,只需调用 Dematerialize
扩展方法即可。需注意, Dematerialize
方法仅适用于 IObservable<Notification<TSource>>
类型的序列(即已被物化的序列)。
This completes our tour of the transformation operators. Their common characteristic is that they produce an output (or, in the case of SelectMany
, a set of outputs) for each input item. Next we will look at the operators that can combine information from multiple items in their source.
至此,我们完成了对转换运算符的探讨。它们的共同特点是:每个输入项都会生成一个输出项(对于 SelectMany
而言,可能生成多个输出项)。接下来,我们将介绍那些能够组合多个源数据项信息的运算符(例如聚合、合并等操作)。
