烂翻译系列之Rx.NET介绍第二版——筛选
Rx provides us with tools to take potentially vast quantities of events and process these to produce higher level insights. This can often involve a reduction in volume. A small number of events may be more useful than a large number if the individual events in that lower-volume stream are, on average, more informative. The simplest mechanisms for achieving this involve simply filtering out events we don't want. Rx defines several operators that can do this.
Rx 为我们提供了获取潜在的大量事件并处理这些事件以产生更高层次见解的工具。这通常包括减少数量。如果低容量流中的单个事件平均而言信息量更大,那么少量事件可能比大量事件更有用。实现这一点的最简单机制包括简单地过滤掉我们不需要的事件。Rx 定义了几个可以执行此操作的运算符。
Just before we move on to introducing the new operators, we will quickly define an extension method to help illuminate several of the examples. This Dump
extension method subscribes to any IObservable<T>
with handlers that display messages for each notification the source produces. This method takes a name
argument, which will be shown as part of each message, enabling us to see where events came from in examples that subscribe to more than one source.
在我们继续介绍新运算符之前,我们将快速定义一个扩展方法,以帮助阐明几个示例。这个名为 Dump
的扩展方法用于订阅任何 IObservable<T>
,并使用处理程序显示源产生的每个通知的消息。该方法接受一个 name
参数,该参数将作为每条消息的一部分显示,从而让我们能够在订阅多个源的示例中看到事件来自何处。
public static class SampleExtensions { public static void Dump<T>(this IObservable<T> source, string name) { source.Subscribe( value =>Console.WriteLine($"{name}-->{value}"), ex => Console.WriteLine($"{name} failed-->{ex.Message}"), () => Console.WriteLine($"{name} completed")); } }
Where运算符
Applying a filter to a sequence is an extremely common exercise and the most straightforward filter in LINQ is the Where
operator. As usual with LINQ, Rx provides its operators in the form of extension methods. If you are already familiar with LINQ, the signature of Rx's Where
method will come as no surprise:
对序列应用过滤器是非常常见的操作,而在LINQ中最直接的过滤器是Where运算符。和LINQ的惯例一样,Rx也以其扩展方法的形式提供运算符。如果你已经熟悉LINQ,那么Rx的Where方法的签名对你来说应该不会感到意外:
IObservable<T> Where<T>(this IObservable<T> source, Func<T, bool> predicate)
Note that the element type is the same for the source
parameter as it is for the return type. This is because Where
doesn't modify elements. It can filter some out, but those that it does not remove are passed through unaltered.
请注意,源参数和返回类型的元素类型是相同的。这是因为Where不修改元素。它可以过滤掉一些元素,但它不删除的元素会保持不变地传递出去。
This example uses Where
to filter out all odd values produced from a Range
sequence, meaning only even numbers will emerge.
这个示例使用Where来过滤掉Range序列产生的所有奇数,意味着只有偶数会被保留下来。
IObservable<int> xs = Observable.Range(0, 10); // The numbers 0-9 IObservable<int> evenNumbers = xs.Where(i => i % 2 == 0); evenNumbers.Dump("Where");
Output:
输出:
Where-->0 Where-->2 Where-->4 Where-->6 Where-->8 Where completed
The Where
operator is one of the many standard LINQ operators you'll find on all LINQ providers. LINQ to Objects, the IEnumerable<T>
implementation, provides an equivalent method, for example. In most cases, Rx's operators behave just as they do in the IEnumerable<T>
implementations, although there are some exceptions as we'll see later. We will discuss each implementation and explain any variation as we go. By implementing these common operators Rx also gets language support for free via C# query expression syntax. For example, we could have written the first statement this way, and it would have compiled to effectively identical code:
Where运算符是你在所有LINQ提供者中都会找到的许多标准LINQ运算符之一。例如,LINQ to Objects,即IEnumerable<T>的实现,提供了一个等效的方法。在大多数情况下,Rx的运算符的行为与IEnumerable<T>的实现相同,尽管后面我们会看到一些例外。我们将逐个讨论这些实现并解释任何差异。通过实现这些公共运算符,Rx(Reactive Extensions)还能够免费获得C#查询表达式语法的语言支持。例如,我们本可以以这种方式编写第一个语句,并且它会被编译成效果上完全相同的代码:
IObservable<int> evenNumbers = from i in xs where i % 2 == 0 select i;
The examples in this book mostly use extension methods, not query expressions, partly because Rx implements some operators for which there is no corresponding query syntax, and partly because the method call approach can sometimes make it easier to see what is happening.
本书中的示例主要使用扩展方法而不是查询表达式,部分原因是因为Rx实现了一些没有对应查询语法的运算符,部分原因是因为方法调用的方式有时可以更容易地看出发生了什么。
As with most Rx operators, Where
does not subscribe immediately to its source. (Rx LINQ operators are much like those in LINQ to Objects: the IEnumerable<T>
version of Where
returns without attempting to enumerate its source. It's only when something attempts to enumerate the IEnumerable<T>
that Where
returns that it will in turn start enumerating the source.) Only when something calls Subscribe
on the IObservable<T>
returned by Where
will it call Subscribe
on its source. And it will do so once for each such call to Subscribe
. More generally, when you chain LINQ operators together, each Subscribe
call on the resulting IObservable<T>
results in a cascading series of calls to Subscribe
all the way down the chain.
与大多数Rx运算符一样,Where不会立即订阅其源。(Rx LINQ运算符与LINQ to Objects中的运算符非常相似:IEnumerable<T>版本的Where在尝试枚举其源之前返回。只有当某些东西尝试枚举这个IEnumerable<T>时,Where才会转而开始枚举源。)只有当对Where返回的IObservable<T>调用Subscribe时,它才会对其源调用Subscribe。并且对于每个这样的Subscribe调用,它都会这样做一次。更一般地说,当你将LINQ运算符链接在一起时,对最终生成的IObservable<T>的每个Subscribe调用都会导致在链中一路向下的一系列Subscribe调用。
A side effect of this cascading Subscribe
is that Where
(like most other LINQ operators) is neither inherently hot or cold: since it just subscribes to its source, then it will be hot if its source is hot, and cold if its source is cold.
这种级联Subscribe的一个副作用是,Where(像大多数其他LINQ运算符一样)既不是天生的热源也不是冷源:因为它只是订阅了其源,所以如果它的源是热的,那么它就是热的;如果它的源是冷的,那么它就是冷的。
The Where
operator passes on all elements for which its predicate
callback returns true
. To be more precise, when you subscript to Where
, it will create its own IObserver<T>
which it passes as the argument to source.Subscribe
, and this observer invokes the predicate
for each call to OnNext
. If that predicate returns true
, then and only then will the observer created by Where
call OnNext
on the observer that you passed to Where
.
Where运算符传递其谓词回调返回true的所有元素。更具体地说,当你订阅Where时,它会创建自己的IObserver<T>并将其作为参数传递给source.Subscribe,这个观察者会在每次调用OnNext时调用谓词。如果该谓词返回true,那么并且只有那时,由Where创建的观察者才会调用你传递给Where的观察者的OnNext。
Where
always passes the final call to either OnComplete
or OnError
through. That means that if you were to write this:
Where 总是将最终的 OnComplete 或 OnError 调用传递下去。这意味着如果你写下这样的代码:
IObservable<int> dropEverything = xs.Where(_ => false);
then although this would filter out all elements (because the predicate ignores its argument and always returns false
, instructing Where
to drop everything), this won't filter out an error or completion.
那么,尽管这会过滤掉所有元素(因为谓词忽略了它的参数并始终返回false,指示Where丢弃所有元素),但这不会过滤掉错误或完成信号。
In fact if that's what you want—an operator that drops all the elements and just tells you when a source completes or fails—there's a simpler way.
实际上,如果你想要的是这样一个运算符:它丢弃所有元素,只是告诉你源何时完成或失败,那么有更简单的方法。
IgnoreElements运算符
The IgnoreElements
extension method allows you to receive just the OnCompleted
or OnError
notifications. It is equivalent to using the Where
operator with a predicate that always returns false
, as this example illustrates:
IgnoreElements扩展方法允许你只接收OnCompleted或OnError通知。它相当于使用谓词始终返回false的Where运算符,如下例所示:
IObservable<int> xs = Observable.Range(1, 3); IObservable<int> dropEverything = xs.IgnoreElements(); xs.Dump("Unfiltered"); dropEverything.Dump("IgnoreElements");
As the output shows, the xs
source produces the numbers 1 to 3 then completes, but if we run that through IgnoreElements
, all we see is the OnCompleted
.
如输出所示,xs源产生了数字1到3然后完成,但如果我们通过IgnoreElements运行它,我们只看到OnCompleted。
Unfiltered-->1 Unfiltered-->2 Unfiltered-->3 Unfiltered completed IgnoreElements completed
OfType
Some observable sequences produce items of various types. For example, consider an application that wants to keep track of ships as they move. This is possible with an AIS receiver. AIS is the Automatic Identification System, which most ocean-going ships use to report their location, heading, speed, and other information. There are numerous kinds of AIS message. Some report a ship's location and speed, but its name is reported in a different kind of message. (This is because most ships move more often than they change their names, so they broadcast these two types of information at quite different intervals.)
一些可观察序列会产生各种类型的项。例如,考虑一个想要追踪移动中的船只的应用程序。这可以通过AIS接收器来实现。AIS是自动识别系统(Automatic Identification System),大多数远洋船舶都使用该系统来报告其位置、航向、速度和其他信息。AIS消息有多种类型。一些消息报告船舶的位置和速度,但船名则在另一种类型的消息中报告。(这是因为大多数船舶的移动比船名更改更频繁,所以它们以完全不同的间隔广播这两种类型的信息。)
Imagine how this might look in Rx. Actually you don't have to imagine it. The open source Ais.Net project includes a ReceiverHost
class that makes AIS messages available through Rx. The ReceiverHost
defines a Messages
property of type IObservable<IAisMessage>
. Since AIS defines numerous message types, this observable source can produce many different kinds of objects. Everything it emits will implement the IAisMessage
interface, which reports the ship's unique identifier, but not much else. But the Ais.Net.Models
library defines numerous other interfaces, including IVesselNavigation
, which reports location, speed, and heading, and IVesselName
, which tells you the vessel's name.
想象一下这在Rx中可能会是什么样子。实际上你不需要想象。开源的Ais.Net项目包含一个ReceiverHost类,该类通过Rx提供AIS消息。ReceiverHost定义了一个类型为IObservable<IAisMessage>的Messages属性。由于AIS定义了多种消息类型,这个可观察源可以产生许多不同类型的对象。它发出的一切都将实现IAisMessage接口,该接口报告船舶的唯一标识符,但其他信息不多。但是,Ais.Net.Models库定义了许多其他接口,包括IVesselNavigation,它报告位置、速度和航向,以及IVesselName,它告诉你船的名字。
Suppose you are interested only in the locations of vessels in the water, and you don't care about the vessels' names. You will want to see all messages that implement the IVesselNavigation
interface, and to ignore all those that don't. You could try to achieve this with the Where
operator:
假设你只关心水上船只的位置,而不关心船的名字。你将希望看到所有实现IVesselNavigation接口的消息,并忽略所有不实现该接口的消息。你可以尝试使用Where运算符来实现这一点:
// Won't compile! IObservable<IVesselNavigation> vesselMovements = receiverHost.Messages.Where(m => m is IVesselNavigation);
However, that won't compile. You will get this error:
然而,这不会编译通过。你会遇到这个错误:
Cannot implicitly convert type 'System.IObservable<Ais.Net.Models.Abstractions.IAisMessage>' to 'System.IObservable<Ais.Net.Models.Abstractions.IVesselNavigation>'
Remember that the return type of Where
is always the same as its input. Since receiverHost.Messages
is of type IObservable<IAisMessage>
, that's is also the type that Where
will return. It so happens that our predicate ensures that only those messages that implement IVesselNavigation
make it through, but there's no way for the C# compiler to understand the relationship between the predicate and the output. (For all it knows, Where
might do the exact opposite, including only those elements for which the predicate returns false
. In fact the compiler can't guess anything about how Where
might use its predicate.)
请记住,Where的返回类型总是与其输入类型相同。由于receiverHost.Messages的类型是IObservable<IAisMessage>,因此Where也将返回该类型。我们的谓词确保只有实现IVesselNavigation的消息会通过,但C#编译器无法理解谓词和输出之间的关系。(编译器只知道,Where可能会做完全相反的事情,只包括那些谓词返回false的元素。实际上,编译器无法猜测Where可能会如何使用其谓词。)
Fortunately, Rx provides an operator specialized for this case. OfType
filters items down to just those that are of a particular type. Items must be either the exact type specified, or inherit from it, or, if it's an interface, they must implement it. This enables us to fix the last example:
幸运的是,Rx为此情况提供了一个专门的运算符。OfType将项过滤为仅特定类型的项。这些项必须是指定的确切类型,或者是其派生类,或者是如果它是一个接口,则必须实现它。这使得我们能够修复上一个示例:
IObservable<IVesselNavigation> vesselMovements =
receiverHost.Messages.OfType<IVesselNavigation>();
Positional Filtering 位置过滤
Sometimes, we don't care about what an element is, so much as where it is in the sequence. Rx defines a few operators that can help us with this.
有时,我们并不太关心元素是什么,而是关心它在序列中的位置。Rx 定义了一些符运算来帮助我们处理这种情况。
FirstAsync and FirstOrDefaultAsync FirstAsync 和 FirstOrDefaultAsync
LINQ providers typically implement a First
operator that provides the first element of a sequence. Rx is no exception, but the nature of Rx means we typically need this to work slightly differently. With providers for data at rest (such as LINQ to Objects or Entity Framework Core) the source elements already exist, so retrieving the first item is just a matter of reading it. But with Rx, sources produce data when they choose, so there's no way of knowing when the first item will become available.
LINQ 提供程序通常会实现一个 First 运算符,该运算符提供序列的第一个元素。Rx 也不例外,但 Rx 的性质意味着我们通常需要这种操作稍微有些不同。对于静态数据(如 LINQ to Objects 或 Entity Framework Core)的提供程序,源元素已经存在,因此检索第一个元素只是读取它的问题。但是,对于 Rx,在选择时,源可能没有产生数据,因此无法知道第一个元素何时可用。
So with Rx, we typically use FirstAsync
. This returns an IObservable<T>
that will produce the first value that emerges from the source sequence and will then complete. (Rx does also offer a more conventional First
method, but it can be problematic. See the Blocking Versions of First/Last/Single[OrDefault] section later for details.)
因此,在 Rx 中,我们通常使用 FirstAsync。这将返回一个 IObservable<T>,该对象将产生源序列中出现的第一个值,然后完成。(Rx 也提供了一个更传统的 First 方法,但它可能会遇到问题。有关详细信息,请参阅后文的 First/Last/Single[OrDefault] 的阻塞版本部分。)
For example, this code uses the AIS.NET source introduced earlier to report the first time a particular boat (the aptly named HMS Example, as it happens) reports that it is moving:
例如,这段代码使用了前面介绍的AIS.NET源,来报告一艘特定船只(恰巧名为HMS Example)首次报告它正在移动的情况:
uint exampleMmsi = 235009890; IObservable<IVesselNavigation> moving = receiverHost.Messages .Where(v => v.Mmsi == exampleMmsi) .OfType<IVesselNavigation>() .Where(vn => vn.SpeedOverGround > 1f) .FirstAsync();
As well as using FirstAsync
, this also uses a couple of the other filter elements already described. It starts with a Where
step that filters messages down to those from the one boat we happen to be interested in. (Specifically, we filter based on that boat's Maritime Mobile Service Identity, or MMSI.) Then we use OfType
so that we are looking only at those messages that report how/whether the vessel is moving. Then we use another Where
clause so that we can ignore messages indicating that the boat is not actually moving, finally, we use FirstAsync
so that we get only the first message indicating movement. As soon as the boat moves, this moving
source will emit a single IVesselNavigation
event and will then immediately complete.
除了使用 FirstAsync 之外,此代码还使用了之前描述过的其他几个过滤元素。它首先使用 Where 步骤将消息过滤为我们碰巧感兴趣的那一艘船的消息。(具体来说,我们根据该船的海上移动服务身份(Maritime Mobile Service Identity,简称 MMSI)进行过滤。)然后,我们使用 OfType,以便我们仅查看报告船舶如何/是否移动的消息。然后,我们使用另一个 Where 子句,以便我们可以忽略指示船舶实际上并未移动的消息。最后,我们使用 FirstAsync,以便我们仅获取第一个指示移动的消息。一旦船舶移动,这个 moving
源将发出一个 IVesselNavigation 事件,并立即完成。
We can simplify that query slightly, because FirstAsync
optionally takes a predicate. This enables us to collapse the final Where
and FirstAsync
into a single operator:
我们可以稍微简化这个查询,因为 FirstAsync 可以选择性地接受一个谓词。这使我们能够将最后的 Where 和 FirstAsync 合并为一个运算符:
IObservable<IVesselNavigation> moving = receiverHost.Messages .Where(v => v.Mmsi == exampleMmsi) .OfType<IVesselNavigation>() .FirstAsync(vn => vn.SpeedOverGround > 1f);
What if the input to FirstAsync
is empty? If its completes without ever producing an item, FirstAsync
invokes its subscriber's OnError
, passing an InvalidOperationException
with an error message reporting that the sequence contains no elements. The same is true if we're using the form that takes a predicate (as in this second example), and no elements matching the predicate emerged. This is consistent with the LINQ to Objects First
operator. (Note that we wouldn't expect this to happen with the examples just shown, because the source will continue to report AIS messages for as long as the application is running, meaning there's no reason for it ever to complete.)
如果 FirstAsync
的输入为空会怎样?如果它在不产生任何元素的情况下完成,FirstAsync
会调用其订阅者的 OnError
方法,并传递一个包含错误消息的 InvalidOperationException
,该错误消息会报告序列中不包含任何元素。同样地,如果我们使用的是带有谓词(如第二个示例中所示)的形式,且没有元素匹配该谓词,也会发生这种情况。这与 LINQ to Objects 的 First
运算符的行为是一致的。(请注意,我们不会期望在刚才展示的示例中发生这种情况,因为源会持续报告 AIS 消息,只要应用程序在运行,就意味着没有理由让它完成。)
Sometimes, we might want to tolerate this kind of absence of events. Most LINQ providers offer not just First
but FirstOrDefault
. We can use this by modify the preceding example. This uses the TakeUntil
operator to introduce a cut-off time: this example is prepared to wait for 5 minutes, but gives up after that. (So although the AIS receiver can produce messages endlessly, this example has decided it won't wait forever.) And since that means we might complete without ever seeing the boat move, we've replaced FirstAsync
with FirstOrDefaultAsync
:
有时,我们可能希望容忍这种事件的缺失。大多数 LINQ 提供者不仅提供 First
,还提供 FirstOrDefault
。我们可以通过修改前面的示例来使用它。这里使用 TakeUntil
运算符来引入一个截止时间:本示例准备等待5分钟,但之后就会放弃。(因此,尽管 AIS 接收器可以无限制地产生消息,但本示例已经决定不会无限期地等待。)并且,由于这意味着我们可能会在没有看到船只移动的情况下就完成操作,所以我们已经将 FirstAsync
替换为 FirstOrDefaultAsync
:
IObservable<IVesselNavigation?> moving = receiverHost.Messages .Where(v => v.Mmsi == exampleMmsi) .OfType<IVesselNavigation>() .TakeUntil(DateTimeOffset.Now.AddMinutes(5)) .FirstOrDefaultAsync(vn => vn.SpeedOverGround > 1f);
If, after 5 minutes, we've not seen a message from the boat indicating that it's moving at 1 knot or faster, TakeUntil
will unsubscribe from its upstream source and will call OnCompleted
on the observer supplied by FirstOrDefaultAsync
. Whereas FirstAsync
would treat this as an error, FirstOrDefaultAsync
will produce the default value for its element type (IVesselNavigation
in this case; the default value for an interface type is null
), pass that to its subscriber's OnNext
, and then call OnCompleted
.
如果5分钟后,我们还没有收到来自船只表明其以1节或更快速度移动的消息,TakeUntil
将从其上游源取消订阅,并调用由 FirstOrDefaultAsync
提供的观察者的 OnCompleted
方法。而 FirstAsync
会将此情况视为错误,但 FirstOrDefaultAsync
会为其元素类型生成默认值(在这种情况下为 IVesselNavigation
;接口类型的默认值为 null
),将该值传递给其订阅者的 OnNext
,然后调用 OnCompleted
。
In short, this moving
observable will always produce exactly one item. Either it will produce an IVesselNavigation
indicating that the boat has moved, or it will produce null
to indicate that this didn't happen in the 5 minutes that this code has allowed.
简而言之,这个可观察的移动对象将始终产生恰好一个项目。它要么产生一个 IVesselNavigation
来指示船只已经移动,要么产生 null
来指示在代码允许的5分钟内这种情况没有发生。
This production of a null
might be an OK way to indicate that something didn't happen, but there's something slightly clunky about it: anything consuming this moving
source now has to work out whether a notification signifies the event of interest, or the absence of any such event. If that happens to be convenient for your code, then great, but Rx provides a more direct way to represent the absence of an event: an empty sequence.
产生 null
可能是一种可以接受的方式来表示某事没有发生,但这种方式略显笨拙:任何消耗这个移动源的对象现在都必须判断一个通知是表示感兴趣的事件,还是表示没有任何此类事件的发生。如果这恰好适合你的代码,那当然很好,但 Rx 提供了一种更直接的方式来表示事件的缺失:空序列。
You could imagine a first or empty operator that worked this way. This wouldn't make sense for LINQ providers that return an actual value. For example, as LINQ to Objects' First
returns T
, not IEnumerable<T>
, so there's no way for it to return an empty sequence. But because Rx's offers First
-like operators that return IObservable<T>
, it would be technically possible to have an operator that returns either the first item or no items at all. There is no such operator built into Rx, but we can get exactly the same effect by using a more generalised operator, Take
.
你可以想象一个以这种方式工作的first或empty运算符。这对于返回实际值的 LINQ 提供程序来说是没有意义的。例如,由于 LINQ to Objects 的 First
返回 T
而不是 IEnumerable<T>
,因此它无法返回一个空序列。但是,由于 Rx 提供了返回 IObservable<T>
的类似 First
的运算符,因此从技术上讲,可以有一个运算符返回第一个项目或者不返回任何项目。Rx 本身没有内置这样的运算符,但我们可以通过使用更通用的运算符 Take
来获得完全相同的效果。
Take
Take
is a standard LINQ operator that takes the first few items from a sequence and then discards the rest.
Take
是一个标准的 LINQ 运算符,它从序列中取出前几个项,然后丢弃其余的项。
In a sense, Take
is a generalization of First
: Take(1)
returns only the first item, so you could think of LINQ's First
as being a special case of Take
. That's not strictly correct because these operators respond differently to missing elements: as we've just seen, First
(and Rx's FirstAsync
) insists on receiving at least one element, producing an InvalidOperationException
if you supply it with an empty sequence. Even the more existentially relaxed FirstOrDefault
still insists on producing something. Take
works slightly differently.
从某种意义上说,Take
是 First
的泛化:Take(1)
只返回第一个项,因此你可以将 LINQ 的 First
看作是 Take
的一个特例。但这并不完全正确,因为这些运算符对缺失元素的处理方式不同:正如我们刚才所见,First
(以及 Rx 的 FirstAsync
)坚持要求至少接收到一个元素,如果你为它提供一个空序列,它会产生一个 InvalidOperationException
。即使更加宽松存在的 FirstOrDefault
仍然坚持要产生某些东西。Take
的工作方式略有不同。
If the input to Take
completes before producing as many elements as have been specified, Take
does not complain—it just forwards whatever the source has provided. If the source did nothing other than call OnCompleted
, then Take
just calls OnCompleted
on its observer. If we used Take(5)
, but the source produced three items and then completed, Take(5)
will forward those three items to its subscriber, and will then complete. This means we could use Take
to implement the hypothetical FirstOrEmpty
discussed in the preceding section:
如果 Take
的输入在产生指定数量的元素之前就完成了,Take
并不会报错——它只是转发源所提供的任何内容。如果源除了调用 OnCompleted
之外没有做任何其他事情,那么 Take
就会在其观察者上调用 OnCompleted
。如果我们使用了 Take(5)
,但源只产生了三个项目然后就完成了,Take(5)
会将这三个项目转发给其订阅者,然后完成。这意味着我们可以使用 Take
来实现前一节中讨论的假设的 FirstOrEmpty
:
public static IObservable<T> FirstOrEmpty<T>(this IObservable<T> src) => src.Take(1);
Now would be a good time to remind you that most Rx operators (and all the ones in this chapter) are not intrinsically either hot or cold. They defer to their source. Given some hot source
, source.Take(1)
is also hot. The AIS.NET receiverHost.Messages
source I've been using in these examples is hot (because it reports live message broadcasts from ships), so observable sequences derived from it are also hot. Why is now a good time to discuss this? Because it enables me to make the following absolutely dreadful pun:
现在是一个提醒你的好时机,大多数 Rx 运算符(以及本章中的所有运算符)本质上既不是热的也不是冷的。它们依赖于其源。给定某个热源,source.Take(1)
也是热的。我在这些示例中一直使用的 AIS.NET receiverHost.Messages
源是热的(因为它报告来自船舶的实时消息广播),因此从它派生出的可观察序列也是热的。为什么现在是一个讨论这个问题的好时机?因为这能让我做出以下绝对糟糕的泥球:
IObservable<IAisMessage> hotTake = receiverHost.Messages.Take(1);
Thank you. I'm here all week.
谢谢。我一周都在这里。
The FirstAsync
and Take
operators work from the start of the sequence. What if we're interested only in the tail end?
FirstAsync
和 Take
运算符从序列的开始处开始工作。如果我们只对序列的尾部感兴趣,那该怎么办?
LastAsync, LastOrDefaultAsync, and PublishLast LastAsync, LastOrDefaultAsync, 和 PublishLast
LINQ providers typically provide Last
and LastOrDefault
. These do almost exactly the same thing as First
or FirstOrDefault
except, as the name suggests, they return the final element instead of the first one. As with First
, the nature of Rx means that unlike with LINQ providers working with data at rest, the final element might not be just sitting there waiting to be fetched. So just as Rx offers FirstAsync
and FirstOrDefault
, it offers LastAsync
and LastOrDefaultAsync
. (It does also offer Last
, but again, as the Blocking Versions of First/Last/Single[OrDefault] section discusses, this can be problematic.)
LINQ 提供程序通常会提供 Last
和 LastOrDefault
。这些运算符几乎与 First
或 FirstOrDefault
执行相同的操作,但顾名思义,它们返回的是最后一个元素而不是第一个元素。与 First
一样,Rx 的特性意味着与处理静态数据的 LINQ 提供程序不同,最后一个元素可能并不会那里等待被检索。因此,就像 Rx 提供 FirstAsync
和 FirstOrDefault
一样,它也提供了 LastAsync
和 LastOrDefaultAsync
。(它也提供 Last,但同样,如 First/Last/Single[OrDefault] 的阻塞版本部分所述,这可能会带来问题。)
There is also PublishLast
. This has similar semantics to LastAsync
but it handles multiple subscriptions differently. Each time you subscribe to the IObservable<T>
that LastAsync
returns, it will subscribe to the underlying source, but PublishLast
makes only a single Subscribe
call to the underlying source. (To provide control over exactly when this happens, PublishLast
returns an IConnectableObservable<T>
. As the Hot and Cold Sources section of chapter 2 described, this provides a Connect
method, and the connectable observable returned by PublishLast
subscribes to its underlying source when you call this.) Once this single subscription receives an OnComplete
notification from the source, it will deliver the final value to all subscribers. (It also remembers the final value, so if any new observers subscribe after the final value has been produced, they will immediately receive that value when they subscribe.) The final value is immediately followed by an OnCompleted
notification. This is one of a family of operators based on the Multicast
operator described in more detail in later chapters.
还有 PublishLast
。它与 LastAsync
有类似的语义,但在处理多个订阅时有所不同。每次你订阅 LastAsync
返回的 IObservable<T>
时,它都会订阅底层源,但 PublishLast
只会对底层源进行一次 Subscribe
调用。(为了精确控制这何时发生,PublishLast
返回一个 IConnectableObservable<T>
。如第2章的“热源和冷源”部分所述,这提供了一个 Connect
方法,当你调用它时,PublishLast
返回的 IConnectableObservable<T>
对象会订阅其底层源。)一旦这个唯一的订阅从源接收到 OnCompleted
通知,它就会将最终值传递给所有订阅者。(它还记住最终值,因此,如果最终值产生后有新的观察者订阅,他们在订阅时会立即收到该值。)最终值紧接着是一个 OnCompleted
通知。这是基于Multicast运算
符的一系列操作符之一,更多细节将在后续章节中详细描述。
The distinction between LastAsync
and LastOrDefaultAsync
is the same as with FirstAsync
and FirstOrDefaultAsync
. If the source completes having produced nothing, LastAsync
reports an error, whereas LastOrDefaultAsync
emits the default value for its element type and then completes. PublishLast
handles an empty source differently again: if the source completes without producing any elements, the observable returned by PublishLast
will do the same: it produces neither an error nor a default value in this scenario.
LastAsync
和 LastOrDefaultAsync
之间的区别与 FirstAsync
和 FirstOrDefaultAsync
之间的区别相同。如果源在完成时没有产生任何内容,LastAsync
会报告错误,而 LastOrDefaultAsync
会发出其元素类型的默认值,然后完成。PublishLast
对空源的处理方式又有所不同:如果源在完成时没有产生任何元素,PublishLast
返回的可观察对象也会做同样的处理(即直接完成):在这种情况下,它既不会产生错误也不会发出默认值。
Reporting the final element of a sequence entails a challenge that First
does not face. It's very easy to know when you've received the first element from a source: if the source produces an element, and it hasn't previously produced an element, then that's the first element right there. This means that operators such as FirstAsync
can report the first element immediately. But LastAsync
and LastOrDefaultAsync
don't have that luxury.
报告序列中的最后一个元素面临着一个 First
所不面临的问题。要确定何时从源中收到了第一个元素非常容易:如果源产生了一个元素,并且它之前还没有产生过元素,那么那个元素就是第一个元素。这意味着像 FirstAsync
这样的运算符可以立即报告第一个元素。但是 LastAsync
和 LastOrDefaultAsync
没有这样的优势。
If you receive an element from a source, how do you know that it is the last element? In general, you can't know this at the instant that you receive it. You will only know that you have received the last element when the source goes on to invoke your OnCompleted
method. This won't necessarily happen immediately. An earlier example used TakeUntil(DateTimeOffset.Now.AddMinutes(5))
to bring a sequence to an end after 5 minutes, and if you do that, it's entirely possible that a significant amount of time might elapse between the final element being emitted, and TakeUntil
shutting things down. In the AIS scenario, boats might only emit messages once every few minutes, so it's quite plausible that we could end up with TakeUntil
forwarding a message, and then discovering a few minutes later that the cutoff time has been reached without any further messages coming in. Several minutes could have elapsed between the final OnNext
and the OnComplete
.
如果你从源中接收到一个元素,如何知道它是最后一个元素呢?一般来说,在你接收它的那一刻,你是无法得知这一点的。只有当源继续调用你的 OnCompleted
方法时,你才会知道你已经收到了最后一个元素。而这并不一定会立即发生。之前的一个例子使用了 TakeUntil(DateTimeOffset.Now.AddMinutes(5))
来在5分钟后结束一个序列,如果你这样做,那么在最后一个元素被发出和 TakeUntil
关闭序列之间,很可能会有一段相当长的时间间隔。在AIS(自动识别系统)场景中,船只可能每隔几分钟才发出一次消息,所以我们很有可能遇到这样的情况:TakeUntil
转发了一条消息,然后在几分钟后发现已经达到了截止时间,而之后没有收到任何进一步的消息。在最后一个 OnNext
和 OnComplete
之间,可能已经过去了几分钟。
Because of this. LastAsync
and LastOrDefaultAsync
emit nothing at all until their source completes. This has an important consequence: there might be a significant delay
因此,LastAsync
和 LastOrDefaultAsync
在其源完成之前根本不会发出任何内容。这有一个重要的结果:在 LastAsync
从源接收到最后一个元素并将其转发给订阅者之间,可能会有显著的延迟。
TakeLast
Earlier we saw that Rx implements the standard Take
operator, which forwards up to a specified number of elements from the start of a sequence and then stops. TakeLast
forwards the elements at the end of a sequence. For example, TakeLast(3)
asks for the final 3 elements of the source sequence. As with Take
, TakeLast
is tolerant of sources that produce too few items. If a source produces fewer than 3 items, TaskLast(3)
will just forward the entire sequence.
之前我们了解到,Rx 实现了标准的 Take
运算符,它从序列的开始处转发指定数量的元素,然后停止。而 TakeLast
则转发序列末尾的元素。例如,TakeLast(3)
会请求源序列的最后3个元素。与 Take
一样,TakeLast
对产生元素过少的源具有容错性。如果源产生的元素少于3个,TakeLast(3)
将只会转发整个序列。
TakeLast
faces the same challenge as Last
: it doesn't know when it is near the end of the sequence. It therefore has to hold onto copies of the most recently seen values. It needs memory to hold onto however many values you've specified. If you write TakeLast(1_000_000)
, it will need to allocate a buffer large enough to store 1,000,000 values. It doesn't know if the first element it receives will be one of the final million. It can't know that until either the source completes, or the source has emitted more than 1,000,000 items. When the source finally does complete, TakeLast
will now know what the final million elements were and will need to pass all of them to its subscriber's OnNext
method one after another.
TakeLast
面临着与 Last
相同的挑战:它不知道何时接近序列的末尾。因此,它必须保留最近看到的值的副本。它需要内存来保留你指定的任意数量的值。如果你写 TakeLast(1_000_000)
,它将需要分配一个足够大的缓冲区来存储 1,000,000 个值。它不知道它接收到的第一个元素是否是最终的一百万个元素之一。它无法知道这一点,直到源完成,或者源已经发出了超过 1,000,000 个项。当源最终完成时,TakeLast
现在将知道最后的一百万个元素是什么,并需要将它们全部一个接一个地传递给其订阅者的 OnNext
方法。
Skip and SkipLast Skip 和 SkipLast
What if we want the exact opposite of the Take
or TakeLast
operators? Instead of taking the first 5 items from a source, maybe I want to discard the first 5 items instead? Perhaps I have some IObservable<float>
taking readings from a sensor, and I have discovered that the sensor produces garbage values for its first few readings, so I'd like to ignore those, and only start listening once it has settled down. I can achieve this with Skip(5)
.
如果我们想要 Take
或 TakeLast
运算符的完全相反行为怎么办?比如,不是从源中取前 5 个项目,而是想丢弃前 5 个项目呢?也许我有一个 IObservable<float>
正在从传感器读取数据,并且我发现传感器在前几次读数时会产生垃圾值,所以我想忽略这些值,只在它稳定下来后才开始监听。我可以使用 Skip(5)
来实现这一点。
SkipLast
does the same thing at the end of the sequence: it omits the specified number of elements at the tail end. As with some of the other operators we've just been looking at, this has to deal with the problem that it can't tell when it's near the end of the sequence. It only gets to discover which were the last (say) 4 elements after the source has emitted all of them, followed by an OnComplete
. So SkipLast
will introduce a delay. If you use SkipLast(4)
, it won't forward the first element that the source produces until the source produces a 5th element. So it doesn't need to wait for OnCompleted
or OnError
before it can start doing things, it just has to wait until its certain that an element is not one of the ones we want to discard.
SkipLast
在序列的末尾执行相同的操作:它省略尾部指定数量的元素。就像我们刚才查看的一些其他操作符一样,这必须处理它无法判断何时接近序列末尾的问题。它只有在源发出所有元素之后,并且紧接着是 OnComplete
时,才能发现哪些是最后(比如)4 个元素。因此,SkipLast
会引入延迟。如果你使用 SkipLast(4)
,它不会转发源产生的第一个元素,直到源产生了第 5 个元素。所以它不需要等待 OnCompleted
或 OnError
才能开始执行操作,它只需要等待直到它确定某个元素不是我们想要丢弃的元素之一。
The other key methods to filtering are so similar I think we can look at them as one big group. First we will look at Skip
and Take
. These act just like they do for the IEnumerable<T>
implementations. These are the most simple and probably the most used of the Skip/Take methods. Both methods just have the one parameter; the number of values to skip or to take.
过滤的其他关键方法非常相似,我认为我们可以将它们视为一个大类。首先,我们来看 Skip
和 Take
。它们的行为与 IEnumerable<T>
实现中的行为完全一致。这些是最简单且可能是最常用的 Skip/Take 方法。这两个方法都只有一个参数,即要跳过或要获取的值的数量。
SingleAsync and SingleOrDefaultAsync SingleAsync 和 SingleOrDefaultAsync
LINQ operators typically provide a Single
operator, for use when a source should provide exactly one item, and it would be an error for it to contain more, or for it to be empty. The same Rx considerations apply here as for First
and Last
, so you will probably be unsurprised to learn that Rx offers a SingleAsync
method that returns an IObservable<T>
that will either call its observer's OnNext
exactly once, or will call its OnError
to indicate either that the source reported an error, or that the source did not produce exactly one item.
LINQ运算符通常提供一个 Single
运算符,用于当源应该提供且仅提供一个项目时,如果源包含多个项目或为空,则视为错误。Rx在这里的考虑与 First
和 Last
相同,因此你可能会对Rx提供了一个 SingleAsync
方法感到不足为奇,该方法返回一个 IObservable<T>
,它将精确调用其观察者的 OnNext
一次,或者调用其 OnError
来指示源报告了错误,或者源没有精确产生一个项目。
With SingleAsync
, you will get an error if the source is empty, just like with FirstAsync
and LastAsync
, but you will also get an error if the source contains multiple items. There is a SingleOrDefault
which, like its first/last counterparts, tolerates an empty input sequence, generating a single element with the element type's default value in that case.
与 FirstAsync
和 LastAsync
一样,如果源为空,SingleAsync
将导致错误,但如果源包含多个项目,也会导致错误。有一个 SingleOrDefault
,就像它的 First
/Last
对应项一样,它容忍空输入序列,在这种情况下,将生成一个包含元素类型默认值的单个元素。
Single
and SingleAsync
share with Last
and LastAsync
the characteristic that they don't initially know when they receive an item from the source whether it should be the output. That may seem odd: since Single
requires the source stream to provide just one item, surely it must know that the item it will deliver to its subscriber will be the first item it receives. This is true, but the thing it doesn't yet know when it receives the first item is whether the source is going to produce a second one. It can't forward the first item unless and until the source completes. We could say that SingleAsync
's job is to first verify that the source contains exactly one item, and then to forward that item if it does, but to report an error if it does not. In the error case, SingleAsync
will know it has gone wrong if it ever receives a second item, so it can immediately call OnError
on its subscriber at that point. But in the success scenario, it can't know that all is well until the source confirms that nothing more is coming by completing. Only then will SingleAsync
emit the result.
Single
和 SingleAsync
与 Last
和 LastAsync
共享一个特性,即它们在最初从源接收到一个项目时,并不知道该项目是否应该作为输出。这可能看起来有些奇怪:由于 SingleAsync
要求源流仅提供一个项目,它当然必须知道它将传递给订阅者的项目是它接收到的第一个项目。这是事实,但当它接收到第一个项目时,它还不知道的是源是否会生成第二个项目。除非源完成,否则它不能转发第一个项目。我们可以说, SingleAsync
的工作首先是验证源是否恰好包含一个项目,如果是,则转发该项目,如果不是,则报告错误。在错误情况下,如果 SingleAsync
接收到第二个项目,它将知道出了问题,因此它可以在此时立即调用其订阅者的`OnError`。但在成功的情况下,它只有在源通过完成确认没有更多项目到来时,才能知道一切正常。只有到那时, SingleAsync
才会发出结果。
Blocking Versions of First/Last/Single[OrDefault] First/Last/Single[OrDefault] 的阻塞版本
Several of the operators described in the preceding sections end in the name Async
. This is a little strange because normally, .NET methods that end in Async
return a Task
or Task<T>
, and yet these all return an IObservable<T>
. Also, as already discussed, each of these methods corresponds to a standard LINQ operator which does not generally end in Async
. (And to further add to the confusion, some LINQ providers such as Entity Framework Core do include Async
versions of some of these operators, but they are different. Unlike Rx, these do in fact return a Task<T>
, so they still produce a single value, and not an IQueryable<T>
or IEnumerable<T>
.) This naming arises from an unfortunate choice early in Rx's design.
前面几节中描述的多个运算符以 Async
结尾。这有点奇怪,因为通常以 Async
结尾的.NET方法会返回一个 Task
或 Task<T>
,而这些方法都返回一个 IObservable<T>
。此外,正如已经讨论过的,这些方法中的每一个都对应于一个标准的LINQ运算符,而这些运算符通常不以 Async
结尾。(更令人困惑的是,一些LINQ提供程序,如Entity Framework Core,确实包含其中一些运算符的 Async
版本,但它们不同。与Rx不同,这些 Async
版本实际上返回一个 Task<T>
,因此它们仍然产生单个值,而不是 IQueryable<T>
或 IEnumerable<T>
。)这种命名源于Rx设计早期的一个不幸选择。
If Rx were being designed from scratch today, the relevant operators in the preceding section would just have the normal names: First
, and FirstOrDefault
, and so on. The reason they all end with Async
is that these were added in Rx 2.0, and Rx 1.0 had already defined operators with those names. This example uses the First
operator:
如果 Rx 是从今天开始从头设计的,那么前一节中提到的相关运算符将只使用常规名称:First、FirstOrDefault 等。它们都以 Async
结尾的原因是这些运算符是在 Rx 2.0 中添加的,而 Rx 1.0 已经定义了具有这些名称的运算符。以下示例使用了 First
运算符:
int v = Observable.Range(1, 10).First(); Console.WriteLine(v);
This prints out the value 1
, which is the first item returned by Range
here. But look at the type of that variable v
. It's not an IObservable<int>
, it's just an int
. What would happen if we used this on an Rx operator that didn't immediately produce values upon subscription? Here's one example:
这会打印出值 1,这是 Range
在这里返回的第一个项目。但是,看看变量 v
的类型。它不是 IObservable<int>
,而只是 int
。如果我们在一个 Rx 运算符上使用它,而这个运算符在订阅时不会立即产生值,会发生什么?这里有一个例子:
long v = Observable.Timer(TimeSpan.FromSeconds(2)).First(); Console.WriteLine(v);
If you run this, you'll find that the call to First
doesn't return until a value is produced. It is a blocking operator. We typically avoid blocking operators in Rx, because it's easy to create deadlocks with them. The whole point of Rx is that we can create code that reacts to events, so to just sit and wait until a specific observable source produces a value is not really in the spirit of things. If you find yourself wanting to do that, there are often better ways to achieve the results you're looking for. (Or perhaps Rx isn't good model for whatever you're doing.)
如果你运行这段代码,你会发现 First
的调用在产生一个值之前不会返回。它是一个阻塞运算符。我们通常在Rx中避免使用阻塞运算符,因为很容易用它们创建死锁。Rx的全部意义在于我们可以创建对事件做出反应的代码,因此只是坐着等待直到特定的可观察源产生一个值并不符合其精神。如果你发现自己想要这样做,通常有更好的方法来实现你想要的结果。(或者,Rx可能并不适合你正在做的事情的模型。)
If you really do need to wait for a value like this, it might be better to use the Async
forms in conjunction with Rx's integrated support for C#'s async
/await
:
如果你确实需要像这样等待一个值,那么最好结合 Rx 对 C# 的 async/await 的集成支持来使用异步形式:
long v = await Observable.Timer(TimeSpan.FromSeconds(2)).FirstAsync(); Console.WriteLine(v);
This logically has the same effect, but because we're using await
, this won't block the calling thread while it waits for the observable source to produce a value. This might reduce the chances of deadlock.
这在逻辑上有相同的效果,但因为我们在使用 await,所以在等待可观察源产生值的过程中,它不会阻塞调用线程。这可能会减少死锁的机会。
The fact that we're able to use await
makes some sense of the fact that these methods end with Async
, but you might be wondering what's going on here. We've seen that these methods all return IObservable<T>
, not Task<T>
, so how are we able to use await
? There's a full explanation in the Leaving Rx's World chapter, but the short answer is that Rx provides extension methods that enable this to work. When you await
an observable sequence, the await
will complete once the source completes, and it will return the final value that emerges from the source. This works well for operators such as FirstAsync
and LastAsync
that produce exactly one item.
我们能够使用 await 使得这些方法以 Async 结尾是有道理的,但你可能想知道这里发生了什么。我们已经看到这些方法都返回 IObservable<T>
而不是 Task<T>
,那么我们是如何能够使用 await 的呢?在“离开 Rx 的世界”一章中有一个完整的解释,但简短的回答是 Rx 提供了扩展方法使这成为可能。当你对一个可观察序列使用 await 时,一旦源完成,await 就会完成,并且它会返回从源中产生的最终值。这对于像 FirstAsync
和 LastAsync
这样正好产生一个项目的运算符来说效果很好。
Note that there are occasionally situations in which values are available immediately. For example, the BehaviourSubject<T>
section in chapter 3, showed that the defining feature of BehaviourSubject<T>
is that it always has a current value. That means that Rx's First
method won't actually block—it will subscribe to the BehaviourSubject<T>
, and BehaviourSubject<T>.Subscribe
calls OnNext
on its subscriber's observable before returning. That enables First
to return a value immediately without blocking. (Of course, if you use the overload of First
that accepts a predicate, and if the BehaviourSubject<T>
's value doesn't satisfy the predicate, First
will then block.)
需要注意的是,偶尔会有一些情况下值可以立即获得。例如,第3章中的 BehaviourSubject<T>
部分展示了 BehaviourSubject<T>
的定义特性是它总是有一个当前值。这意味着Rx的 First
方法实际上不会阻塞——它会订阅 BehaviourSubject<T>
,而 BehaviourSubject<T>.Subscribe
在返回之前会调用其订阅者的`OnNext`。这使得 First
能够立即返回一个值而不阻塞。(当然,如果你使用接受谓词的 First
重载,并且如果 BehaviourSubject<T>
的值不满足谓词, First
将会阻塞。)
ElementAt
There is yet another standard LINQ operator for selecting one particular element from the source: ElementAt
. You provide this with a number indicating the position in the sequence of the element you require. In data-at-rest LINQ providers, this is logically equivalent to accessing an array element by index. Rx implements this operator, but whereas most LINQ providers' ElementAt<T>
implementation returns a T
, Rx's returns an IObservable<T>
. Unlike with First
, Last
, and Single
, Rx does not provide a blocking form of ElementAt<T>
. But since you can await any IObservable<T>
, you can always do this:
从源中选择一个特定元素的另一个标准 LINQ 运算符是 ElementAt
。你向它提供一个数字,该数字指示所需元素在序列中的位置。在静态数据 LINQ 提供程序中,这在逻辑上等同于通过索引访问数组元素。Rx 实现了这个运算符,但与大多数 LINQ 提供程序的 ElementAt<T>
实现返回 T
不同,Rx 返回的是 IObservable<T>
。与 First
、Last
和 Single
不同,Rx 没有提供 ElementAt<T>
的阻塞形式。但是,由于你可以await任何 IObservable<T>
,你总是可以这样做:
IAisMessage fourth = await receiverHost.Message.ElementAt(4);
If your source sequence only produces five values and we ask for ElementAt(5)
, the sequence that ElementAt
returns will report an ArgumentOutOfRangeException
error to its subscriber when the source completes. There are three ways we can deal with this:
如果你的源序列只产生五个值,而我们请求 ElementAt(5)
,那么当源完成时,ElementAt
返回的序列将向它的订阅者报告一个 ArgumentOutOfRangeException
错误。我们可以通过三种方式处理这个问题:
- Handle the OnError gracefully 优雅地处理
OnError
- Use
.Skip(5).Take(1);
This will ignore the first 5 values and the only take the 6th value. If the sequence has less than 6 elements we just get an empty sequence, but no errors. 使用.Skip(5).Take(1)
;这将忽略前 5 个值,并只取第 6 个值。如果序列的元素少于 6 个,我们只会得到一个空序列,但不会出错。 - Use
ElementAtOrDefault
使用ElementAtOrDefault
ElementAtOrDefault
extension method will protect us in case the index is out of range, by pushing the default(T)
value. Currently there is not an option to provide your own default value.
ElementAtOrDefault
扩展方法将在索引超出范围时保护我们,通过推送 default(T)
值。目前,没有提供自定义默认值的选项。
Temporal Filtering 时间过滤
The Take
and TakeLast
operators let us filter out everything except elements either at the very start or very end (and Skip
and SkipLast
let us see everything but those), but these all require us to know the exact number of elements. What if we want to specify the cut-off not in terms of an element count, but in terms of a particular instant in time?
Take
和 TakeLast
操作符允许我们过滤掉除了最开始或最末尾的元素之外的所有元素(而 Skip
和 SkipLast
允许我们看到除了这些元素之外的所有元素),但这些都需要我们知道确切的元素数量。如果我们想根据特定的时间点而不是元素数量来指定截断点,那该怎么办呢?
In fact you've already seen one example: earlier I used TakeUntil
to convert an endless IObservable<T>
into one that would complete after five minutes. This is one of a family of operators.
事实上,你已经看到过一个例子:之前我使用 TakeUntil
将一个无限的 IObservable<T>
转换为一个在五分钟后完成的 IObservable<T>
。这是这一系列运算符中的一个。
SkipWhile and TakeWhile SkipWhile 和 TakeWhile
In the Skip
and SkipLast
section, I described a sensor that produces garbage values for its first few readings. This is quite common. For example, gas monitoring sensors often need to get some component up to a correct operating temperature before they can produce accurate readings. In the example in that section, I used Skip(5)
to ignore the first few readings, but that is a crude solution. How do we know that 5 is enough? Or might it be ready sooner, in which case 5 is too few.
在“Skip 和 SkipLast”部分中,我描述了一个传感器,它在最初的几次读数中会产生垃圾值。这是相当常见的。例如,气体监测传感器通常需要在能够产生准确读数之前将其某个组件加热到正确的操作温度。在该部分的示例中,我使用了 Skip(5)
来忽略最初的几次读数,但这是一种粗糙的解决方案。我们怎么知道 5 次就足够了呢?或者它可能更快地准备就绪,在这种情况下 5 次又太多了。
What we really want to do is discard readings until we know the readings will be valid. And that's exactly the kind of scenario that SkipWhile
can be useful for. Suppose we have a gas sensor that reports concentrations of some particular gas, but which also reports the temperature of the sensor plate that is performing the detection. Instead of hoping that 5 readings is a sensible number to skip, we could express the actual requirement:
我们真正想做的是丢弃读数,直到我们知道这些读数是有效的。这正是 SkipWhile
可以派上用场的场景。假设我们有一个气体传感器,它可以报告某种特定气体的浓度,并且还可以报告执行检测的传感器板的温度。与其希望跳过 5 次读数是合理的,我们可以表达实际的需求:
const int MinimumSensorTemperature = 74; IObservable<SensorReading> readings = sensor.RawReadings .SkipUntil(r => r.SensorTemperature >= MinimumSensorTemperature);
This directly expresses the logic we require: this will discard readings until the device is up to its minimum operating temperature.
这直接表达了我们所需的逻辑:这将丢弃读数,直到设备达到其最低工作温度。
The next set of methods allows you to skip or take values from a sequence while a predicate evaluates to true. For a SkipWhile
operation this will filter out all values until a value fails the predicate, then the remaining sequence can be returned.
下一组方法允许你在谓词评估为 true 时从序列中跳过或取出值。对于 SkipWhile
操作,它将过滤掉所有值,直到一个值不满足谓词,然后可以返回剩余的序列。
var subject = new Subject<int>(); subject .SkipWhile(i => i < 4) .Subscribe(Console.WriteLine, () => Console.WriteLine("Completed")); subject.OnNext(1); subject.OnNext(2); subject.OnNext(3); subject.OnNext(4); subject.OnNext(3); subject.OnNext(2); subject.OnNext(1); subject.OnNext(0); subject.OnCompleted();
Output:
输出:
4 3 2 1 0 Completed
TakeWhile
will return all values while the predicate passes, and when the first value fails the sequence will complete.
TakeWhile
将在谓词通过时返回所有值,而当第一个值失败时,序列将完成。
var subject = new Subject<int>(); subject .TakeWhile(i => i < 4) .Subscribe(Console.WriteLine, () => Console.WriteLine("Completed")); subject.OnNext(1); subject.OnNext(2); subject.OnNext(3); subject.OnNext(4); subject.OnNext(3); subject.OnNext(2); subject.OnNext(1); subject.OnNext(0); subject.OnCompleted();
Output:
输出:
1 2 3 Completed
SkipUntil and TakeUntil SkipUntil 和 TakeUntil
In addition to SkipWhile
and TakeWhile
, Rx defines SkipUntil
and TakeUntil
. These may sound like nothing more than an alternate expression of the same idea: you might expect SkipUntil
to do almost exactly the same thing as SkipWhile
, with the only difference being that SkipWhile
runs for as long as its predicate returns true
, whereas SkipUntil
runs for as long as its predicate returns false
. And there is an overload of SkipUntil
that does exactly that (and a corresponding one for TakeUntil
). If that's all these were they wouldn't be interesting. However, there are overloads of SkipUntil
and TakeUntil
that enable us to do things we can't do with SkipWhile
and TakeWhile
.
除了 SkipWhile
和 TakeWhile
之外,Rx 还定义了 SkipUntil
和 TakeUntil
。这些听起来可能只是同一想法的另一种表达:你可能会期望 SkipUntil
几乎与 SkipWhile
做完全相同的事情,唯一的区别是 SkipWhile
在其谓词返回 true 时运行,而 SkipUntil
在其谓词返回 false 时运行。并且确实有一个 SkipUntil
的重载版本正是这样做的(TakeUntil
也有一个对应的重载版本)。如果它们仅此而已,那它们就不会那么有趣了。然而,SkipUntil
和 TakeUntil
的重载版本使我们能够做一些 SkipWhile
和 TakeWhile
无法做到的事情。
You've already seen one example. The FirstAsync
and FirstOrDefaultAsync
included an example that used an overload of TakeUntil
that accepted a DateTimeOffset
. This wraps any IObservable<T>
, returning an IObservable<T>
that will forward everything from the source until the specified time, at which point it will immediately complete (and will unsubscribe from the underlying source).
你已经看过一个例子了。FirstAsync
和 FirstOrDefaultAsync
中包含了一个使用 TakeUntil
重载版本的示例,该版本接受一个 DateTimeOffset
。这会将任何 IObservable<T>
包装起来,返回一个 IObservable<T>
,该 IObservable<T>
将从源转发所有内容,直到指定的时间,此时它将立即完成(并将从基础源取消订阅)。
We couldn't have achieved this with TakeWhile
, because that consults its predicate only when the source produces an item. If we want the source to complete at a specific time, the only way we could do that with TakeWhile
is if its source happens to produce an item at the exact moment we wanted to finish. TakeWhile
will only ever complete as a result of its source producing an item. TakeUntil
can complete asynchronously. If we specified a time 5 minutes into the future, it doesn't matter if the source is completely idle when that time arrives. TakeUntil
will complete anyway. (It relies on Schedulers to be able to do this.)
我们无法使用 TakeWhile
来实现这一点,因为 TakeWhile
仅在源产生项时才咨询其谓词。如果我们希望源在特定时间完成,那么使用 TakeWhile
实现的唯一方式就是源恰好在我们要完成的确切时刻产生了一个项。TakeWhile
只有在源产生项时才会完成。而 TakeUntil
可以异步完成。如果我们指定了一个未来 5 分钟的时间点,那么当这个时间点到来时,源是否完全空闲并不重要。TakeUntil
会无论如何都完成。(它依赖于调度器来做到这一点。)
We don't have to use a time, TakeUntil
offers an overload that accept a second IObservable<T>
. This enables us to tell it to stop when something interesting happens, without needing to know in advance exactly when that will occur. This overload of TakeUntil
forwards items from the source until that second IObservable<T>
produces a value. SkipUntil
offers a similar overload in which the second IObservable<T>
determines when it should start forwarding items from the source.
我们不必使用时间,TakeUntil
提供了一个接受第二个 IObservable<T>
的重载版本。这使我们能够告诉它在发生有趣的事情时停止,而无需提前确切知道何时会发生。这个 TakeUntil
的重载版本会从源转发项,直到第二个 IObservable<T>
产生一个值。SkipUntil
提供了一个类似的重载版本,其中第二个 IObservable<T>
确定了何时开始从源转发项。
Note: these overloads require the second observable to produce a value in order to trigger the start or end. If that second observable completes without producing a single notification, then it has no effect—TakeUntil
will continue to take items indefinitely; SkipUntil
will never produce anything. In other words, these operators would treat Observable.Empty<T>()
as being effectively equivalent to Observable.Never<T>()
.
注意:这些重载版本需要第二个可观察对象产生一个值来触发开始或结束。如果第二个可观察对象完成而没有产生任何通知,那么它将不会产生任何效果——TakeUntil
将继续无限期地接收项;SkipUntil
将永远不会产生任何内容。换句话说,这些运算符会将 Observable.Empty<T>()
视为与 Observable.Never<T>()
在效果上等价。
Distinct and DistinctUntilChanged Distinct 和 DistinctUntilChanged
Distinct
is yet another standard LINQ operator. It removes duplicates from a sequence. To do this, it needs to remember all the values that its source has ever produced, so that it can filter out any items that it has seen before. Rx includes an implementation of Distinct
, and this example uses it to display the unique identifier of vessels generating AIS messages, but ensuring that we only display each such identifier the first time we see it:
Distinct
是另一个标准的 LINQ 运算符。它从序列中移除重复项。为了做到这一点,它需要记住源产生的所有值,以便过滤掉之前已经看到的任何项。Rx 包含了 Distinct
的实现,本示例使用它来显示生成 AIS 消息的船只的唯一标识符,但确保我们仅在第一次看到这样的标识符时显示它:
IObservable<uint> newIds = receiverHost.Messages .Select(m => m.Mmsi) .Distinct(); newIds.Subscribe(id => Console.WriteLine($"New vessel: {id}"));
(This is leaping ahead a little—it uses Select
, which we'll get to in the Transformation of Sequences chapter. However, this is a very widely used LINQ operator, so you are probably already familiar with it. I'm using it here to extract just the MMSI—the vessel identifier—from the message.)
(这稍微超前了一点——它使用了 Select
,我们将在序列的转换章节中介绍它。然而,这是一个非常广泛使用的 LINQ 运算符,所以你可能已经对它很熟悉了。我在这里使用它来仅从消息中提取 MMSI——即船只标识符。)
This example is fine if we are only interested in vessels' identifiers. But what if we want to inspect the detail of these messages? How can we retain the ability to see messages only for vessels we've never previously heard of, but still be able to look at the information in those message? The use of Select
to extract the id stops us from doing this. Fortunately, Distinct
provides an overload enabling us to change how it determines uniqueness. Instead of getting Distinct
to look at the values it is processing, we can provide it with a function that lets us pick whatever characteristics we like. So instead of filtering the stream down to values that have never been seen before, we can instead filter the stream down to values that have some particular property or combination of properties we've never seen before. Here's a simple example:
如果我们只对船只的标识符感兴趣,那么这个例子是可以的。但是,如果我们想检查这些消息的详细信息怎么办?我们如何保持只查看之前从未听说过的船只的消息的能力,但同时仍然能够查看这些消息中的信息?使用 Select
来提取 ID 阻止了我们这样做。幸运的是,Distinct
提供了一个重载,使我们能够改变它确定唯一性的方式。我们不是让 Distinct
查看它正在处理的值,而是可以提供一个函数,让我们选择我们喜欢的任何特性。因此,我们不是将流过滤为以前从未见过的值,而是可以将流过滤为具有我们以前从未见过的特定属性或属性组合的值。下面是一个简单的例子:
IObservable<IAisMessage> newVesselMessages =
receiverHost.Messages.Distinct(m => m.Mmsi);
Here, the input to Distinct
is now an IObservable<IAisMessage>
. (In the preceding example it was actually IObservable<uint>
, because the Select
clause picked out just the MMSI.) So Distinct
now receives the entire IAisMessage
each time the source emits one. But because we've supplied a callback, it's not going try and compare entire IAisMessage
messages with one another. Instead, each time it receives one, it passes that to our callback, and then looks at the value our callback returns, and compares that with the values the callback returned for all previously seen messages, and lets the message through only if that's new.
在这里,Distinct
的输入现在是 IObservable<IAisMessage>
。(在前面的例子中,它实际上是 IObservable<uint>
,因为 Select
子句只选择了 MMSI。)所以 Distinct
现在每次在源发出消息时都会接收到整个 IAisMessage
。但是,因为我们提供了一个回调,所以它不会尝试将整个 IAisMessage
消息相互比较。相反,每次接收到一个消息时,它都会将该消息传递给我们的回调,然后查看回调返回的值,并将该值与回调为所有先前看到的消息返回的值进行比较,只有在新值的情况下才允许消息通过。
So the effect is similar to before. Messages will be allowed through only if they have an MMSI not previously seen. But the difference is that the Distinct
operator's output here is IObservable<IAisMessage>
, so when Distinct
lets an item through, the entire original message remains available.
因此,效果与之前类似。只有当消息具有以前未见过的 MMSI 时,才会允许其通过。但不同的是,这里的 Distinct
运算符的输出是 IObservable<IAisMessage>
,所以当 Distinct
允许一个项通过时,整个原始消息仍然可用。
In addition to the standard LINQ Distinct
operator, Rx also provides DistinctUntilChanged
. This only lets through notifications when something has changed, which it achieved by filtering out only adjacent duplicates. For example, given the sequence 1,2,2,3,4,4,5,4,3,3,2,1,1
it would produce 1,2,3,4,5,4,3,2,1
. Whereas Distinct
remembers every value ever produced, DistinctUntilChanged
remembers only the most recently emitted value, and filters out new values if and only if they match that most recent value.
除了标准的 LINQ Distinct
运算符外,Rx 还提供了 DistinctUntilChanged
。它只在有变化时才允许通知通过,这是通过仅过滤掉相邻的重复项来实现的。例如,给定序列 1,2,2,3,4,4,5,4,3,3,2,1,1,DistinctUntilChanged
将产生 1,2,3,4,5,4,3,2,1。而 Distinct
会记住所有曾经产生的值,DistinctUntilChanged
只记住最近发出的值,并且仅当新值与最近的值匹配时才过滤掉它们。
This example uses DistinctUntilChanged
to detect when a particular vessel reports a change in NavigationStatus
.
这个例子使用 DistinctUntilChanged
来检测特定船只何时报告了 NavigationStatus
的变化。
uint exampleMmsi = 235009890; IObservable<IAisMessageType1to3> statusChanges = receiverHost.Messages .Where(v => v.Mmsi == exampleMmsi) .OfType<IAisMessageType1to3>() .DistinctUntilChanged(m => m.NavigationStatus) .Skip(1);
For example, if the vessel had repeatedly been reporting a status of AtAnchor
, DistinctUntilChanged
would drop each such message because the status was the same as it had previously been. But if the status were to change to UnderwayUsingEngine
, that would cause DistinctUntilChanged
to let the first message reporting that status through. It would then not allow any further messages through until there was another change in value, either back to AtAnchor
, or to something else such as Moored
. (The Skip(1)
on the end is there because DistinctUntilChanged
always lets through the very first message it sees. We have no way of knowing whether that actually represents a change in status, but it is very likely not to: ships report their status every few minutes, but they change that status much less often, so the first time we receive a report of a ship's status, it probably doesn't represent a change of status. By dropping that first item, we ensure that statusChanges
provides notifications only when we can be certain that the status changed.)
例如,如果船只反复报告的状态为“锚泊”(AtAnchor),DistinctUntilChanged
将丢弃这些消息,因为状态与之前相同。但是,如果状态更改为“使用发动机航行”(UnderwayUsingEngine),那么 DistinctUntilChanged
将允许第一个报告该状态的消息通过。然后,在值再次更改之前(无论是改回“锚泊”还是其他状态,如“系泊”(Moored)),它将不允许任何其他消息通过。(末尾的 Skip(1)
是因为 DistinctUntilChanged
总是允许它看到的第一个消息通过。我们无法确定这是否真正代表了状态的更改,但实际上很可能不是:船只每几分钟报告一次状态,但它们更改状态的频率要低得多,所以当我们第一次收到一艘船的状态报告时,它很可能并不代表状态的更改。通过丢弃第一个项,我们确保 statusChanges
仅在我们可以确定状态已更改时才提供通知。)
That was our quick run through of the filtering methods available in Rx. While they are relatively simple, as we have already begun to see, the power in Rx is down to the composability of its operators.
这就是我们快速浏览的 Rx 中可用的过滤方法。正如我们已经开始看到的那样,虽然它们相对简单,但 Rx 的强大之处在于其运算符的可组合性。
The filter operators are your first stop for managing the potential deluge of data we can face in this information-rich age. We now know how to apply various criteria to remove data. Next, we will move on to operators that can transform data.
过滤运算符是我们在这个信息丰富的时代管理潜在的数据洪流的第一站。现在我们已经知道如何应用各种标准来删除数据。接下来,我们将继续学习可以转换数据的运算符。
