IObservable收集当前流中的所有元素并执行批量操作

时间:2017-11-20 14:29:56

标签: c# system.reactive

示例代码

async Task Main()
{
    var entities = new double[] { 1, 2, 4, 8, 16, 32 }.ToObservable();

    entities = Square(entities);
    entities = Print(entities);
    entities = Add(entities, 1);
    entities = await SaveObservable(entities);
    entities = Add(entities, 2);

    await PrintAll(entities);
}

public IObservable<double> Square(IObservable<int> source)
    => source.Select(i => Math.Pow(i, 2.0));

public IObservable<double> Add(IObservable<double> source, double add)
    => source.Select(i => i + add);

public IObservable<T> Print<T>(IObservable<T> source)
    => source.Do(i => Console.WriteLine("Print: {0}", i));

public async Task<IObservable<T>> SaveObservable<T>(IObservable<T> source)
{
    var last = await source.LastOrDefaultAsync();
    Console.WriteLine("Collected observable and saving to the db. Last element is '{0}'", last);

    // await database.SaveChangesAsync();

    return source;
}

public async Task PrintAll(IObservable<double> source)
    => await source.ForEachAsync(i => Console.WriteLine("PrintAll: {0}", i));

想象一下,在我的SaveObservable函数中,我想对数据库执行操作,例如保存批量添加到数据库上下文中的所有实体,以便我可以获取数据库引擎(例如SQL)来填充任何数据库生成的ID。

所以我希望这个函数做的是收集传入的可观察流,调用database.SaveChangesAsync然后基本上继续传入传入的流。

但是,如果我运行上面的代码,则会再次调用Print函数。我不希望之前调用的observable的任何投影/映射再次运行。

我将如何实现这一目标?它甚至可能吗?

2 个答案:

答案 0 :(得分:3)

首先,你的观点是:这可能最好用普通的,基于拉的编程完成。

至于你的代码,你有两个问题:

1)Observable就像项目管道。您的代码基本上设置了两个管道:

entities -> Square -> Print -> Add(1) -> SaveObservable
entities -> Square -> Print -> Add(1) -> Add(2) -> PrintAll

这就是为什么你看到Print被叫两次的原因。如果你想看到它被调用一次,你可能希望管道做这样的事情:

entities -> Square -> Print -> Add(1) -> SaveObservable
                                     \-> Add(2) -> PrintAll

2)然而,由于asyncIObservable的混合,这说起来更容易。制作多线程管道的懒惰方法是在Add调用后拍一个.Publish().RefCount()

entities = Square(entities);
entities = Print(entities);
var entitiesAdded = Add(entities, 1).Publish().RefCount();
var _ = await SaveObservable(entitiesAdded);
entities = Add(entitiesAdded, 2);

await PrintAll(entities);

但这不起作用,因为Publish使得observable很热,一旦在SaveObservable中使用它,项目就不会再次发出。当所有孩子都已连接时,另一种方法是.Publish(),然后是.Connect(),但这与await的效果不佳。

对您的代码起作用的方式是添加.Replay().RefCount(),但它可能效果不佳,因为Rx必须设置一个内部缓冲区,该缓冲区包含订阅生命周期内的所有项目。

var entities = new double[] { 1, 2, 4, 8, 16, 32 }.ToObservable();

entities = Square(entities);
entities = Print(entities);
entities = Add(entities, 1).Replay().RefCount();
entities = await SaveObservable(entities);
entities = Add(entities, 2);

await PrintAll(entities);

有关热/冷可观测量的更多信息,PublishConnectReplayRefCountsee here

总结虽然:我建议您在没有Rx的情况下使用EF和TPL。

答案 1 :(得分:2)

您有两个订阅的原因是由于这两行:

  1. var last = await source.LastOrDefaultAsync();
  2. await source.ForEachAsync(i => Console.WriteLine("PrintAll: {0}", i));
  3. 两者都会导致订阅您的原始资源可观察资料。

    第一个是有效的:

    var last = await
        new double[] { 1, 2, 4, 8, 16, 32 }
            .ToObservable()
            .Select(i => Math.Pow(i, 2.0))
            .Do(i => Console.WriteLine("Print: {0}", i))
            .Select(i => i + 1)
            .LastOrDefaultAsync();
    

    第二个是有效的:

    await
        new double[] { 1, 2, 4, 8, 16, 32 }
            .ToObservable()
            .Select(i => Math.Pow(i, 2.0))
            .Do(i => Console.WriteLine("Print: {0}", i))
            .Select(i => i + 1)
            .Select(i => i + 2)
            .ForEachAsync(i => Console.WriteLine("PrintAll: {0}", i));
    

    此问题的答案是避免使用任何async / await代码。无论如何,Rx处理所有同步工作远比TPL更好。

    您也可以在不涉及任务的情况下await观察。

    也许以这种方式尝试你的代码:

    async void Main()
    {
        var entities = new double[] { 1, 2, 4, 8, 16, 32 }.ToObservable();
    
        entities = Square(entities);
        entities = Print(entities);
        entities = Add(entities, 1);
        entities = SaveObservable(entities);
        entities = Add(entities, 2);
    
        await PrintAll(entities);
    }
    
    public IObservable<double> Square(IObservable<double> source)
        => source.Select(i => Math.Pow(i, 2.0));
    
    public IObservable<double> Add(IObservable<double> source, double add)
        => source.Select(i => i + add);
    
    public IObservable<T> Print<T>(IObservable<T> source)
        => source.Do(i => Console.WriteLine("Print: {0}", i));
    
    public IObservable<T> SaveObservable<T>(IObservable<T> source)
    {
        return Observable.Create<T>(o =>
        {
            var last = default(T);
            return 
                source
                    .Do(
                        t => last = t,
                        () =>
                        {
                            Console.WriteLine("Collected observable and saving to the db. Last element is '{0}'", last);
                        })
                    .Subscribe(o);
        });
    }
    
    public IObservable<double> PrintAll(IObservable<double> source)
        => source.Do(i => Console.WriteLine("PrintAll: {0}", i));
    

    这可以工作并产生以下输出:

    Print: 1
    PrintAll: 4
    Print: 4
    PrintAll: 7
    Print: 16
    PrintAll: 19
    Print: 64
    PrintAll: 67
    Print: 256
    PrintAll: 259
    Print: 1024
    PrintAll: 1027
    Collected observable and saving to the db. Last element is '1025'
    

    要在继续管道之前允许SaveObservable完成,您需要为该方法添加.ToArray()。这会将产生零个或多个值的IObservable<T>更改为IObservable<T[]>,从而生成一个包含零个或多个元素的数组。因此,它必须在继续前完成所有值。

    尝试将SaveObservable更改为:

    public IObservable<T> SaveObservable<T>(IObservable<T> source)
    {
        return
            source
                .ToArray()
                .Do(ts =>
                {
                    var last = ts.Last();
                    Console.WriteLine("Collected observable and saving to the db. Last element is '{0}'", last);
                })
                .SelectMany(t => t);
    }
    

    现在输出:

    Print: 1
    Print: 4
    Print: 16
    Print: 64
    Print: 256
    Print: 1024
    Collected observable and saving to the db. Last element is '1025'
    PrintAll: 4
    PrintAll: 7
    PrintAll: 19
    PrintAll: 67
    PrintAll: 259
    PrintAll: 1027
    

    现在代码实际上是这样的:

    await
        new double[] { 1, 2, 4, 8, 16, 32 }
            .ToObservable()
            .Select(i => Math.Pow(i, 2.0))
            .Do(i => Console.WriteLine("Print: {0}", i))
            .Select(i => i + 1)
            .ToArray()
            .Do(ts =>
            {
                var last = ts.Last();
                Console.WriteLine("Collected observable and saving to the db. Last element is '{0}'", last);
            })
            .SelectMany(t => t)
            .Select(i => i + 2)
            .Do(i => Console.WriteLine("PrintAll: {0}", i));