Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Task.Run how to write it with action and ref variable name

I am standing for a "little" syntax problem and cannt figure out how to correctly write what I desire.

I have the following method:

public void DoSomeMagic(string foo, ref string bar)
{
    //DoSomeMagic...
}

Now I would like to offload this code inside a Task.Run I would normally write the following:

public async void Button_Click(object sender, EventArgs args)
{
    string foo = "Hello Foo";
    string bar = "Hello Bar";

    await Task.Run(() => DoSomeMagic(foo, ref bar));
}

This doesn't compile telling me: "Cannot use 'ref' or 'out' parameter 'bar' inside an anonymous method body"

So I thought why even do the () => since I am just calling the method and I could reduce it to this:

Task.Run(DoSomeMagic(foo, ref bar));

This again doesn't compile telling me: Cannot resolve method 'Run(void)', candidates are: Run(Action) and Run(Func)

So again no problem Visual Studio your demand is my command.

And changed the code to this:

Task.Run((Action)DoSomeMagic(foo, ref bar));

Again doesn't compile telling me: Cannot cast expression of type 'void' to type 'Action',

Okay this starts getting tricky...

I than tried to instead of returning void I will just try int and cast to Func giving me the error: Cannot cast expression of type 'int' to type 'Func'

Saw that one coming but I thought lets give it a try:

So I tried the following approach:

public Action CallDoSomeMagic(string foo, ref string bar)
{
    //DoSomeMagic...
    return new Action(() => DoSomeMagic(foo, ref bar));
}

Task.Run(CallDoSomeMagic);

But this again gives me the message "Cannot use 'ref' or 'out' parameter 'bar' inside an anonymous method body"

Since my headache is increasing with every try, I thought you guys can help me out. Is it even possible?

like image 681
Rand Random Avatar asked Jul 30 '14 09:07

Rand Random


1 Answers

As the message says: you can't do that.

You could take a copy of the parameter value, and capture that, for example:

public Action CallDoSomeMagic(string foo, ref string bar)
{
    var snapshot = bar;
    return new Action(() => DoSomeMagic(foo, ref snapshot));
}

But note that updates to snapshot are not visible outside the caller via bar.

The reasons for this are two-fold:

Firstly, captured variables used on a lambda become fields on a compiler-generated context class. This works fine for non-ref/out parameters, as they already have a copy semantic. So in the case of my snapshot example, this is actually:

var ctx = new CompilerGeneratedContextClassWithHorribleName();
ctx.foo = foo;
ctx.snapshot = bar;
return new Action(ctx.CompilerGeneratedMethod);

where CompilerGeneratedMethod is:

DoSomeMagic(foo, ref snapshot);

this isn't possible for refs, as you'd essentially need the field to be a reference to a string-reference, which is... messy.

But more importantly: consider the lifetime of the caller. This could be:

void SomeMethod() {
    string s = "abc";
    CallDoSomeMagic("def", ref s);
}

Note in particular that the code needs to work even if the delegate is invoked much later - as indeed we might expect it to in your case since it involves Task and async. Now: if SomeMethod has exited: where is that reference to a string-reference pointing? Hint: it is just an arbitrary location on the stack, now out of scope.


Just to give a simpler workaround: instead of passing ref string bar, consider passing SomeType obj, where obj.Bar is the string you want; i.e.

public Action CallDoSomeMagic(string foo, SomeType obj)
{
    return new Action(() => DoSomeMagic(foo, obj));
}

public void DoSomeMagic(string foo, SomeType obj)
{
    // read and write obj.Bar here
}

Note you could also move foo to obj.Foo if you wanted.

like image 200
Marc Gravell Avatar answered Oct 26 '22 23:10

Marc Gravell