.NET равенство делегатов? - PullRequest
       30

.NET равенство делегатов?

1 голос
/ 26 октября 2009

Я думаю, что это вопрос, во всяком случае. Я использую RelayCommand, который украшает ICommand с двумя делегатами. Одним из них является Предикат для _canExecute, а другим - Действие для метода _execute.

--- Фоновая мотивация -

Мотивация связана с модульным тестированием ViewModels для презентации WPF . Часто встречается ситуация, когда у меня есть одна ViewModel, у которой есть ObservableCollection, и я хочу, чтобы модульный тест подтвердил, что данные в этой коллекции - это то, что я ожидаю, учитывая некоторые исходные данные (которые также необходимо преобразовать в коллекцию ViewModels). Несмотря на то, что данные в обеих коллекциях выглядят одинаково в отладчике, похоже, что тест не пройден из-за ошибки равенства в RelayCommand ViewModel. Вот пример неудачного модульного теста:

[Test]
    public void Creation_ProjectActivities_MatchFacade()
    {
        var all = (from activity in _facade.ProjectActivities
                   orderby activity.BusinessId
                   select new ActivityViewModel(activity, _facade.SubjectTimeSheet)).ToList();

        var models = new ObservableCollection<ActivityViewModel>(all);
        CollectionAssert.AreEqual(_vm.ProjectActivities, models);
    }

--- Вернуться к равенству делегатов ----

Вот код для RelayCommand - это, по сути, прямой отрыв идеи Джоша Смита с реализацией для равенства, которую я добавил в попытке решить эту проблему:

public class RelayCommand : ICommand, IRelayCommand
{
    readonly Action<object> _execute;
    readonly Predicate<object> _canExecute;

    /// <summary>Creates a new command that can always execute.</summary>
    public RelayCommand(Action<object> execute) : this(execute, null) { }

    /// <summary>Creates a new command which executes depending on the logic in the passed predicate.</summary>
    public RelayCommand(Action<object> execute, Predicate<object> canExecute) {
        Check.RequireNotNull<Predicate<object>>(execute, "execute");

        _execute = execute;
        _canExecute = canExecute;
    }

    [DebuggerStepThrough]
    public bool CanExecute(object parameter) { return _canExecute == null ? true : _canExecute(parameter); }

    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }

    public void Execute(object parameter) { _execute(parameter); }

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(null, obj)) return false;
        if (ReferenceEquals(this, obj)) return true;
        if (obj.GetType() != typeof(RelayCommand)) return false;
        return Equals((RelayCommand)obj);
    }

    public bool Equals(RelayCommand other)
    {
        if (ReferenceEquals(null, other)) return false;
        if (ReferenceEquals(this, other)) return true;
        return Equals(other._execute, _execute) && Equals(other._canExecute, _canExecute);
    }

    public override int GetHashCode()
    {
        unchecked
        {
            return ((_execute != null ? _execute.GetHashCode() : 0) * 397) ^ (_canExecute != null ? _canExecute.GetHashCode() : 0);
        }
    }

}

В модульном тесте, где я фактически установил делегат _execute для одного и того же метода (в обоих случаях _canExecute имеет значение null), модульный тест завершается неудачно в этой строке:

return Equals(other._execute, _execute) && Equals(other._canExecute, _canExecute)

Вывод отладчика:

?_execute
{Method = {Void <get_CloseCommand>b__0(System.Object)}}
base {System.MulticastDelegate}: {Method = {Void CloseCommand>b__0(System.Object)}}

?other._execute
{Method = {Void <get_CloseCommand>b__0(System.Object)}} 
base {System.MulticastDelegate}: {Method = {Void CloseCommand>b__0(System.Object)}}

Может кто-нибудь объяснить, что мне не хватает и что за исправление?

---- РЕДАКТИРОВАННЫЕ ЗАМЕЧАНИЯ ----

Как отметил Мерадад, get_CloseCommand из сеанса отладки поначалу выглядит немного странно. Это действительно просто свойство get, но оно поднимает вопрос о том, почему равенство делегата проблематично, если мне нужно сделать трюки, чтобы заставить его работать.

Одной из целей MVVM является предоставление того, что может быть полезно в презентации, в качестве свойств, чтобы вы могли использовать привязку WPF. Конкретный класс, который я тестировал, имеет иерархию WorkspaceViewModel, которая является просто ViewModel, которая уже имеет свойство команды close. Вот код:

открытый абстрактный класс WorkspaceViewModel: ViewModelBase {

    /// <summary>Returns the command that, when invoked, attempts to remove this workspace from the user interface.</summary>
    public ICommand CloseCommand
    {
        get
        {
            if (_closeCommand == null)
                _closeCommand = new RelayCommand(param => OnRequestClose());

            return _closeCommand;
        }
    }
    RelayCommand _closeCommand;

    /// <summary>Raised when this workspace should be removed from the UI.</summary>
    public event EventHandler RequestClose;

    void OnRequestClose()
    {
        var handler = RequestClose;
        if (handler != null)
            handler(this, EventArgs.Empty);
    }

    public bool Equals(WorkspaceViewModel other) {
        if (ReferenceEquals(null, other)) return false;
        if (ReferenceEquals(this, other)) return true;
        return Equals(other._closeCommand, _closeCommand) && base.Equals(other);
    }

    public override int GetHashCode() {
        unchecked {
            {
                return (base.GetHashCode() * 397) ^ (_closeCommand != null ? _closeCommand.GetHashCode() : 0);
            }
        }
    }
}

Вы можете видеть, что команда close - это RelayCommand, и что я включил equals, чтобы заставить работать модульный тест.

@ Merhdad Вот модульный тест, который работает только тогда, когда я использую делегат Трикстера. Метод сравнения равенств.

[TestFixture] открытый класс WorkspaceViewModelTests { частный WorkspaceViewModel vm1; частный WorkspaceViewModel vm2;

    private class TestableModel : WorkspaceViewModel
    {

    }

    [SetUp]
    public void SetUp() {
        vm1 = new TestableModel();
        vm1.RequestClose += OnWhatever;
        vm2 = new TestableModel();
        vm2.RequestClose += OnWhatever;
    }

    private void OnWhatever(object sender, EventArgs e) { throw new NotImplementedException(); }


    [Test]
    public void Equality() {
        Assert.That(vm1.CloseCommand.Equals(vm2.CloseCommand));
        Assert.That(vm1.Equals(vm2));
    }


}

----- ПОСЛЕДНИЕ РЕДАКТИРОВАТЬ, ЧТОБЫ ИСПОЛЬЗОВАТЬ ИДЕЮ МЕРХДАДА

выход отладчика ? valueOfThisObject {} Smack.Wpf.ViewModel.RelayCommand base {SharpArch.Core.DomainModel.ValueObject}: {Smack.Wpf.ViewModel.RelayCommand} _canExecute: null _execute: {Method = {Void _executeClose (System.Object)}}

?valueToCompareTo
{Smack.Wpf.ViewModel.RelayCommand}
base {SharpArch.Core.DomainModel.ValueObject}: {Smack.Wpf.ViewModel.RelayCommand}
_canExecute: null
_execute: {Method = {Void _executeClose(System.Object)}}

?valueOfThisObject.Equals(valueToCompareTo)
false

Это результат после изменения кода на:

    public ICommand CloseCommand
    {
        get
        {
            if (_closeCommand == null)
                _closeCommand = new RelayCommand(_executeClose);

            return _closeCommand;
        }
    }
    RelayCommand _closeCommand;

    void _executeClose(object param) {
        OnRequestClose();
    }

Ответы [ 2 ]

6 голосов
/ 26 октября 2009

Вы создаете делегата из анонимных функций или чего-то еще? Это точные правила равенства делегатов согласно спецификации C # (§7.9.8):

Делегировать операторы равенства

Два экземпляра делегата считаются равными следующим образом: Если один из экземпляров делегата равен null, , они равны тогда и только тогда, когда оба являются null.
Если делегаты имеют другой тип времени выполнения , они никогда не равны . Если оба экземпляра делегата имеют список вызовов (§15.1), эти экземпляры равны в том и только в том случае, если их списки вызовов имеют одинаковую длину, а каждая запись в своем списке вызовов равна (как определено ниже) соответствующей записи, в порядке, в списке вызовов другого. Следующие правила регулируют равенство записей списка вызовов:
Если две записи списка вызовов обе относятся к одному и тому же static методу , то записи равны.
Если две записи списка вызовов обе ссылаются на один и тот же не static метод в одном и том же целевом объекте (как определено операторами равенства ссылок), тогда записи равны.
Записи списка вызовов, полученные из оценки семантически идентичных выражений анонимных функций с тем же (возможно пустым) набором захваченных экземпляров внешних переменных разрешены (но не обязательны) будет равным.

Итак, в вашем случае, возможно, что экземпляры делегатов ссылаются на один и тот же метод в двух разных объектах или ссылаются на два анонимных метода.


ОБНОВЛЕНИЕ: Действительно, проблема в том, что вы не передаете одну и ту же ссылку на метод при вызове new RelayCommand(param => OnCloseCommand()). В конце концов, лямбда-выражение, указанное здесь, на самом деле является анонимным методом (вы не передаете ссылку на метод на OnCloseCommand; вы передаете ссылку на анонимный метод, который принимает один параметр и вызывает OnCloseCommand). Как упоминалось в последней строке приведенной выше спецификации, необязательно, чтобы при сравнении этих двух делегатов возвращалось true.

Side Note: Получатель свойства CloseCommand будет называться просто get_CloseCommand, а не <get_CloseCommand>b__0. Это сгенерированное компилятором имя метода для анонимного метода внутри метода get_CloseCommand (метод получения CloseCommand). Это еще раз подтверждает точку, о которой я говорил выше.

1 голос
/ 26 октября 2009

Я ничего не знаю о других строках, но что, если

CollectionAssert.AreEqual(_vm.ProjectActivities, models);

терпит неудачу только потому, что используется ReferenceEquality?

Вы переопределили сравнение для RelayCommand, но не для ObservableCollection.

И, похоже, в случае делегатов также используется равенство ссылок.

Попробуйте вместо этого сравнить с Delegate.Method.

...