Почему я должен захватить лямбда для переменной поля при вызове конструктора

Недавно я был немного странным, с выражением лямбда и переменными захватами. Код был приложением WPF/MVVM с использованием .NET 4.5 (VS2012). Я использовал разные конструкторы моей модели для установки обратного вызова для RelayCommand (эта команда будет привязана к элементу меню в моем представлении)

В сущности, у меня был следующий код:

public class MyViewModel : ViewModelBase
{
    public MyViewModel(Action menuCallback)
    {
        MyCommand = new RelayCommand(menuCallback);
    }

    public MyViewModel(Func<ViewModelBase> viewModelCreator)
    // I also tried calling the other constructor, but the result was the same
    //  : this(() => SetMainContent(viewModelCreator())
    {
        Action action = () => SetMainContent(viewModelCreator());
        MyCommand = new RelayCommand(action);
    }

    public ICommand MyCommand { get; private set; }
}

а затем создайте экземпляры выше, используя:

// From some other viewmodel code:
new MyViewModel(() => new SomeViewModel());
new MyViewModel(() => new SomeOtherViewModel());

Затем они были привязаны к меню WPF - каждый элемент меню имел экземпляр MyViewModel в качестве своего контекста данных. Странно, что меню только работало. Независимо от того, какой из элементов я попытался, он назовет соответствующий Func<ViewModelBase> - но только один раз. Если я попытался снова выбрать другой пункт меню или даже тот же элемент, он просто не сработает. Ничего не вызвало и не выводило на выходе VS отладки о каких-либо ошибках.

Мне известно о проблемах с захватами переменных в циклах, поэтому я предположил, что эта проблема была связана, поэтому изменила мою виртуальную машину на:

public class MyViewModel : ViewModelBase
{
    public MyViewModel(Action buttonCallback)
    {
        MyCommand = new RelayCommand(buttonCallback);
    }
    private Func<ViewModelBase> _creator;
    public MyViewModel(Func<ViewModelBase> viewModelCreator)
    {
        // Store the Func<> to a field and use that in the Action lambda
        _creator = viewModelCreator;
        var action = () => SetMainContent(_creator());
        MyCommand = new RelayCommand(action);
    }

    public ICommand MyCommand { get; private set; }
}

и называется так же. Теперь все работает так, как должно.

Просто для удовольствия, я также работал вокруг всего конструктора Func<ViewModelBase>, создав соответствующий Action вне конструктора MyViewModel:

// This code also works, even without the _creator field in MyViewModel
new MyViewModel(() => SetMainContent(new SomeViewModel()));
new MyViewModel(() => SetMainContent(new SomeOtherViewModel()));

Поэтому мне удалось заставить его работать, но мне все еще интересно, почему он работает так. Почему компилятор не правильно захватил Func<ViewModelBase> в конструкторе?

Ответ 1

Я предполагаю, что следующий код также будет работать

public MyViewModel(Func<ViewModelBase> viewModelCreator)
{
    var action = () => { creator = viewModelCreator; SetMainContent(creator()); };
    MyCommand = new RelayCommand(action);
}

Если это так, то причина, по которой он не работает первым, заключается в том, что вы фактически не закрываете переменную viewModelCreator, вы закрываете результат ее вызова.

Я все еще играю с кодом в LINQPad, но на самом деле не похоже, что я получаю ту же проблему, что и вы. Возможно, это нечто специфическое для RelayCommand. Не могли бы вы больше разместить свой код?