.NET代理平等? [英] .NET delegate equality?

查看:180
本文介绍了.NET代理平等?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

无论如何,我认为这是个问题。我正在使用一个RelayCommand,它用两个代表装饰一个ICommand。一个是_canExecute的谓词,另一个是_execute方法的Action。



---背景动机 -



动机与 WPF 演示文稿的单元测试ViewModels有关。一个频繁的模式是,我有一个ViewModel有一个ObservableCollection,我想要一个单元测试来证明该集合中的数据是我期望的一些源数据(也需要转换成ViewModels的集合)。即使两个集合中的数据在调试器中看起来都一样,由于ViewModel的RelayCommand上的等式失败,测试失败。以下是失败单元测试的示例:

  [测试] 
public void Creation_ProjectActivities_MatchFacade()
{
var all =(来自_facade.ProjectActivities中的活动
orderby activity.BusinessId
选择新的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> _执行;
readonly谓词< object> _canExecute;

///< summary>创建一个可以执行的新命令。< / summary>
public RelayCommand(Action< object> execute):this(execute,null){}

///< summary>创建一个新的命令,根据传递的逻辑执行谓词。< / 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))返回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))返回true;
return Equals(other._execute,_execute)&&等于(other._canExecute,_canExecute);
}

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

}

单元测试我已经有效地将_execute代表设置为相同的方法(_canExecute在两种情况下都为空),单元测试在此行失败:

  return Equals(other._execute,_execute)& amp;&等于(other._canExecute,_canExecute)

调试器输出:

 ?_执行
{Method = {Void< get_CloseCommand> b__0(System.Object)}}
base {System.MulticastDelegate}:{方法= {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,它只是一个已经具有close命令属性的ViewModel。这里是代码:



public abstract class WorkspaceViewModel:ViewModelBase
{

 code> ///< summary>返回当被调用时尝试从用户界面删除该工作空间的命令。< / summary> 
public ICommand CloseCommand
{
get
{
if(_closeCommand == null)
_closeCommand = new RelayCommand(param => OnRequestClose()) ;

return _closeCommand;
}
}
RelayCommand _closeCommand;

///< summary>当从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))返回true;
return Equals(other._closeCommand,_closeCommand)&&基数(其他);
}

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

你可以看到close命令是一个RelayCommand,并且我用等于猴子来进行单元测试。



@Merhdad
这是单元测试只有当我使用Trickster的委托时才有效。方法在等式比较中。



[TestFixture]
public class WorkspaceViewModelTests
{
private WorkspaceViewModel vm1;
private WorkspaceViewModel vm2;

 私人类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();


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


}

- 最新版本使用MERHDADS IDEA



调试器输出
?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();
}


解决方案

您是否创建委托的匿名功能还是什么?这些是根据C#规范(§7.9.8)的确切的代理平等规则:


代理等式运算符



两个委托实例被认为是相等的,如下所示:
如果任一委托实例是 null 它们是相等的并且只有两者都是 null

如果代理具有不同的运行时类型,则它们不会相等
如果两个委托实例都有一个调用列表(§15.1),那么当且仅当调用列表的长度相同时,这些实例才相同,并且一个调用列表中的每个条目都相同(如下所定义)相应的条目,按顺序,在另一个的调用列表中。
以下规则控制调用列表条目的相等性:

如果两个调用列表条目引用相同的 static 方法那么条目是相等的。

如果两个调用列表项引用相同的非 static 方法相同的目标对象(由引用相等运算符定义),则条目相等。

从语义相同的匿名函数的评估产生的调用列表条目表达式 与捕获的外部变量实例相同(可能为空)的集合允许(但不是必需)相等。


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




更新:确实,问题我当您调用新的RelayCommand(param =>)时,您不会传递相同的方法引用OnCloseCommand())。毕竟,这里指定的lambda表达式实际上是一个匿名方法(你没有传递一个方法引用到 OnCloseCommand ;你传递一个匿名方法的引用,它需要一个参数并调用 OnCloseCommand )。如上面说明书的最后一行所述,没有必要比较这两个代表返回 true



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


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

解决方案

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.

这篇关于.NET代理平等?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆