对Publish()行为的困惑.Refcount()

时间:2016-07-14 06:49:04

标签: system.reactive

我这里有一个简单的程序,用不同的单词显示字母数。它按预期工作。

static void Main(string[] args) {
    var word = new Subject<string>();
    var wordPub = word.Publish().RefCount();
    var length = word.Select(i => i.Length);
    var report =
        wordPub
        .GroupJoin(length,
            s => wordPub,
            s => Observable.Empty<int>(),
            (w, a) => new { Word = w, Lengths = a })
        .SelectMany(i => i.Lengths.Select(j => new { Word = i.Word, Length = j }));
    report.Subscribe(i => Console.WriteLine($"{i.Word} {i.Length}"));
    word.OnNext("Apple");
    word.OnNext("Banana");
    word.OnNext("Cat");
    word.OnNext("Donkey");
    word.OnNext("Elephant");
    word.OnNext("Zebra");
    Console.ReadLine();
}

输出是:

Apple 5
Banana 6
Cat 3
Donkey 6
Elephant 8
Zebra 5

我使用了Publish()。RefCount(),因为“wordpub”包含在“report”中两次。没有它,当一个单词被发出时,报告的一部分将通过回调得到通知,然后通知报告的另一部分,将通知加倍。这就是所发生的事情;输出最终有11项而不是6.至少这是我认为正在发生的事情。我想在这种情况下使用Publish()。RefCount()同时更新报告的两个部分。

但是,如果我将长度函数更改为ALSO,请使用已发布的源代码:

var length = wordPub.Select(i => i.Length);

然后输出是这样的:

Apple 5
Apple 6
Banana 6
Cat 3
Banana 3
Cat 6
Donkey 6
Elephant 8
Donkey 8
Elephant 5
Zebra 5

为什么长度函数也不能使用相同的已发布源?

4 个答案:

答案 0 :(得分:3)

这是一个很难解决的挑战! 如此微妙的条件发生了这种情况。 请提前道歉,但请耐心等待我!

<强> TL; DR

对已发布源的订阅按顺序处理,但在任何其他订阅之前直接处理未发布的源。即你可以跳队! 使用GroupJoin订阅顺序对于确定何时打开和关闭窗口非常重要。

我首先要关注的是你发布一个主题的引用计数。 这应该是一个无操作。 Subject<T>没有订阅费用。

所以当您删除Publish().RefCount()

var word = new Subject<string>();
var wordPub = word;//.Publish().RefCount();
var length = word.Select(i => i.Length);

然后你会遇到同样的问题。

那么我期待GroupJoin(因为我的直觉表明Publish().Refcount()是红鲱鱼)。 对我来说,单独关注这一点太难以合理化了,所以我依靠简单的调试,我已经使用了几十年的时间 - TraceLog扩展方法。

public interface ILogger
{
    void Log(string input);
}
public class DumpLogger : ILogger
{
    public void Log(string input)
    {
        //LinqPad `Dump()` extension method. 
        //  Could use Console.Write instead.
        input.Dump();
    }
}


public static class ObservableLoggingExtensions
{
    private static int _index = 0;

    public static IObservable<T> Log<T>(this IObservable<T> source, ILogger logger, string name)
    {
        return Observable.Create<T>(o =>
        {
            var index = Interlocked.Increment(ref _index);
            var label = $"{index:0000}{name}";
            logger.Log($"{label}.Subscribe()");
            var disposed = Disposable.Create(() => logger.Log($"{label}.Dispose()"));
            var subscription = source
                .Do(
                    x => logger.Log($"{label}.OnNext({x.ToString()})"),
                    ex => logger.Log($"{label}.OnError({ex})"),
                    () => logger.Log($"{label}.OnCompleted()")
                )
                .Subscribe(o);

            return new CompositeDisposable(subscription, disposed);
        });
    }
}

当我将日志记录添加到您提供的代码时,它看起来像这样:

var logger = new DumpLogger();

var word = new Subject<string>();
var wordPub = word.Publish().RefCount();
var length = word.Select(i => i.Length);
var report =
    wordPub.Log(logger, "lhs")
    .GroupJoin(word.Select(i => i.Length).Log(logger, "rhs"),
        s => wordPub.Log(logger, "lhsDuration"),
        s => Observable.Empty<int>().Log(logger, "rhsDuration"),
        (w, a) => new { Word = w, Lengths = a })
    .SelectMany(i => i.Lengths.Select(j => new { Word = i.Word, Length = j }));
report.Subscribe(i => ($"{i.Word} {i.Length}").Dump("OnNext"));
word.OnNext("Apple");
word.OnNext("Banana");
word.OnNext("Cat");
word.OnNext("Donkey");
word.OnNext("Elephant");
word.OnNext("Zebra");

然后在我的日志中输出类似下面的内容

使用Publish()登录。使用RefCount()

0001lhs.Subscribe()             
0002rhs.Subscribe()             
0001lhs.OnNext(Apple)
0003lhsDuration.Subscribe()     
0002rhs.OnNext(5)
0004rhsDuration.Subscribe()
0004rhsDuration.OnCompleted()
0004rhsDuration.Dispose()

    OnNext
    Apple 5 

0001lhs.OnNext(Banana)
0005lhsDuration.Subscribe()     
0003lhsDuration.OnNext(Banana)
0003lhsDuration.Dispose()       
0002rhs.OnNext(6)
0006rhsDuration.Subscribe()
0006rhsDuration.OnCompleted()
0006rhsDuration.Dispose()

    OnNext
    Banana 6 
...

但是当我删除使用Publish().RefCount()时,新的日志输出如下:

只记录主题

0001lhs.Subscribe()                 
0002rhs.Subscribe()                 
0001lhs.OnNext(Apple)
0003lhsDuration.Subscribe()         
0002rhs.OnNext(5)
0004rhsDuration.Subscribe()
0004rhsDuration.OnCompleted()
0004rhsDuration.Dispose()

    OnNext
    Apple 5 

0001lhs.OnNext(Banana)
0005lhsDuration.Subscribe()         
0002rhs.OnNext(6)
0006rhsDuration.Subscribe()
0006rhsDuration.OnCompleted()
0006rhsDuration.Dispose()

    OnNext
    Apple 6 

    OnNext
    Banana 6 

0003lhsDuration.OnNext(Banana)
0003lhsDuration.Dispose()
...

这为我们提供了一些见解,但是当问题真正变得清晰时,我们开始使用逻辑订阅列表来注释我们的日志。

在带有RefCount的原始(工作)代码中,我们的注释可能如下所示

//word.Subsribers.Add(wordPub)

0001lhs.Subscribe()             //wordPub.Subsribers.Add(0001lhs)
0002rhs.Subscribe()             //word.Subsribers.Add(0002rhs)
0001lhs.OnNext(Apple)
0003lhsDuration.Subscribe()     //wordPub.Subsribers.Add(0003lhsDuration)
0002rhs.OnNext(5)
0004rhsDuration.Subscribe()
0004rhsDuration.OnCompleted()
0004rhsDuration.Dispose()

    OnNext
    Apple 5 

0001lhs.OnNext(Banana)
0005lhsDuration.Subscribe()     //wordPub.Subsribers.Add(0005lhsDuration)
0003lhsDuration.OnNext(Banana)
0003lhsDuration.Dispose()       //wordPub.Subsribers.Remove(0003lhsDuration)
0002rhs.OnNext(6)
0006rhsDuration.Subscribe()
0006rhsDuration.OnCompleted()
0006rhsDuration.Dispose()

    OnNext
    Banana 6 

所以在这个例子中,当执行word.OnNext("Banana");时,观察者链按此顺序链接

  1. wordPub
  2. 0002rhs
  3. 但是 wordPub 有子订阅! 所以真正的订阅列表看起来像

    1. wordPub
      1. 0001lhs
      2. <击> 0003lhsDuration
      3. 0005lhsDuration
    2. 0002rhs
    3. 如果我们注释只有主题的日志,我们会看到微妙的位置

      0001lhs.Subscribe()                 //word.Subsribers.Add(0001lhs)
      0002rhs.Subscribe()                 //word.Subsribers.Add(0002rhs)
      0001lhs.OnNext(Apple)
      0003lhsDuration.Subscribe()         //word.Subsribers.Add(0003lhsDuration)
      0002rhs.OnNext(5)
      0004rhsDuration.Subscribe()
      0004rhsDuration.OnCompleted()
      0004rhsDuration.Dispose()
      
          OnNext
          Apple 5 
      
      0001lhs.OnNext(Banana)
      0005lhsDuration.Subscribe()         //word.Subsribers.Add(0005lhsDuration)
      0002rhs.OnNext(6)
      0006rhsDuration.Subscribe()
      0006rhsDuration.OnCompleted()
      0006rhsDuration.Dispose()
      
          OnNext
          Apple 6 
      
          OnNext
          Banana 6 
      
      0003lhsDuration.OnNext(Banana)
      0003lhsDuration.Dispose()
      

      所以在这个例子中,当执行word.OnNext("Banana");时,观察者链按此顺序链接

      1. 0001lhs
      2. 0002rhs
      3. 0003lhsDuration
      4. 0005lhsDuration
      

      0003lhsDuration之后激活0002rhs订阅时,它不会看到&#34; Banana&#34;终止窗口的值,直到 rhs 发送了值之后,从而在仍然打开的窗口中产生它。

      正如@ francezu13k50指出的那样明显而简单的问题解决办法就是使用word.Select(x => new { Word = x, Length = x.Length });,但我认为你已经给了我们一个简单版本的真实问题(赞赏)我理解为什么这不是&# 39; t合适。 但是,由于我不知道你真正的问题空间是什么,我不知道建议你提供一个解决方案,除了你有一个当前的代码,现在你应该知道它为什么会这样运作。< / p>

答案 1 :(得分:0)

只要对返回的Observable至少有一个订阅,

RefCount就会返回一个与源保持连接的Observable。处理完最后一个订阅后,RefCount会将其与源连接,并在新订阅时重新连接。报告查询可能就是“wordPub”的所有订阅都在查询完成之前处理完毕。

您可以简单地执行以下操作,而不是复杂的GroupJoin查询:

var report = word.Select(x => new { Word = x, Length = x.Length });

编辑: 如果要使用GroupJoin运算符,请将报告查询更改为

    var report =
        wordPub
        .GroupJoin(length,
            s => wordPub,
            s => Observable.Empty<int>(),
            (w, a) => new { Word = w, Lengths = a })
        .SelectMany(i => i.Lengths.FirstAsync().Select(j => new { Word = i.Word, Length = j }));

答案 2 :(得分:0)

因为GroupJoin似乎非常难以使用,所以这是另一种关联函数输入和输出的方法。

static void Main(string[] args) {
    var word = new Subject<string>();
    var length = new Subject<int>();
    var report =
        word
        .CombineLatest(length, (w, l) => new { Word = w, Length = l })
        .Scan((a, b) => new { Word = b.Word, Length = a.Word == b.Word ? b.Length : -1 })
        .Where(i => i.Length != -1);
    report.Subscribe(i => Console.WriteLine($"{i.Word} {i.Length}"));
    word.OnNext("Apple"); length.OnNext(5);
    word.OnNext("Banana");
    word.OnNext("Cat"); length.OnNext(3);
    word.OnNext("Donkey");
    word.OnNext("Elephant"); length.OnNext(8);
    word.OnNext("Zebra"); length.OnNext(5);
    Console.ReadLine();
}

如果每个输入具有0或更多输出,则该方法有效,受限于(1)输出仅以与输入相同的顺序到达AND(2)每个输出对应于其最近的输入。这就像一个LeftJoin - 第一个列表中的每个项目(单词)与随后到达的右侧列表(长度)中的项目配对,直到第一个列表中的另一个项目被发出。

答案 3 :(得分:0)

尝试使用常规Join而不是GroupJoin。我认为问题在于,当创建一个新单词时,在创建新窗口和结束当前窗口之间存在一个竞争条件。所以在这里我尝试通过将每个单词与一个表示窗口末尾的空值配对来强制它。不起作用,就像第一个版本没有那样。如何在不先前关闭每个单词的情况下为每个单词创建一个新窗口?完全糊涂了。

function initCropper() {
    // create blob url from file object
    vm.selectedImage.dataUrl = URL.createObjectURL(vm.selectedImage);

    $timeout(function () {
        // initialise cropper
        var image = document.getElementById(vm.modalId + '-image');
        vm.cropper = new Cropper(image, {
            aspectRatio: $scope.width / $scope.height,
            minContainerHeight: Number($scope.height) + 200,
            guides: false,
            cropBoxResizable: false,
            cropBoxMovable: false,
            zoomable: true,
            dragMode: 'move',
            toggleDragModeOnDblclick: false,
            checkOrientation: false,
            responsive: false,
            built: function () {
                // revoke blob url after cropper is built
                URL.revokeObjectURL(vm.selectedImage.dataUrl);
            }
        });
    });
}

function cropImage() {
    // get cropped image and pass to output file
    vm.cropper
        .getCroppedCanvas({ width: $scope.width, height: $scope.height })
        .toBlob(function (croppedImage) {
            $timeout(function () {
                croppedImage.name = vm.selectedImage.name;
                vm.croppedImage = croppedImage;
                vm.croppedImage.dataUrl = URL.createObjectURL(croppedImage);
                $scope.outputFile = vm.croppedImage;

                // destroy cropper
                vm.cropper.destroy();
                vm.cropper = null;
                vm.selectedImage = null;
            });
        }, 'image/jpeg');
}