Reactive Extensions:拆分输入,处理和连接

时间:2016-02-06 15:04:33

标签: c# .net system.reactive

基本上,我有一个可观察的输入字符串,我想单独处理,然后对结果做一些事情。如果输入字符串包含逗号(作为分隔符),我想分割字符串并单独处理每个子字符串,然后对每个字符串序列执行某些操作。下面的代码段说明了我尝试做的简化版本:

[Fact]
public void UniTest1()
{
    var observable = new ReplaySubject<string>();
    observable.OnNext("a,b");
    observable.OnNext("c,d,e");
    observable.OnCompleted();

    var result = new List<string[]>();
    observable
        .SelectMany(x => x.Split(','))
        .Select(x => x.ToUpper())
        .ToArray() // How to collect an IEnumerable for each item here?
        .Do(s => result.Add(s))
        .Subscribe();

    // Here, result is actually {{"A","B","C","D","E"}}, I need {{"A","B"},{"C","D","E"}}
    Assert.Equal(2, result.Count);

    Assert.Equal("A", result[0][0]);
    Assert.Equal("B", result[0][1]);

    Assert.Equal("C", result[1][0]);
    Assert.Equal("D", result[1][1]);
    Assert.Equal("E", result[1][2]);
}

正如评论中所解释的,上述方法无效。 .ToArray() - 调用将整个observable连接成一个序列。

但是,我已经通过将拆分和处理分成单个操作来解决了这个问题:

[Fact]
public void UniTest2()
{
    var observable = new ReplaySubject<string>();
    observable.OnNext("a,b");
    observable.OnNext("c,d,e");
    observable.OnCompleted();

    var result = new List<string[]>();
    observable
        .Select(x => x.Split(',').Select(s => s.ToUpper()).ToArray())
        .Do(s => result.Add(s))
        .Subscribe();

    // Result is as expected: {{"A","B"},{"C","D","E"}}
    Assert.Equal(2, result.Count);
    Assert.Equal("A", result[0][0]);
    Assert.Equal("B", result[0][1]);
    Assert.Equal("C", result[1][0]);
    Assert.Equal("D", result[1][1]);
    Assert.Equal("E", result[1][2]);
}

但有没有办法,使用Rx,通过不将分裂和处理放在同一个动作中来解决这个问题?这个问题的推荐解决方案是什么?

我还应该提到,处理,即ToUpper() - 呼叫,实际上是一个网络服务呼叫。我在我的例子中使用了ToUpper(),这样我的问题就很容易解释了。但这意味着我希望这种处理能够并行完成并且不会阻塞。

2 个答案:

答案 0 :(得分:3)

您的代码中有很多内容值得一提。

首先,.ToArray()运算符接受一个返回零个或多个单值的observable,并将其更改为一个observable,该observable返回零个或多个值的单个数组。这样的观察必须在它能够返回其唯一值之前完成。

考虑到这一点,第一个查询的结果应该是有意义的。

你使用x.Split(',').Select(s => s.ToUpper()).ToArray()的第二个查询会产生你想要的输出,但你想知道“有没有办法,使用RX,通过不将分裂和处理放在同一个动作中来解决这个问题”

嗯,琐碎,是的:

var result = new List<string[]>();
observable
    .Select(x => x.Split(','))
    .Select(x => x.Select(s => s.ToUpper()))
    .Select(x => x.ToArray())
    .Do(s => result.Add(s))
    .Subscribe();

但是,这不会并行处理这些项目。除非您调用引入并行性的操作,否则Rx旨在串行工作。

通常,一种简单的方法是使用长期运行的选择,例如.Select(x => longRunningOperation(x))并使用它执行此操作:

.SelectMany(x => Observable.Start(() => longRunningOperation(x)))

在你的情况下,你可以从这开始:

observable
    .ObserveOn(Scheduler.Default)
    .SelectMany(x => Observable.Start(() => x.Split(',')))
    .SelectMany(x => Observable.Start(() => x.Select(s => s.ToUpper())))
    .SelectMany(x => Observable.Start(() => x.ToArray()))
    .Do(s => result.Add(s))
    .Subscribe();

但这只是并行化每个原始.OnNext调用,而不是内部处理。为此,您需要将x.Split(',')的结果转换为可观察对象,然后并行处理。

    observable
        .SelectMany(x => Observable.Start(() => x.Split(',').ToObservable()))
        .SelectMany(x => Observable.Start(() => x.SelectMany(s => Observable.Start(() => s.ToUpper()))))
        .SelectMany(x => Observable.Start(() => x.ToArray()))
        .Do(s => s.Do(t => result.Add(t)))
        .Merge()
        .Subscribe();

但是这开始看起来很疯狂,它不再在当前线程上运行,这意味着你的测试不会等待结果。

让我们回顾一下这个问题。

我开始摆脱.Do电话。这些通常适用于调试,但对于任何状态更改,它们都很糟糕。它们可以在查询中的任何线程上的任何位置运行,因此您需要确保.Do调用中的代码是线程安全的,并且调用result.Add(s) NOT 线程-safe。

我还引入了一个“webservice”调用,用一秒钟的处理延迟来替换.ToUpper(),这样我们就可以看到查询需要多长时间来处理,从而知道它是否并行运行。如果最后一个查询需要5秒才能运行,那么没有并行性,如果它少了,那么我们就赢了。

所以,如果我以最基本的方式编写查询,它看起来像这样:

Func<string, string> webservice = x =>
{
    Thread.Sleep(1000);
    return x.ToUpper();
};

var query =
    observable
        .Select(ls =>
            from p in ls.Split(',')
            select webservice(p))
        .Select(rs => rs.ToArray())
        .ToArray()
        .Select(rss => new List<string[]>(rss));

var sw = Stopwatch.StartNew();
List<string[]> result = query.Wait();
sw.Stop();

当我运行此操作时,我得到了预期结果{{"A","B"},{"C","D","E"}},但完成时间仅需5秒钟。这里没有预期的并行性。

现在让我们介绍一些并行性:

var query =
    observable
        .Select(ls =>
            from p in ls.Split(',').ToObservable()
            from r in Observable.Start(() => webservice(p))
            select r)
        .Select(rs => rs.ToArray())
        .Merge()
        .ToArray()
        .Select(rss => new List<string[]>(rss));

我基本上应用了上面描述的“SelectSelectMany / Start”模式。唯一棘手的部分是.Select(rs => rs.ToArray())IObservable<string[]>变为IObservable<IObservable<string[]>>所以我弹出.Merge()以将其展平。将并行性引入Rx查询时,这是正常的。

现在我运行查询 - BOOM - 只需一秒钟。所有五个输入都是并行运行的。现在唯一的问题是订单不再是决定因素。但是,当结果并行执行时,你无能为力。

我得到了这样的结果:

results

如果我将此作为测试运行,我会将结果排序为已知顺序,并将其与预期结果进行比较。

答案 1 :(得分:0)

如果我理解正确,你想保留原始数组。但是,在SelectMany之后,您已直接在流上将数组展平为单个值,因此您无法再将它们更改回单个数组。诀窍是将ToUpperToArray移到SelectMany

ToUpper也不是异步函数。它是重要的,否则你不会得到任何并行性(我认为它在你的真实代码中,但它使ToUpper成为一个糟糕的替代品。)。相反,我将使用Observable.Timer。如果您的Web服务调用不是一个可观察的,您需要转换它,但这是一个不同的问题,这里有点超出范围。

这确实意味着您的结果可能会出现故障。

new string[] { "a,b", "c,d,e" }.ToObservable()
    .SelectMany(str => str.Split(',')
        .ToObservable()
        .SelectMany(x => Observable.Timer(DateTime.Now.AddSeconds(2))
            .Select(_ => x.ToUpper()))
        .ToArray())
    .Subscribe(arr => { Console.WriteLine(string.Join(",", arr)); });

我在你的代码中注意到的其他一些事情:

    .Do(s => result.Add(s))
    .Subscribe();

您可以将result.Add(s)直接放在Subscribe

    .Subscribe(s => result.Add(s));

事实上,如果您正在编写测试用例,请使用testSchedulerresults.Messages.AssertEqual

using Microsoft.Reactive.Testing;
using NUnit.Framework;
using System;
using System.Reactive.Linq;

namespace test
{
    [TestFixture]
    public class UnitTests : ReactiveTest
    {

        [Test]
        public void UniTest1()
        {
            var testScheduler = new TestScheduler();

            var source = new string[] { "a,b", "c,d,e" }.ToObservable();

            var results = testScheduler.Start(
                () => source.SelectMany(str => str.Split(',')
                    .ToObservable()
                    .Select(x => x.ToUpper())
                    .ToArray()));

            results.Messages.AssertEqual(
                OnNext<string[]>(Subscribed, new string[] { "A", "B" }),
                OnNext<string[]>(Subscribed, new string[] { "C", "D", "E" }),
                OnCompleted<string[]>(Subscribed)
                );
        }
    }
}

测试Rx的有用资源:
http://www.introtorx.com/content/v1.0.10621.0/16_TestingRx.html#TestingRx
http://blogs.msdn.com/b/rxteam/archive/2012/06/14/testing-rx-queries-using-virtual-time-scheduling.aspx
https://msdn.microsoft.com/en-us/library/hh242967%28v=vs.103%29.aspx