Foreword: I am trying to describe the scenario very precisely here. The TL;DR
version is 'how do I tell if a lambda will be compiled into an instance method or a closure'...
I am using MvvmLight in my WPF projects, and that library recently changed to using WeakReference
instances in order to hold the actions that are passed into a RelayCommand
. So, effectively, we have an object somewhere which is holding a WeakReference
to an Action<T>
.
Now, since upgrading to the latest version, some of our commands stopped working. And we had some code like this:
ctor(Guid token)
{
Command = new RelayCommand(x => Messenger.Default.Send(x, token));
}
This caused a closure (please correct me if I'm not using the correct term) class to be generated - like this:
[CompilerGenerated]
private sealed class <>c__DisplayClass4
{
public object token;
public void <.ctor>b__0(ReportType x)
{
Messenger.Default.Send<ReportTypeSelected>(new ReportTypeSelected(X), this.token);
}
}
This worked fine previously, as the action was stored within the RelayCommand
instance, and was kept alive whether it was compiled to an instance method or a closure (i.e. using the '<>DisplayClass' syntax).
However, now, because it is held in a WeakReference
, the code only works if the lambda specified is compiled into an instance method. This is because the closure class is instantiated, passed into the RelayCommand
and virtually instantly garbage collected, meaning that when the command came to be used, there was no action to perform. So, the above code has to be modified. Changing it to the following causes that, for instance:
Guid _token;
ctor(Guid token)
{
_token = token;
Command = new RelayCommand(x => Messenger.Default.Send(x, _token));
}
This causes the compiled code to result in a member - like the following:
[CompilerGenerated]
private void <.ctor>b__0(ReportType x)
{
Messenger.Default.Send<ReportTypeSelected>(new ReportTypeSelected(X), this._token);
}
Now the above is all fine, and I understand why it didn't work previously, and how changing it caused it to work. However, what I am left with is something which means the code I write now has to be stylistically different based on a compiler decision which I am not privy to.
So, my question is - is this a documented behaviour in all circumstances - or could the behaviour change based on future implementations of the compiler? Should I just forget trying to use lambdas and always pass an instance method into the RelayCommand
? Or should I have a convention whereby the action is always cached into an instance member:
Action<ReportTypeSelected> _commandAction;
ctor(Guid token)
{
_commandAction = x => Messenger.Default.Send(x, token);
Command = new RelayCommand(_commandAction);
}
Any background reading pointers are also gratefully accepted!
Whether you will end up with a new class or an instance method on the current class is an implementation detail you should not rely on.
From the C# specification, chapter 7.15.2 (emphasis mine):
It is explicitly unspecified whether there is any way to execute the block of an anonymous function other than through evaluation and invocation of the lambda-expression or anonymous-method-expression. In particular, the compiler may choose to implement an anonymous function by synthesizing one or more named methods or types.
-> Even the fact that it generates any methods at all is not specified.
Given the circumstances, I would go with named methods instead of anonymous ones. If that's not possible, because you need to access variables from the method that registers the command, you should go with the code you showed last.
In my opinion the decision to change RelayCommand
to use WeakReference
was a poor one. It created a lot more problems than it solved.
As soon as the lambda references any free variables (aka capture), then this will happen as it needs a common location (aka storage class/closure) to reference (and/or assign to) them.
An exercise for the reader is to determine why these storage classes cannot just be static.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With