基本上,我有一个可观察的输入字符串,我想单独处理,然后对结果做一些事情。如果输入字符串包含逗号(作为分隔符),我想分割字符串并单独处理每个子字符串,然后对每个字符串序列执行某些操作。下面的代码段说明了我尝试做的简化版本:
[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(),这样我的问题就很容易解释了。但这意味着我希望这种处理能够并行完成并且不会阻塞。
答案 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));
我基本上应用了上面描述的“Select
到SelectMany
/ Start
”模式。唯一棘手的部分是.Select(rs => rs.ToArray())
从IObservable<string[]>
变为IObservable<IObservable<string[]>>
所以我弹出.Merge()
以将其展平。将并行性引入Rx查询时,这是正常的。
现在我运行查询 - BOOM - 只需一秒钟。所有五个输入都是并行运行的。现在唯一的问题是订单不再是决定因素。但是,当结果并行执行时,你无能为力。
我得到了这样的结果:
如果我将此作为测试运行,我会将结果排序为已知顺序,并将其与预期结果进行比较。
答案 1 :(得分:0)
如果我理解正确,你想保留原始数组。但是,在SelectMany
之后,您已直接在流上将数组展平为单个值,因此您无法再将它们更改回单个数组。诀窍是将ToUpper
和ToArray
移到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));
事实上,如果您正在编写测试用例,请使用testScheduler
和results.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