.NET 委托平等?

发布于 2024-08-09 03:59:08 字数 6685 浏览 5 评论 0原文

无论如何,我认为这就是问题所在。我正在使用 RelayCommand,它用两个委托来装饰 ICommand。一个是 _canExecute 的 Predicate,另一个是 _execute 方法的 Action。

---背景动机 --

动机与 WPF 演示文稿的 ViewModel 单元测试有关。一种常见的模式是,我有一个具有 ObservableCollection 的 ViewModel,并且我想要一个单元测试来证明该集合中的数据是我所期望的给定的一些源数据(也需要转换为 ViewModel 的集合)。尽管两个集合中的数据在调试器中看起来相同,但测试似乎由于 ViewModel 的 RelayCommand 上的相等失败而失败。这是失败的单元测试的示例:

[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 的代码 - 它基本上是直接抄袭 Josh Smith 的想法,并在其中添加了一个平等的实现尝试解决此问题:

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)}}

任何人都可以解释我缺少什么解决办法是什么?

---- 编辑备注 ----

正如 Mehrdad 指出的那样,调试会话中的 get_CloseCommand 乍一看有点奇怪。它实际上只是一个属性获取,但它确实提出了一个问题:如果我需要采取一些技巧来使其工作,那么为什么委托的平等性会出现问题。

MVVM 的一些要点是将演示文稿中可能有用的任何内容公开为属性,以便您可以使用 WPF 绑定。我正在测试的特定类在其层次结构中有一个 WorkspaceViewModel,它只是一个已经具有关闭命令属性的 ViewModel。这是代码:

公共抽象类 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,并且我使用 equals 来使单元测试工作。

@梅尔达德 这是仅当我在相等比较中使用 Trickster 的 delegate.Method 时才有效的单元测试。

[测试治具] 公共类 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));
    }


}

----- 使用 MERHDAD"S IDEA 调试

器输出的 最新编辑 ?此对象的值 {Smack.Wpf.ViewModel.RelayCommand} 基础 {SharpArch.Core.DomainModel.ValueObject}:{Smack.Wpf.ViewModel.RelayCommand} _canExecute:空 _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();
    }

I think this is the question, anyway. I am using a RelayCommand, which decorates an ICommand with two delegates. One is Predicate for the _canExecute and the other is Action for the _execute method.

---Background motivation --

The motivation has to do with unit testing ViewModels for a WPF presentation. A frequent pattern is that I have one ViewModel that has an ObservableCollection, and I want a unit test to prove the data in that collection is what I expect given some source data (which also needs to be converted into a collection of ViewModels). Even though the data in both collections looks the same in the debugger, it looks like the test fails due to an equality failure on the ViewModel's RelayCommand. Here's an example of the failing unit test:

[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);
    }

--- Back to delegate equality ----

Here is the code for the RelayCommand - it's basically a direct rip-off of Josh Smith's idea, with an implementation for equality that I added in an attempt to solve this issue:

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);
        }
    }

}

In a unit test where I've effectively set the _execute delegate to the same method (_canExecute is null in both cases), the unit test fails at this line:

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

Debugger output:

?_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)}}

Can anyone explain what I am missing and what the fix is?

---- EDITED REMARKS ----

As Mehrdad pointed out, the get_CloseCommand from the debug session looks a bit weird at first. It really is just a property get, but it does raise the point as to why the equality of the delegate is problematic if I need to do tricks to make it work.

Some of the point of MVVM is to expose whatever might be useful in a presentation as properties, so you can use WPF binding. The particular class I was testing has a WorkspaceViewModel in it's heirarchy, which is just a ViewModel that already has a close command property. Here is the code:

public abstract class 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);
            }
        }
    }
}

You can see that the close command is a RelayCommand, and that I monkeyed with equals to make the unit test work.

@Merhdad
Here is the unit test that only works when I use Trickster's delegate.Method in the equality comparison.

[TestFixture]
public class WorkspaceViewModelTests
{
private WorkspaceViewModel vm1;
private 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));
    }


}

----- LATEST EDITS TO USE MERHDAD"S IDEA

debugger out put
?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

This is the result after changing the code to:

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

            return _closeCommand;
        }
    }
    RelayCommand _closeCommand;

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

软糖 2024-08-16 03:59:08

您是否使用匿名函数或其他函数创建委托?这些是根据 C# 规范 (§7.9.8) 的确切委托相等规则:

委托相等运算符

两个委托实例被视为相等,如下所示:
如果任一委托实例为 null,则当且仅当两者都为 null 时它们才相等。
如果委托具有不同的运行时类型它们永远不会相等
如果两个委托实例都有一个调用列表(第 15.1 节),那么当且仅当它们的调用列表长度相同,并且调用列表中的每个条目都等于(如下所定义)相应条目时,这些实例才相等,按顺序,在对方的调用列表中。
以下规则控制调用列表条目的相等性:
如果两个调用列表条目都引用相同的静态方法,则这些条目相等。
如果两个调用列表条目都引用同一目标对象上的同一非静态方法(由引用相等运算符定义),则条目是相等的。
允许(但不是必需的)通过评估语义上相同的匿名函数表达式具有相同(可能为空)的捕获外部变量实例集而生成的调用列表条目) 相等。

因此,在您的情况下,委托实例可能引用两个不同对象中的相同方法,或者引用两个匿名方法。


更新:事实上,问题是当您调用new RelayCommand(param => OnCloseCommand())时,您没有传递相同的方法引用。毕竟,此处指定的 lambda 表达式实际上是一个匿名方法(您没有传递对 OnCloseCommand 的方法引用;您正在传递对匿名方法的引用,该方法采用单个参数并调用 OnCloseCommand)。正如上面引用规范的最后一行所提到的,比较这两个委托没有必要返回 true

附注CloseCommand 属性的 getter 将简单地称为 get_CloseCommand,而不是 b__0。这是编译器为 get_CloseCommand 方法(CloseCommand getter)内的匿名方法生成的方法名称。这进一步证明了我上面提到的观点。

Are you creating the delegate out of anonymous functions or something? These are the exact delegate equality rules according to C# specification (§7.9.8):

Delegate equality operators

Two delegate instances are considered equal as follows:
If either of the delegate instances is null, they are equal if and only if both are null.
If the delegates have different runtime type they are never equal.
If both of the delegate instances have an invocation list (§15.1), those instances are equal if and only if their invocation lists are the same length, and each entry in one’s invocation list is equal (as defined below) to the corresponding entry, in order, in the other’s invocation list.
The following rules govern the equality of invocation list entries:
If two invocation list entries both refer to the same static method then the entries are equal.
If two invocation list entries both refer to the same non-static method on the same target object (as defined by the reference equality operators) then the entries are equal.
Invocation list entries produced from evaluation of semantically identical anonymous-function-expressions with the same (possibly empty) set of captured outer variable instances are permitted (but not required) to be equal.

So, in your case, it's possible that the delegate instances are referring to the same method in two different objects, or referring to two anonymous methods.


UPDATE: Indeed, the problem is that you are not passing the same method reference when you are calling new RelayCommand(param => OnCloseCommand()). After all, the lambda expression specified here is actually an anonymous method (you are not passing a method reference to OnCloseCommand; you are passing a reference to an anonymous method which takes a single parameter and calls OnCloseCommand). As mentioned in the last line of the specification quotation above, it's not necessary that comparing those two delegates return true.

Side Note: The getter of the CloseCommand property would be simply called get_CloseCommand and not <get_CloseCommand>b__0. This is the compiler generated method name for the anonymous method inside get_CloseCommand method (the CloseCommand getter). This further proves the point I mentioned above.

錯遇了你 2024-08-16 03:59:08

我现在对其他行一无所知,但是如果

CollectionAssert.AreEqual(_vm.ProjectActivities, models);

仅仅因为使用了 ReferenceEquality 就失败了怎么办?

您已经覆盖了 RelayCommand 的比较,但没有覆盖 ObservableCollection 的比较。

看起来在委托引用相等的情况下也使用了。

尝试通过 Delegate.Method 进行比较。

I don't know anything now about other lines but what if

CollectionAssert.AreEqual(_vm.ProjectActivities, models);

fails just because ReferenceEquality is used?

You have overridden the comparison for RelayCommand but not for ObservableCollection.

And it looks like in case of Delegates Reference equality is used too.

Try to compare by Delegate.Method instead.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文