Равенство делегатов .NET?
-
06-07-2019 - |
Вопрос
Я думаю, что это вопрос, в любом случае.Я использую RelayCommand, который украшает ICommand двумя делегатами.Один из них — Predicate для _canExecute, а другой — Action для метода _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 из сеанса отладки на первый взгляд выглядит немного странно.На самом деле это просто получение свойства, но оно поднимает вопрос о том, почему равенство делегата является проблематичным, если мне нужно делать какие-то трюки, чтобы заставить его работать.
Одной из целей 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);
}
}
}
}
Вы можете видеть, что команда закрытия — это RelayCommand, и что я использовал равные значения, чтобы модульный тест работал.
@Merhdad Вот модульный тест, который работает только тогда, когда я использую Delegate's Tickster's Delegate.method в сравнении с равенством.
TestFixture] Public Class WorkSpaceViewModeltests {Private 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));
}
}
----- ПОСЛЕДНИЕ ИЗМЕНЕНИЯ ДЛЯ ИСПОЛЬЗОВАНИЯ ИДЕИ МЕРХДАДА
Debugger Out Pul?{Smack.wpf.viewmodel.relaycommand} _canexecute:null _execute:{Метод = {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();
}
Решение
Вы создаете делегат из анонимных функций или что-то в этом роде?Это точные правила равенства делегатов согласно спецификации C# (§7.9.8):
Делегирование операторов равенства
Два экземпляра делегата считаются равными следующим образом:Если любой из экземпляров делегата
null
, они равны тогда и только тогда, когда обаnull
.
Если делегаты имеют другой тип среды выполнения они никогда не равны.Если оба экземпляра делегата имеют список вызовов (§15.1), эти экземпляры равны тогда и только тогда, когда их списки вызовов имеют одинаковую длину, и каждая запись в одном списке вызовов равна (как определено ниже) соответствующей записи, по порядку, в списке вызовов другого.Следующие правила регулируют равенство записей списка вызовов:
Если обе записи списка вызовов обратитесь к тому жеstatic
метод тогда записи равны.
Если обе записи списка вызовов относятся к тому же не-static
метод на тот же целевой объект (как определено операторами ссылочного равенства), тогда записи равны.
Записи списка вызовов, созданные в результате оценки семантически идентичных анонимные-функциональные-выражения с тем же (возможно, пустым) набором захваченных экземпляров внешних переменных являются разрешено (но не обязательно) быть равным.
Итак, в вашем случае возможно, что экземпляры делегата ссылаются на один и тот же метод в двух разных объектах или ссылаются на два анонимных метода.
ОБНОВЛЯТЬ: Действительно, проблема в том, что вы не передаете одну и ту же ссылку на метод при вызове new RelayCommand(param => OnCloseCommand())
.В конце концов, указанное здесь лямбда-выражение на самом деле является анонимным методом (вы не передаете ссылку на метод в OnCloseCommand
;вы передаете ссылку на анонимный метод, который принимает один параметр и вызывает OnCloseCommand
).Как упоминалось в последней строке цитаты из спецификации выше, сравнение этих двух делегатов не обязательно возвращает результат. true
.
Примечание: Добытчик CloseCommand
свойство будет называться просто get_CloseCommand
и не <get_CloseCommand>b__0
.Это имя метода, сгенерированное компилятором для анонимного метода внутри. get_CloseCommand
метод ( CloseCommand
добытчик).Это еще раз подтверждает то, о чем я говорил выше.
Другие советы
Я сейчас ничего не знаю о других линиях, но что, если
CollectionAssert.AreEqual(_vm.ProjectActivities, models);
терпит неудачу только потому, что используется ReferenceEquality?
Вы переопределили сравнение для RelayCommand, но не для ObservableCollection.
И похоже, что в случае ссылки делегатов также используется равенство.
Вместо этого попробуйте сравнить с помощью Delegate.Method.