Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice

Question Pass a method to a function by reference

Discussion in 'Scripting' started by andrew_pearce_, Oct 23, 2022.

  1. andrew_pearce_

    andrew_pearce_

    Joined:
    Nov 5, 2018
    Posts:
    164
    Hello,

    I would like to know, how to "pass a method" by reference. To better understand my question:

    Code (CSharp):
    1.  
    2. class Worker {
    3.   public void DoJob(Action OnComplete) {
    4.     // do things
    5.     OnComplete?.Invoke();
    6.   }
    7. }
    8.  
    9. public class Example : MonoBehaviour {
    10.   private void Awake() {
    11.     Worker.instance.DoJob(this.OnCompleteCallback);
    12.     GameObject.Destroy(this.gameObject);
    13.   }
    14.  
    15.   private void OnCompleteCallback() {
    16.     Debug.Log($"Is 'this' null? {this == null}");
    17.     Debug.Log("Example:OnCompleteCallback()");
    18.   }
    19. }
    I intentionally destroy the object to demonstrate that "this" will be null but OnCompleteCallback will be still called, when I need a callback not to be called, if its parent is null (destroyed). Ideally I prefer callback to be automatically null, once parent gets destroyed.

    In actual game, there are cases when object is waiting for some job to complete but get destroyed (e.g. scene unload) by the time when reply arrives. So I have to check for "this == null" in every callback. Is there a better approach to do this checking only once, within DoJob method?

    Thank you in advance for any suggestion

    p.s. I even though to use event system instead as it allows me to remove listeners OnDestroy() but IMHO it looks like an overkill for simple logic and it's still easier to add extra IF statement at the beginning of callback to avoid null reference exception.
     
  2. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,043
    In C#, to pass a method by reference we use delegates and lambdas.

    A delegate is a top-level type declaration similar to class or interface, but you instead specify the name and signature without any body. Then you can declare a function argument by using that type and a method can be passed if it has the same signature.
    Code (csharp):
    1. delegate void MouseHandler(Vector2 position);
    2.  
    3. public class LibraryCode {
    4.   public void DoStuff(MouseHandler handler, float arg) {
    5.     if(handler is not null) {
    6.       handler(...);
    7.     }
    8.     ...
    9.   }
    10. }
    11.  
    12. public class UserCode {
    13.  
    14.   public void Test() {
    15.     _libr.DoStuff(myLocalMouseHandler, 1f);
    16.   }
    17.  
    18.   void myLocalMouseHandler(Vector2 position) {
    19.     ...
    20.   }
    21.  
    22. }
    To speed things up, you can use generic delegates in the System namespace, called Action and Func.
    Action can only receive arguments, while Func also returns a result.
    You specify the argument/return types via generic declaration (and there is virtually no cap to the count, order or combination of argument types, this is just an example).
    Code (csharp):
    1. using System;
    2.  
    3. public class LibraryCode {
    4.   public void DoStuff(Action<Vector2> handler, float arg) {
    5.     if(handler is not null) {
    6.       handler(...);
    7.     }
    8.     ...
    9.   }
    10. }
    11.  
    12. public class UserCode {
    13.  
    14.   public void Test() {
    15.     _libr.DoStuff(mouseHandler, 1f);
    16.   }
    17.  
    18.   void mouseHandler(Vector2 position) {
    19.     ...
    20.   }
    21.  
    22. }
    In this example, the handler is supposed to return an int. (return type is specified last.)
    Code (csharp):
    1. using System;
    2.  
    3. public class LibraryCode {
    4.   public void DoStuff(Func<Vector2, int> handler, float arg) {
    5.     if(handler is not null) {
    6.       int result = handler(...);
    7.     }
    8.     ...
    9.   }
    10. }
    11.  
    12. public class UserCode {
    13.  
    14.   public void Test() {
    15.     _libr.DoStuff(mouseHandler, 1f);
    16.   }
    17.  
    18.   int mouseHandler(Vector2 position) {
    19.     int result = 0;
    20.     ...
    21.     return result;
    22.   }
    23.  
    24. }
    Delegates can be reused with multiple variables and also features additional functionalities, including dynamic invoking and multicasting, which is then further expanded with the integrated events system.

    Lambdas are somewhat similar, but have certain peculiarities of their own, namely the syntax is slightly different, and lambdas also feature scope capturing. Syntax is easy but make sure to learn more about the latter. (Edit: the most recent explanation by Bunny.) You can also assign lambdas to variables declared with generic delegates and generally mix the two however you'd like.

    In this example I show how to shortcircuit incompatible function via lambda, but the real power of lambdas vastly surpasses my current ability to come up with a poetic or even pragmatic example.
    Code (csharp):
    1. using System;
    2.  
    3. public class LibraryCode {
    4.   public void DoStuff(Action<Vector2> handler, float arg) {
    5.     if(handler is not null) {
    6.       handler(...); // supply the argument
    7.     }
    8.     ...
    9.   }
    10. }
    11.  
    12. public class UserCode {
    13.  
    14.   public void Test() {
    15.     _libr.DoStuff(p => mouseHandler(p), 1f);
    16.   }
    17.  
    18.   int mouseHandler(Vector2 position) {
    19.     int result = 0;
    20.     ...
    21.     return result;
    22.   }
    23.  
    24. }
    There are also anonymous functions, but stick to delegates and lambdas and you'll never need them. They were implemented before they realized lambdas were much cooler.

    Additionally, when calling a function via argument or variable you can also use the Invoke method like with delegates
    Code (csharp):
    1. handler.Invoke(...); // supply the argument(s)
    Checking for null then can be simplified with the null-conditional operator.
    Code (csharp):
    1. handler?.Invoke(...); // auto-abandon statement if handler is null
    Edit:
    Working with callbacks as one would in Javascript is pretty much possible and sometimes even required. They've done a glorious job to streamline working with 'anonymous functions' et al in a strict statically-typed language, imho. And this was my biggest pet-peeve with C# coming from much softer languages like ActionScript 3 and HaXe. I have to admit I never worked with Rust or Scala etc, but C# offers the best imperative/oop design if I ever saw one and I kind of prefer c-likes anyway.
     
    Last edited: Oct 23, 2022
    Yoreki, Ryiah and Kiwasi like this.
  3. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,043
    Sorry I just now realized what you're asking for.

    Destroy is unreliable for what you're trying to do. Null check with MonoBehaviour derivations is a hack, not truly a C# null check (== and != operator are overridden), and so the callback truly does exist and there are no guarantees that the object will be dead by the time callback is called.

    You might do it with a delegate and += -= subscription if you have perfect control over the timing.

    I.e.
    Code (csharp):
    1. class Worker {
    2.  
    3.   public delegate void Callback();
    4.   public Callback Callback { get; }
    5.  
    6.   public void DoJob(Action OnComplete) {
    7.      Callback?.Invoke();
    8.   }
    9.  
    10. }
    11.  
    12. public class Example : MonoBehaviour {
    13.  
    14.   void Awake() {
    15.     Worker.instance.Callback += OnCompleteCallback;
    16.     Worker.instance.DoJob();
    17.   }
    18.  
    19.   void OnDisable() {
    20.     Worker.instance.Callback -= OnCompleteCallback;
    21.   }
    22.  
    23.   void OnCompleteCallback() {
    24.     Debug.Log("Example:OnCompleteCallback()");
    25.   }
    26.  
    27. }
    But this isn't much different than what you already got.
     
    Yoreki, Ryiah, Kiwasi and 1 other person like this.
  4. andrew_pearce_

    andrew_pearce_

    Joined:
    Nov 5, 2018
    Posts:
    164
    Thanks, that explains everything! I changed lambdas with methods in hope that they will be null when parent gets destroyed but that it did not work, I thought that may be passing a method as delegate causes it to become value type. I even give a try to pass delegate by reference but this requires method to be assigned to a variable first and the connection with "this" is lost.

    In my initial post I though that events (delegates with += and -=) will be an overkill but your example suggested a solution: I need to use delegates because it allows me to remove them once object gets destroyed.

    BTW @orionsyndrome is there any special reason you are removing delegate OnDisable instead of OnDestroy?
     
    Yoreki and orionsyndrome like this.
  5. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,043
    It makes sense that you want to also unsubscribe when the script is disabled, and OnDisable is guaranteed to be called just before OnDestroy, so you cover both cases. But no special reasons other than that.

    I just missed to handle re-enabling. That part is wrong/misleading.
    Code (csharp):
    1. void OnEnable() {
    2.   Worker.instance.Callback += OnCompleteCallback;
    3. }
     
  6. andrew_pearce_

    andrew_pearce_

    Joined:
    Nov 5, 2018
    Posts:
    164
    I tried to implement a logic using delegates but it did not workout. Implementing DoJob with delegates limits it to single job at a time, while it could be used simultaneously by different scripts. I did not mention parameter which is sent along with callback and affects what will be done. Of course we can pass that parameter to OnComplete so script can identify if the completed job is theirs but that will even out the advantage of using delegates (I could use lambda and check "this" for null).

    p.s. Sorry for the late reply. I spent last two days localizing a Unity 2020 bug, related to AssetBundle loading while scene loading is in progress. It causes all textures on a new scene to be null (completely white). You have to either do not load AssetBundle while scene is loading, or unload it later, once current scene finishes unloading and new one gets visible.