试图理解SelectMany的这种用法

时间:2013-05-24 16:21:23

标签: c# system.reactive

我有一些关于使用SelectMany的问题,我在其中一个项目中遇到过。下面是一个重现其用途的小样本(我使用了一些Console.WriteLines来帮助查看不同点的状态):

public partial class MainWindow : INotifyPropertyChanged
{
    private bool _cb1, _cb2, _cb3, _isDirty;
    private readonly ISubject<Unit> _cb1HasChanged = new Subject<Unit>();
    private readonly ISubject<Unit> _cb2HasChanged = new Subject<Unit>();
    private readonly ISubject<Unit> _cb3HasChanged = new Subject<Unit>();
    private readonly ISubject<string> _initialState = new ReplaySubject<string>(1);

    public MainWindow()
    {
        InitializeComponent();
        DataContext = this;
        ObserveCheckBoxes();

        var initialState = string.Format("{0}{1}{2}", CB1, CB2, CB3);
        _initialState.OnNext(initialState);
        Console.WriteLine("INITIAL STATE: " + initialState);
    }

    public bool CB1
    {
        get
        {
            return _cb1;
        }
        set
        {
            _cb1 = value;
            _cb1HasChanged.OnNext(Unit.Default);
        }
    }

    public bool CB2
    {
        get
        {
            return _cb2;
        }
        set
        {
            _cb2 = value;
            _cb2HasChanged.OnNext(Unit.Default);
        }
    }

    public bool CB3
    {
        get
        {
            return _cb3;
        }
        set
        {
            _cb3 = value;
            _cb3HasChanged.OnNext(Unit.Default);
        }
    }

    public bool IsDirty
    {
        get
        {
            return _isDirty;
        }
        set
        {
            _isDirty = value;
            OnPropertyChanged("IsDirty");
        }
    }

    private void ObserveCheckBoxes()
    {
        var checkBoxChanges = new[]
            {
                _cb1HasChanged,
                _cb2HasChanged,
                _cb3HasChanged
            }
            .Merge();

        var isDirty = _initialState.SelectMany(initialState => checkBoxChanges
                                                                   .Select(_ => GetNewState(initialState))
                                                                   .Select(updatedState => initialState != updatedState)
                                                                   .StartWith(false)
                                                                   .TakeUntil(_initialState.Skip(1)));
        isDirty.Subscribe(d => IsDirty = d);
    }

    private string GetNewState(string initialState = null)
    {
        string update = string.Format("{0}{1}{2}", CB1, CB2, CB3);
        if (initialState != null)
        {
            Console.WriteLine("CREATING UPDATE: " + update + " INITIAL STATE: " + initialState);
        }
        else
        {
            Console.WriteLine("CREATING UPDATE: " + update);
        }
        return update;
    }

    public event PropertyChangedEventHandler PropertyChanged;
    void OnPropertyChanged(string prop)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(prop));
        }
    }

    private void Button_Click(object sender, System.Windows.RoutedEventArgs e)
    {
        var newState = GetNewState();
        _initialState.OnNext(newState);
        Console.WriteLine("SAVED AS: " + newState);
    }
}

和xaml:

<Window x:Class="WpfSB2.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <StackPanel>
            <CheckBox IsChecked="{Binding CB1}"></CheckBox>
            <CheckBox IsChecked="{Binding CB2}"></CheckBox>
            <CheckBox IsChecked="{Binding CB3}"></CheckBox>
            <Button IsEnabled="{Binding IsDirty}" Click="Button_Click">APPLY</Button>
        </StackPanel>
    </Grid>
</Window>

所以这个小应用程序的作用是显示三个复选框(所有最初都未选中)和一个“应用”按钮。当复选框的状态发生变化时,该按钮应该变为启用状态,并且在单击时变为禁用状态,直到复选框的状态再次变化。如果您更改复选框的状态,然后将其更改回其初始状态,则该按钮将适当地启用/禁用。该应用程序按预期工作,我只是想弄清楚为什么/如何。

现在问题:

  • 只要_initialState或复选框发生更改,是否会触发SelectMany调用?

  • _initialState.OnNext(initialState)的第一次调用; (在构造函数中)在SelectMany代码中并没有真正做任何事情。我看到它进入SelectMany代码,但实际上没有做任何事情(我的意思是,如果我在checkBoxChanges.Select部分放置一个断点,它会中断,但实际上没有选择)。这是因为还没有对任何复选框进行任何更改吗?

  • 正如所料,检查任何checkBox会触发isDirty检查。第一次更改单个复选框时,SelectMany语句中究竟发生了什么?

  • 选中一个复选框后,启用“应用”按钮,然后点击“应用”。这会导致_initialState.OnNext(newState);被称为。与我的第一个问题类似,SelectMany语句似乎没有发生任何事情。我认为初始状态获得一个新值会重新计算,但它似乎直接进入isDirty.Subscribe的OnNext处理程序(d =&gt; IsDirty = d);

  • 现在我点击了Apply,_initialState.OnNext总共被调用了两次。如果我选中一个新复选框,SelectMany如何处理?它是否经历了_initialState的所有过去状态?这些值是否存储在可观察量被处理之前?

  • StartsWith / TakeUntil / Skip系列有哪些功能?我注意到,如果我删除了TakeUntil行,那么当SelectMany子句开始遍历_initialState的所有过去值时,应用程序会停止正常工作,并且会混淆哪个是要比较的实际当前状态。

如果您需要其他信息,请与我们联系。

1 个答案:

答案 0 :(得分:2)

我认为您问题的关键部分是您对SelectMany的理解。如果您将SelectMany称为“从一个,选择多个”,我认为更容易理解。

对于源序列中的每个值,SelectMany将从另一个序列中提供零个,一个或多个值。

在您的情况下,您的源序列为_initialState。每次从该序列产生一个值时,它将订阅所提供的“内部序列”。

直接回答你的问题:
1)当_initialState推送一个值时,该值将传递给SelectMany运算符,并将订阅提供的“内部序列”。

2)第一个调用是将InitialState放在ReplaySubject的缓冲区中。这意味着当您第一次订阅_initialState序列时,它将立即推送一个值。将您的断点放在GetNewState中将显示此工作。

3)当你选中一个复选框时,它将调用setter,它将OnNext _cbXHasChanged主题(yuck),它将流入Merged序列(checkBoxChanges),然后流入SelectMany委托查询。

4)在复选框推送新值(它们不是重放主题)之前不会发生任何事情

5-6)是的,你已经调用了两次,因此它将运行selectMany委托两次,但是当第二个“内部序列”被启动时,TakeUntil将终止第一个“内部序列”。

SelectMany章节中的(我的网站)IntroToRx.com详细介绍了这一点。