C# 可观测流上转换链(管道)的正确模式?

C# 可观测流上转换链(管道)的正确模式?,c#,reactive-programming,C#,Reactive Programming,我有以下情况: 给定一个对象流IObservable 处理每个E以获得E1或错误状态,在这种情况下,我需要错误消息M1 处理每个E1以获取E2或错误消息M2 还有一个额外的复杂性,即结果En和/或错误消息Mn可能取决于所有值E,E1,…,En-1——而不仅仅取决于En-1 考虑到所有这些,有没有比我使用的更好的模式 [编辑]根据要求,我添加了一个完全有效的示例;不幸的是,这使得这篇文章相当大 internal class Program { private static void Ma

我有以下情况:

  • 给定一个对象流
    IObservable
  • 处理每个
    E
    以获得
    E1
    或错误状态,在这种情况下,我需要错误消息
    M1
  • 处理每个
    E1
    以获取
    E2
    或错误消息
    M2
还有一个额外的复杂性,即结果En和/或错误消息
Mn
可能取决于所有值
E
E1
,…,
En-1
——而不仅仅取决于
En-1

考虑到所有这些,有没有比我使用的更好的模式

[编辑]根据要求,我添加了一个完全有效的示例;不幸的是,这使得这篇文章相当大

internal class Program
{
  private static void Main()
  {
    var stream = Enumerable.Range(1, 10).Select(i => new Record { Id = i }).ToObservable();

    stream
      .Select(it => new ComplexType { Item = it })
      .SelectIfOk(Process1)
      .SelectIfOk(Process2)
      .SelectIfOk(ProcessN)
      .Subscribe(DisplayResult);

    Console.ReadLine();
  }

  private static ComplexType Process1(ComplexType data)
  {
    // do some processing
    data.E1 = data.Item.Id * 10;

    // check for errors in output
    if (data.E1 == 30 || data.E1 == 70)
    {
      data.Errors.Add("Error");
    }

    return data;
  }

  private static ComplexType Process2(ComplexType data)
  {
    // do some processing
    data.E2 = (data.E1 - 3).ToString();

    // check for errors in output
    // can generate multiple errors for the same item
    if (data.E2.StartsWith("4"))
    {
      // does not only depend on the immediate precursor, E1 in this case
      data.Errors.Add("Starts with 4 -- " + data.Item.Id);
    }

    if (data.E2.StartsWith("8"))
    {
      data.Errors.Add("Starts with 8");
    }

    return data;
  }

  private static ComplexType ProcessN(ComplexType data)
  {
    // do some processing
    data.EN = "Success " + data.E2;

    // this one doesn't generate errors
    return data;
  }

  private static void DisplayResult(ComplexType data)
  {
    if (data.Errors.Any())
    {
      Console.WriteLine("{0:##0} has errors: " + string.Join(",", data.Errors));
    }
    else
    {
      Console.WriteLine("{0:##0}: {1}", data.Item.Id, data.EN);
    }
  }
}
以下是上述代码示例中使用的类:

public class Record
{
  public int Id { get; set; }
  public string FullName { get; set; }
  public string OtherStuff { get; set; }
}

public class ComplexType
{
  public Record Item { get; set; }

  // intermediary results
  public int E1 { get; set; }
  public string E2 { get; set; }

  // final result
  public string EN { get; set; }

  public List<string> Errors { get; set; }

  public ComplexType()
  {
    Errors = new List<string>();
  }
}
运行此代码的结果是:

1: Success 7
2: Success 17
3 has errors: Error
4: Success 37
5 has errors: Starts with 4 -- 5
6: Success 57
7 has errors: Error
8: Success 77
9 has errors: Starts with 8
10: Success 97
我正在使用ComplexType,这样我就可以同时携带中间结果和错误状态,而且看起来。。。可疑的。我已经盯着那个代码看了一周了(这是为了一个爱好项目),我一直觉得我错过了用Rx做事情的正确方法

[编辑]我忘了提到一件非常重要的事情:我必须处理流中的所有项目,即使其中一些项目会产生错误;这就是为什么我不能只使用发生异常的
Subscribe
重载-它将完成流。出现错误时放弃一项是可以的(如果
Process1
生成错误,则
Process2
,…,
ProcessN
将不再执行),但不放弃整个流

[编辑]另一个澄清:如果有帮助的话,我想到的处理将更自然地适用于TPL数据流库,但我仅限于.NET 4.0,因此无法使用它


顺便说一句,我在Rx中找不到任何关于错误处理的严肃讨论,通常会提到
Subscribe
重载/OnError调用,仅此而已。有人推荐深入研究该主题吗?

提前了解所有
ProcessN
意味着您可以摆脱多个
Select
操作符,简化查询并揭示您的真实意图

stream.Select(e => Process(e));

...

? Process(E e)
{
  // Process1, Process2, ...ProcessN
}
现在我们看到这不是一个反应性问题。这更像是一个交互式聚合问题

您还没有提到您最终需要在订阅中观察哪种输出,尽管我假设这是过程的聚合结果

要定义返回类型,我们首先需要定义
ProcessN
的返回类型。现在,我将使用语义更好的类型,而不是使用您的
ComplexType

Either<E, Exception> Process(?);
现在我们可以定义聚合器的返回类型(定义见上文):

IList进程(E)
{
//进程1,进程2,…进程n
}
聚合器的主体可以按如下方式实现:

IList<Either<E, Exception>> Process(E e)
{
  var results = new List<Either<E, Exception>>();
  results.Add(Process1(results.AsReadOnly()));
  results.Add(Process2(results.AsReadOnly()));
  ...
  results.Add(ProcessN(results.AsReadOnly()));
  return results.AsReadOnly();
}
IList进程(E)
{
var results=新列表();
results.Add(Process1(results.AsReadOnly());
results.Add(Process2(results.AsReadOnly());
...
results.Add(ProcessN(results.AsReadOnly());
返回结果。AsReadOnly();
}
接收中的错误处理 从阅读文章开始

有一个关于错误处理操作符的部分

以下是关于Rx中错误处理语义和契约的一些深入讨论/评论:


(完全披露:我参与了所有这些特定的讨论。)

当我问这个问题时,我不知道Rx中的
通知
类和
物化
方法。下面是我提出的解决方案-它主要解决了问题的“管道”方面,但我可以通过使用元组解决“依赖于中间结果”方面:

private static void Main()
{
  var source = new Subject<int>();

  source
    .Materialize()
    .SelectIfOk(Process1)
    .SelectIfOk(Process2)
    .Subscribe(it =>
      Console.WriteLine(it.HasValue
        ? it.Value.ToString()
        : it.Exception != null ? it.Exception.Message : "Completed."));

  source.OnNext(1);
  source.OnNext(2);
  source.OnNext(3);
  source.OnNext(4);
  source.OnNext(5);
  source.OnCompleted();

  Console.ReadLine();
}

private static int Process1(int value)
{
  if (value == 3)
    throw new Exception("error 1");

  // do some processing
  return value * 2;
}

private static string Process2(int value)
{
  if (value == 4)
    throw new Exception("error 2");

  // do some processing
  return value + " good";
}

private static IObservable<Notification<TR>> SelectIfOk<T, TR>(this IObservable<Notification<T>> stream,
  Func<T, TR> selector)
{
  Func<T, Notification<TR>> trySelector = it =>
  {
    try
    {
      var value = selector(it);
      return Notification.CreateOnNext(value);
    }
    catch (Exception ex)
    {
      return Notification.CreateOnError<TR>(ex);
    }
  };

  return stream.Select(it =>
    it.HasValue
      ? trySelector(it.Value)
      : it.Exception != null
        ? Notification.CreateOnError<TR>(it.Exception)
        : Notification.CreateOnCompleted<TR>());
}
private static void Main()
{
var source=新主题();
来源
.具体化
。选择确定(进程1)
。选择确定(进程2)
.订阅(it=>
Console.WriteLine(it.HasValue
?it.Value.ToString()
:it.Exception!=null?it.Exception.Message:“已完成”。);
资料来源.OnNext(1);
资料来源:OnNext(2);
资料来源:OnNext(3);
资料来源:OnNext(4);
资料来源:OnNext(5);
source.OnCompleted();
Console.ReadLine();
}
私有静态int进程1(int值)
{
如果(值==3)
抛出新异常(“错误1”);
//做一些处理
返回值*2;
}
私有静态字符串Process2(int值)
{
如果(值==4)
抛出新异常(“错误2”);
//做一些处理
返回值+良好;
}
私有静态IObservable SelectIfOk(此IObservable流,
Func选择器)
{
Func trySelector=it=>
{
尝试
{
var值=选择器(it);
返回通知.CreateOnNext(值);
}
捕获(例外情况除外)
{
返回通知。CreateOnError(ex);
}
};
返回流。选择(它=>
它有价值
?trySelector(it.值)
:it.Exception!=null
?Notification.CreateOnError(it.Exception)
:Notification.CreateOnCompleted());
}
如果我想使用中间结果,那么,正如我所说,我将使用元组:

private static Tuple<int, string> Process2(int value)
{
  if (value == 4)
    throw new Exception("error 2");

  // do some processing
  return Tuple.Create(value, value * 3 + " good");
}

private static string Process3(Tuple<int, string> value)
{
  return value.Item1 + " -> " + value.Item2;
}
私有静态元组进程2(int值)
{
如果(值==4)
抛出新异常(“错误2”);
//做一些处理
返回Tuple.Create(value,value*3+“good”);
}
私有静态字符串Process3(元组值)
{
返回value.Item1+“->”+value.Item2;
}
(我需要将
。选择ifok(Process3)
添加到管道中。)


我不愿意将自己的答案标记为正确,所以我将暂时不讨论这个问题;然而,据我所知,它确实满足了我的要求。

您提前知道所有流程吗?或者您是否因为Select运算符的惰性而需要它?我提前知道所有ProcessN;我使用Select是因为它需要
IList<Either<E, Exception>> Process(E e)
{
  var results = new List<Either<E, Exception>>();
  results.Add(Process1(results.AsReadOnly()));
  results.Add(Process2(results.AsReadOnly()));
  ...
  results.Add(ProcessN(results.AsReadOnly()));
  return results.AsReadOnly();
}
private static void Main()
{
  var source = new Subject<int>();

  source
    .Materialize()
    .SelectIfOk(Process1)
    .SelectIfOk(Process2)
    .Subscribe(it =>
      Console.WriteLine(it.HasValue
        ? it.Value.ToString()
        : it.Exception != null ? it.Exception.Message : "Completed."));

  source.OnNext(1);
  source.OnNext(2);
  source.OnNext(3);
  source.OnNext(4);
  source.OnNext(5);
  source.OnCompleted();

  Console.ReadLine();
}

private static int Process1(int value)
{
  if (value == 3)
    throw new Exception("error 1");

  // do some processing
  return value * 2;
}

private static string Process2(int value)
{
  if (value == 4)
    throw new Exception("error 2");

  // do some processing
  return value + " good";
}

private static IObservable<Notification<TR>> SelectIfOk<T, TR>(this IObservable<Notification<T>> stream,
  Func<T, TR> selector)
{
  Func<T, Notification<TR>> trySelector = it =>
  {
    try
    {
      var value = selector(it);
      return Notification.CreateOnNext(value);
    }
    catch (Exception ex)
    {
      return Notification.CreateOnError<TR>(ex);
    }
  };

  return stream.Select(it =>
    it.HasValue
      ? trySelector(it.Value)
      : it.Exception != null
        ? Notification.CreateOnError<TR>(it.Exception)
        : Notification.CreateOnCompleted<TR>());
}
private static Tuple<int, string> Process2(int value)
{
  if (value == 4)
    throw new Exception("error 2");

  // do some processing
  return Tuple.Create(value, value * 3 + " good");
}

private static string Process3(Tuple<int, string> value)
{
  return value.Item1 + " -> " + value.Item2;
}