简体   繁体   English

如何实现“更好的”Finally Rx 运算符?

[英]How to implement a "better" Finally Rx operator?

Recently I become aware that the Rx Finally operator behaves in a way which, at least for me, is unexpected.最近我意识到 Rx Finally运算符的行为方式,至少对我来说,是出乎意料的。 My expectation was that any error thrown by the finallyAction would be propagated to the operator's observers downstream.我的期望是finallyAction抛出的任何错误都会传播到下游的操作员观察者。 Alas this is not what happens.唉,这不是发生的事情。 In the reality the operator first propagates the completion (or the failure) of the antecedent sequence to its observers, and then invokes the action , at a point in time when it's not possible to propagate a potential error thrown by the action.在现实操作者首先传播先行序列及其观察员的完成(或失败),然后调用action ,在时间点时,它不可能通过传播动作抛出一个潜在的错误。 So it throws the error on the ThreadPool , and crashes the process.所以它会在ThreadPool上抛出错误,并使进程崩溃。 Which is not only unexpected, but also highly problematic.这不仅出乎意料,而且非常成问题。 Below is a minimal demonstration of this behavior:以下是此行为的最小演示:

Observable
    .Timer(TimeSpan.FromMilliseconds(100))
    .Finally(() => throw new ApplicationException("Oops!"))
    .Subscribe(_ => { }, ex => Console.WriteLine(ex.Message),
        () => Console.WriteLine("Completed"));

Thread.Sleep(1000);

Outcome: Unhandled exception ( Fiddle )结果:未处理的异常(小提琴

The exception thrown by the Finally lambda is not handled by the Subscribe : onError handler, as it would be desirable.被抛出的异常Finally拉姆达没有被处理的SubscribeonError处理程序,因为这将是可取的。

This feature (I am tempted to call it a flaw) limits severely the usefulness of the Finally operator in my eyes.此功能(我很想把它称为一个缺陷)限制狠狠的实用性Finally运营商在我的眼前。 Essentially I can only use it when I want to invoke an action that is expected to never fail, and if it fails it would indicate a catastrophic corruption of the application's state, when no recovery is possible.从本质上讲,我只能在我想调用一个预期永远不会失败的操作时使用它,如果它失败,则表明应用程序的状态发生了灾难性的损坏,而无法恢复。 I could use it for example to Release a SemaphoreSlim (like I've done here for example), which can only fail if my code has a bug.例如,我可以使用它来Release SemaphoreSlim (就像我在这里所做的那样),只有在我的代码有错误时才会失败。 I am OK with my app crashing in this case.在这种情况下,我的应用程序崩溃我没问题。 But I've also used it recently to invoke an unknown action supplied by the caller, an action that could potentially fail, and crashing the app in this case is unacceptable.但我还用它最近调用调用者提供一个未知的动作,即有可能失败的动作,撞毁在这种情况下应用程序是不能接受的。 Instead, the error should be propagated downstream.相反,错误应该向下游传播。 So what I am asking here is how to implement a Finally variant (let's call it FinallySafe ) with identical signature, and the behavior specified below:那么我问这里是如何实现Finally的变体(我们称之为FinallySafe )具有相同的签名,下面指定的行为:

public static IObservable<TSource> FinallySafe<TSource>(
    this IObservable<TSource> source, Action finallyAction);
  1. The finallyAction should be invoked after the source sequence has emitted an OnCompleted or an OnError notification, but before this notification is propagated to the observer.finallyAction应该调用source序列已经发射的OnCompletedOnError通知,但之前通知传播到观察者。
  2. If the finallyAction invocation completed successfully, the original OnCompleted / OnError notification should be propagated to the observer.如果finallyAction调用成功完成,原始OnCompleted / OnError通知应该传播给观察者。
  3. If the finallyAction invocation failed, an OnError notification should be propagated to the observer, containing the error that just occurred.如果finallyAction调用失败,则应将OnError通知传播给观察者,其中包含刚刚发生的错误。 In this case the previous error, the one that may have caused the source to complete with failure, should be ignored (not propagated).在这种情况下,应忽略(不传播)前一个错误,即可能导致source完成失败的错误。
  4. The finallyAction should also be invoked when the FinallySafe is unsubscribed before the completion of the source .finallyAction时,也应该调用FinallySafe在完成前取消订阅source When a subscriber (observer) disposes a subscription, the finallyAction should by invoked synchronously, and any error should be propagated to the caller of the Dispose method.当订阅者(观察者)处理订阅时,应同步调用finallyAction ,并将任何错误传播给Dispose方法的调用者。
  5. If the FinallySafe is subscribed by multiple observers, the finallyAction should be invoked once per subscription, independently for each subscriber, following the rules above.如果FinallySafe被多个观察者订阅,则应按照上述规则为每个订阅者独立地为每个订阅者调用一次finallyAction Concurrent invocations are OK.并发调用是可以的。
  6. The finallyAction should never be invoked more than once per subscriber.对于每个订阅者,不应多次调用finallyAction

Validation: replacing the Finally with the FinallySafe in the code snippet above, should result to a program that doesn't crash with an unhandled exception.验证:更换FinallyFinallySafe在上面的代码片断,应该产生一个不与未处理的异常崩溃的程序。

Alternative: I am also willing to accept an answer that provides a reasonable explanation about why the behavior of the built-in Finally operator is better than the behavior of the custom FinallySafe operator, as specified above.替代方案:我也愿意接受一个答案,该答案提供了一个合理的解释,说明为什么内置的Finally运算符的行为优于自定义的FinallySafe运算符的行为,如上所述。

Finally gets called after the sequence has ended, and since the Rx contract only allows one OnError or OnCompleted it can't issue a second one. Finally在序列结束后被调用,因为 Rx 合约只允许一个OnErrorOnCompleted它不能发出第二个。

But, if you replace the Finally with Do you can get the behaviour that you want.但是,如果你用Do替换Finally ,你可以获得你想要的行为。

Try this code:试试这个代码:

Observable
    .Timer(TimeSpan.FromMilliseconds(100))
    .Do(_ => { }, () => throw new ApplicationException("Oops!"))
    .Subscribe
        (_ => { },
        ex => Console.WriteLine(ex.Message),
        () => Console.WriteLine("Completed"));

Thread.Sleep(TimeSpan.FromMilliseconds(1000));

That operates as you expect it to.这正如您所期望的那样运作。

I get this output:我得到这个输出:

Oops!

I read the documentation and now I'm sure.我阅读了文档,现在我确定了。 The finally -operator will be called after the completition and should not throw any exception. finally -operator 将在完成后被调用,并且不应抛出任何异常。

Compared to non-reactive programming:与非反应式编程相比:

StreamReader file = new StreamReader("file.txt");
string ln;  

try {  
   while ((ln = file.ReadLine()) != null) {  
      Console.WriteLine(ln);
   }
}
finally {
   // avoid to throw an exception inside of finally!
   if (file != null) {
      file.close();
   }
}

It is important to not throw an exception inside of finally .重要的是不要在finally内部抛出异常。

Here is an example howto use it correctly ( fiddle ):这是一个如何正确使用它的示例(小提琴):

using System;
using System.Reactive.Linq;
using System.Threading;

public class Program
{
    public static void Main()
    {
        Observable
            .Range(1,5) // simulates stream-reader
            .Finally(() => Console.WriteLine("Close streamreader"))
            .Do(i => {
                if (i == 5) {
                    throw new ApplicationException("Oops!"); // simulates IO-error
                }
                
                Console.WriteLine("Read " + i);
            })
            .Subscribe(_ => { }, ex => Console.WriteLine(ex.Message),
                () => Console.WriteLine("Completed"));

        Thread.Sleep(1000);
    }
}

I'm not sure what you are trying to do (and I'm pretty new to c# reactive), but I think you are using not the right operator.我不确定您要做什么(而且我对 c# 反应式还很陌生),但我认为您使用的运算符不正确。

Edit编辑

But you can patch it, if you want.但是如果你愿意,你可以修补它。 In this article, they do something familar.在这篇文章中,他们做了一些类似的事情。
http://introtorx.com/Content/v1.0.10621.0/11_AdvancedErrorHandling.html http://introtorx.com/Content/v1.0.10621.0/11_AdvancedErrorHandling.html

Here is an implementation of the FinallySafe operator, having the behavior specified in the question:这是FinallySafe运算符的实现,具有问题中指定的行为:

/// <summary>
/// Invokes a specified action after the source observable sequence terminates
/// successfully or exceptionally. The action is invoked before the propagation
/// of the source's completion, and any exception thrown by the action is
/// propagated to the observer. The action is also invoked if the observer
/// is unsubscribed before the termination of the source sequence.
/// </summary>
public static IObservable<T> FinallySafe<T>(this IObservable<T> source,
    Action finallyAction)
{
    return Observable.Create<T>(observer =>
    {
        var finallyOnce = Disposable.Create(finallyAction);
        var subscription = source.Subscribe(observer.OnNext, error =>
        {
            try { finallyOnce.Dispose(); }
            catch (Exception ex) { observer.OnError(ex); return; }
            observer.OnError(error);
        }, () =>
        {
            try { finallyOnce.Dispose(); }
            catch (Exception ex) { observer.OnError(ex); return; }
            observer.OnCompleted();
        });
        return new CompositeDisposable(subscription, finallyOnce);
    });
}

The finallyAction is assigned as the Dispose action of a Disposable.Create disposable instance, in order to ensure that the action will be invoked at most once. finallyAction被分配为Disposable.Create一次性实例的Dispose操作,以确保该操作最多被调用一次。 This disposable is then combined with the disposable subscription of the source , by using a CompositeDisposable instance.然后,通过使用CompositeDisposable实例,将这个一次性订阅与source的一次性订阅相结合。

As a side note, I would like to address the question if we could go even further, and propagate downstream a possible error of the finallyAction during the unsubscription.作为旁注,我想解决这个问题,如果我们可以更进一步,并在取消订阅期间向下游传播finallyAction的可能错误。 This could be desirable in some cases, but unfortunately it's not possible.在某些情况下这可能是可取的,但不幸的是这是不可能的。 First and foremost doing so would violate a guideline, found in The Observable Contract document, that states:首先,这样做会违反The Observable Contract文件中的指导方针,该指导方针指出:

When an observer issues an Unsubscribe notification to an Observable, the Observable will attempt to stop issuing notifications to the observer.当观察者向 Observable 发出取消订阅通知时,Observable 将尝试停止向观察者发出通知。 It is not guaranteed, however, that the Observable will issue no notifications to the observer after an observer issues it an Unsubscribe notification.但是,不能保证 Observable 在观察者发出取消订阅通知后不会向观察者发出通知。

So such an implementation would be non-conforming.所以这样的实现将是不合格的。 Even worse, the Observable.Create method enforces this guideline, by muting the observer immediately after the subscription is disposed.更糟糕的是, Observable.Create方法通过在处理订阅后立即将observer静音来强制执行此准则。 It does so by encapsulating the observer inside an AutoDetachObserver wrapper.它通过将观察者封装在AutoDetachObserver包装器中来实现。 And even if we tried to circumvent this limitation by implementing an IObservable<T> type from scratch, any built-in operator that could be attached after our non-conforming Finally operator would mute our post-unsubscription OnError notification anyway.即使我们试图通过从头开始实现IObservable<T>类型来规避这个限制,任何可以在我们不符合的Finally运算符之后附加的内置运算符无论如何都会使我们的取消订阅后OnError通知静音。 So it's just not possible.所以这是不可能的。 An error during the unsubscription cannot be propagated to the subscriber that just requested to unsubscribe.取消订阅期间的错误无法传播给刚刚请求取消订阅的订阅者。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM