How to pass a System.Action by reference?
Asked Answered
A

2

7

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());
Ampersand answered 28/8, 2019 at 17:49 Comment(3)
Caveman solution: Don't pass action, pass () => action().Tetraploid
Great, it worked, thanks! If you like please write it as an answer so that I could accept it.Ampersand
Happy to. I'm delighted by this question: I never knew you could "concatenate" delegates like that.Tetraploid
T
5

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();
Tetraploid answered 28/8, 2019 at 18:10 Comment(6)
This works because you are closing over the reference to a when you create b, or by relation, because you are closing over the reference to action when you call the operation with () => action(). As a result, modification to the reference is reflected later on.Punctual
Actually, after testing this in code, I found out that one may need to use not just () => action(), but instead () => action?.Invoke() - for the case if the invocation list for the action was empty.Ampersand
" what a delegate is -- other than, as we see, a value type" -- delegates are not "value types". They are reference types. They are immutable, and so share some features with immutable value types. But it is very wrong to describe them as a "value type".Devil
@PeterDuniho So a+= () =>{} replaces a? Thank you, I’ll update the answer.Tetraploid
Yes. Delegate types work just like strings. And like with strings, the addition/concatenation operator (i.e. +) returns a whole new instance. And regardless of whether the type is mutable or not, reference or value type, the += operator in an expression like x += y is really just shorthand for x = x + y...the value of x is always replaced with the result of the operator.Devil
@PeterDuniho Low IQ day, sorry. Thanks for setting me straight.Tetraploid
G
1

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.

Glandule answered 28/8, 2019 at 18:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.