How Does StartCoroutine() Accept A Delegate With Multiple Signatures?

I use delegates (a method acting as a variable) everywhere. In C#, you cannot set/pass a delegate with different arguments.

However, it seems that StartCoroutine(IEnumerator routine) gets around this somehow since the "IEnumerator routine" argument can accept any coroutine that can have any combination of arguments.

Does anyone know why this works? Is it something special/specific about coroutines? Is it finding the method by its name via Reflection or something like that?

No, you’re not passing your method along but an IEnumerator object that is returned by your generator method. You don’t do StartCoroutine(MyMethod) but you do StartCoroutine(MyMethod()). That’s a big difference. When you call a generator method (a method with a yield statement in it) you actually get back a statemachine object that represents the code inside the original “method”.

A long time ago I’ve written a coroutine crash course on Unity Answers. Though since the site has degraded over time and was ultimatively replaced by Discussions, I do have a mirror on github. Over there I explain what an IEnumerator actually is and how Unity uses those iterators to implement coroutines.

So this has nothing to do with signatures of methods. This is just an interface that is implemented by a class. All the magical transformation of your code is done by the C# compiler that turns your linear code into a statemachine. You can use ILSpy or any other .NET reflector to see the hidden class that was generated. Though ILSpy is clever enough to actually decompile iterators, so look at the settings to turn it off ^^.

2 Likes

Thanks for the quick response. I’ll start to digest your answer. It was more than I bargained for. LOL.

The main thing is that:

  • StartCoroutine simply accepts a parameter of type IEnumerator.
  • When you execute MyMethod(), it returns an object of type IEnumerator.

So you are actually not passing just a reference of MyMethod to StartCoroutine, you are actually immediately executing MyMethod, and then passing the value that was returned by the method to StartCoroutine.

IEnumerator enumerator = MyMethod(1, 2); // My method gets executed immediately here
StartCoroutine(enumerator); // You pass the object returned by it to StartCoroutine

The confusing part is that when you write an IEnumerator method in C#, the compiler actually does a bunch of magic behind-the-scenes and generates a whole new class.

Given a coroutine like this:

public IEnumerator MyMethod(int parameter1, int parameter2)
{
    Debug.Log("Step " + parameter1);
 
    yield return new WaitForSeconds(1f);
 
    Debug.Log("Step " + parameter2);
}

The compiler would generate something like this:

class MyMethodEnumerator : IEnumerator
{
    int parameter1;
    int parameter2;
 
    int step;
 
    public object Current { get; private set; }

    public bool MoveNext()
    {
        if(step == 0)
        {
            Console.WriteLine(string.Concat("Step ", parameter1.ToString()));
            Current = new WaitForSeconds(1f);
            step = 1;
            return true;
        }
 
        if(step == 1)
        {
            step = -1;
            Console.WriteLine(string.Concat("Step ", parameter2.ToString()));
            return false;
        }

        return false;
    }
}

And when you execute MyMethod(), actually, an instance of this compiler-generated type gets returned.

public IEnumerator MyMethod(int parameter1, int parameter2)
{
    return MyMethodEnumerator() { parameter1: parameter1, parameter2: parameter2 };
}

It is with the help of this compiler generated type that coroutines are able to do their thing, where all their code does not get executed at once, but in smaller chunks, separated by yield statements.

Unity can use the generated object to do something like this essentially:

public static void StartCoroutine(IEnumerator routine)
{
    while(routine.MoveNext())
    {
        if(routine.Current is WaitForSeconds waitForSeconds)
        {
            Debug.Log("Waiting for "+waitForSeconds.seconds+ " seconds...");
        }
    }
}

SharpLab example.

2 Likes

Yes the difference all lie in the parenthesis ()

MethodName is a delegate, it means “get a reference to that method without executing it”
MethodName() is a method call, it means execute the method and get the result.

3 Likes

Yes. I see it now. The syntax was too sugary for me.

2 Likes