Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to pass a System.Action by reference?

I'm passing a System.Action as a parameter to a method which does some lengthy operation and want to add more stuff to the invocation list after the Action has been passed:

class Class1
{
    private System.Action onDoneCallback;
    void StartAsyncOperation(System.Action onDoneCallback)
    {
        this.onDoneCallback = onDoneCallback;
        // do lengthy stuff

    }

    void MuchLater()
    {
         this.onDoneCallBack?.Invoke();
    }
}

class Class2
{
    public System.Action action;

    void DoStuff()
    {
        action += () => print ("a");
        new Class1().StartAsyncOperation(action);
    }

    {
        // ... much later in another place but still before StartAsyncOperation ends
        action += () => print ("b");
    }
}

However, only the the stuff that was added with += before passing the Action as parameter is invoked. So, in this example, it will only print "a" but not "b".

This makes me think that System.Action is copied when it's passed as parameter (like a primitive type e.g. int would). So, when += is done later, it has no effect on the local copy of action inside SomeAsyncOperation.

I thought of passing System.Action with ref. However, I need to store it as member variable inside Class1, and I can't make a member variable a ref!

So, basically, the question is: how do you add more stuff to the invocation list of a callback after that callback has been passed and the lengthy operation is long on its way but hasn't ended yet.

EDIT:

Ended up replacing

new Class1().StartAsyncOperation(action);

with

new Class1().StartAsyncOperation(() => action?.Invoke());
like image 820
Igor Galochkin Avatar asked Aug 28 '19 17:08

Igor Galochkin


2 Answers

A delegate type is an immutable reference type, like a string:

s += "\n";

s is now a reference to a different object. If you pass it to a method, the method gets a reference to this object, not to whatever object s may refer to next. This lambda returns, and will continue to return, whatever object s refers to when the lambda is called:

() => s;

The same applies with a += () => {};: a is referencing a different object afterwards, but you can create a lambda which executes the current value of a, whatever that may be.

Hence:

new Class1().StartAsyncOperation(() => action());

Whatever you to do action after that point, the lambda you passed in has a reference to the current value of action.

Try it at home:

Action a = () => Console.Write("a");

//  This would print "a" when we call b() at the end
//Action b = a;

//  This prints "a+" when we call b() at the end.
Action b = () => a();

a += () => Console.Write("+");

b();
like image 90
15ee8f99-57ff-4f92-890c-b56153 Avatar answered Sep 27 '22 16:09

15ee8f99-57ff-4f92-890c-b56153


One option:

class Class2
{
    public List<System.Action> callbacks = new List<System.Action>();

    void DoStuff()
    {

        callbacks.Add(() => print("a"));
        new Class1().StartAsyncOperation(() => {
            forach(var a in callbacks()
            {
                a();
            }
        });
    }

    {
        // ... much later in another place but still before StartAsyncOperation ends
        callbacks.Add(() => print ("b"));
    }
}

You'll get a closure over the list, so any changes will still be available when the callbacks run.

like image 34
Joel Coehoorn Avatar answered Sep 27 '22 18:09

Joel Coehoorn