.NETデリゲート平等?
-
06-07-2019 - |
質問
とにかくこれは問題だと思います。 ICommandを2つのデリゲートで装飾するRelayCommandを使用しています。 1つは_canExecuteの述語で、もう1つは_executeメソッドのアクションです。
---背景の動機-
動機は、 WPF プレゼンテーションのViewModelの単体テストに関係しています。よくあるパターンは、ObservableCollectionを持つViewModelが1つあることです。また、そのコレクション内のデータが、ソースデータ(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のコードは次のとおりです。これは基本的に、この問題を解決するために追加した平等の実装を備えた、ジョシュ・スミスのアイデアを直接引き離したものです。
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があります。これは、すでにcloseコマンドプロパティを持っている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);
}
}
}
}
closeコマンドはRelayCommandであり、ユニットテストを機能させるためにequalsを使用していることがわかります。
@Merhdad 以下は、等価比較でTricksterのdelegate.Methodを使用した場合にのみ機能する単体テストです。
[TestFixture] パブリッククラス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));
}
}
----- MERHDAD <!> quot; S IDEAを使用する最新の編集
デバッガ出力 ?valueOfThisObject {Smack.Wpf.ViewModel.RelayCommand} ベース{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#仕様(<!>#167; 7.9.8)に準拠した正確なデリゲート等価ルールです。
等値演算子のデリゲート
2つのデリゲートインスタンスは、次のように等しいと見なされます。 デリゲートインスタンスのいずれかが
null
の場合、これらは両方がstatic
である場合にのみ等しくなります。
デリゲートのランタイムタイプが異なる場合は、等しくない。 両方のデリゲートインスタンスに呼び出しリスト(<!>#167; 15.1)がある場合、それらのインスタンスは、呼び出しリストが同じ長さであり、one <!>#8217; s呼び出しリストの各エントリである場合にのみ等しいother <!>#8217; s呼び出しリストの対応するエントリと順番に等しい(以下で定義)。 次のルールは、呼び出しリストエントリの等価性を管理します。
2つの呼び出しリストエントリが両方とも同じnew RelayCommand(param => OnCloseCommand())
メソッドを参照する場合、エントリは等しくなります。
2つの呼び出しリストのエントリが両方とも 同じターゲットオブジェクトで同じOnCloseCommand
メソッドを参照している場合(参照等価演算子で定義)、エントリは等しくなります。
意味的に同一の anonymous-function-expressions キャプチャされた外部変数インスタンスの同じ(空の場合もある)セットの評価から生成された呼び出しリストエントリは、許可されます(ただし必須ではありません) )等しくなるようにします。
したがって、あなたの場合、デリゲートインスタンスが2つの異なるオブジェクトの同じメソッドを参照している、または2つの匿名メソッドを参照している可能性があります。
UPDATE:実際、問題は、true
を呼び出すときに同じメソッド参照を渡さないことです。結局のところ、ここで指定されたラムダ式は実際には匿名メソッドです(CloseCommand
にメソッド参照を渡すのではなく、単一のパラメーターを取り、get_CloseCommand
を呼び出す匿名メソッドに参照を渡します)。上記の仕様の引用の最後の行で述べたように、これらの2つのデリゲートを比較すると<get_CloseCommand>b__0
が返される必要はありません。
サイドノート: <=>プロパティのゲッターは、<=>ではなく、単に<=>と呼ばれます。これは、<=>メソッド内の匿名メソッド(<=>ゲッター)に対してコンパイラーが生成したメソッド名です。これは、上記のポイントをさらに証明します。
他のヒント
他の行については今は何も知りませんが、どうすれば
CollectionAssert.AreEqual(_vm.ProjectActivities, models);
ReferenceEqualityが使用されているという理由だけで失敗しますか?
RelayCommandの比較をオーバーライドしましたが、ObservableCollectionの比較はオーバーライドしませんでした。
そして、デリゲート参照の場合、参照平等も使用されているように見えます。
代わりにDelegate.Methodで比較してみてください。