What is a Coroutine and why is there an IEnumerator

The title of the article is a question I was asked in an interview for a Middle position. In this article, we will look at Unity coroutines, what they are, and at the same time we will capture the topic of Enumerator \ Enumerable in C # and a little secret of foreach. The article should be very useful for beginners and interesting for experienced developers.

And so, as everyone knows, the method that represents Coroutine in Unity looks like this:

IEnumerator Coroutine()
{
  yield return null;
}
Some information about coroutines in Unity

As a return object after yield return can be:

  • new WaitForEndOfFrame() – stops execution until the end of the next frame

  • new WaitForFixedUpdate() – stops execution until the next physics engine frame.

  • new WaitForSeconds(float x) – stops execution for x seconds of game time (can be changed via Time.timeScale)

  • new WaitForSecondsRealtime(float x) – stops execution for x seconds of real time

  • new WaitUntil(Func) – stops execution until Func returns true

  • new WaitWhile(Func) – inverse of WaitUntil, continues execution when Func returns false

  • null – Same as WaitForEndOfFrame(), but execution continues at the beginning of the next. frame

  • break – ends the coroutine

  • StartCoroutine() – execution stops until the moment when the newly started coroutine ends.

Coroutines are launched via StartCoroutine(Coroutine()).

It returns an IEnumerator and has an unusual return with yield.
yield return is part of IEnumerator, this binding is converted, at compile time, into a state machine that saves position in the code, waits for IEnumerator’s MoveNext command, and continues execution until the next yield return or end of method, more details can be found at microsoft website.

The IEnumerator interface, in turn, contains the following elements:

public interface IEnumerator
{
  object Current { get; }

  bool MoveNext();
  void Reset();
}

Under the hood of Unity, this is handled like this: Unity receives an IEnumerator, which is passed through StartCoroutine(IEnumerator), immediately calls MoveNext, in order for the code to reach the first yield return, it is worth clarifying here that when such a method is called, the execution of the code inside the method does not start independently, and you need to call MoveNext, this can be checked with a simple script, which is presented under this paragraph, and then if Unity receives an object of type YieldInstruction in Current, then it executes the instruction and calls MoveNext again, that is, the method can return any type, and if it not a YieldInstruction, then Unity will treat it as yield return null.

private IEnumerator _coroutine;

// Start is called before the first frame update
void Start()
{
  _coroutine = Coroutine();
}

// Update is called once per frame
void Update()
{
  if (Time.time > 5)
    _coroutine.MoveNext();
}

IEnumerator Coroutine()
{
  while (true)
  {
    Debug.Log(Time.time);
    yield return null;
  }
}
The log shows that the method was called for the first time at the 5th second, according to the condition in Update()
The log shows that the method was called for the first time at the 5th second, according to the condition in Update()

Great, we covered the main point, namely what is IEnumerator and how it works. Now let’s look at this case:

Let’s describe a class that inherits the IEnumerator interface

class TestEnumerator : IEnumerator
{
    public object Current => new WaitForSeconds(1);
		
    public bool MoveNext()
    {
        Debug.Log(Time.time);
        return true;
    }

    public void Reset()
    {
    }
    
    /// Этот класс равносилен следующей корутине:
    /// IEnumerator Coroutine()
    /// {
    /// 	while(true){
    ///			Debug.Log(Time.time);
    ///			yield return new WaitForSeconds(1);
    /// 	}
    ///	}
}

And now we can use it in the following way:

void Start()
{
  StartCoroutine(new TestEnumerator());
}
Performs the same as a coroutine method
Performs the same as a coroutine method

And so, we have considered IEnumerator and coroutines, here you can consider different use cases for a long time, but at the root there remains the transfer of IEnumerator in any form to the StartCoroutine method.

Now I propose to consider IEnumerable, this interface is inherited by the native C# array, List from System.Generic and other similar types, its whole essence lies in the fact that it contains the GetEnumerator method, which returns an IEnumerator:

public interface IEnumerable
{
  [DispId(-4)]
  IEnumerator GetEnumerator();
}

Let’s implement a simple example:

class TestEnumerable : IEnumerable
{
    public IEnumerator GetEnumerator()
    {
        return new TestEnumerator();
    }
}

And now, we can do the following:

IEnumerator Coroutine()
{
  foreach (var delay in new TestEnumerable())
  {
    Debug.Log($"{Time.time}");
    yield return delay;
  }
}
You can see that the time is displayed twice in the log, this is due to the fact that we still have Debug in the TestEnumerator in the MoveNext method.
You can see that the time is displayed twice in the log, this is due to the fact that we still have Debug in the TestEnumerator in the MoveNext method.

There are many applications for this, for example, you can add a random delay time to the TestEnumerator:

class TestEnumerator : IEnumerator
{
    public object Current => new WaitForSeconds(_currDelay);
    private float _currDelay;

    public bool MoveNext()
    {
        _currDelay = Random.Range(1.0f, 3.0f);
        return true;
    }

    public void Reset()
    {
    }
}
The time between logs is not the same
The time between logs is not the same

And some magic for beginners: foreach does not require that the object returned by GetEnumerator implement IEnumerable, most importantly, that the type after “in” has a GetEnumerator() method, and returns a type with a Current property and a MoveNext() method, that is, we can do So:

class TestEnumerator // Здесь было наследование от IEnumerator
{
    public object Current => new WaitForSeconds(_currDelay);
    private float _currDelay;

    public bool MoveNext()
    {
        _currDelay = Random.Range(1.0f, 3.0f);
        return true;
    }
  
  	// Здесь был Reset из IEnumerator, он теперь не нужен :)
}

class TestEnumerable // Здесь было наследование от IEnumerable
{
  	// Возвращаемый тип был IEnumerator
    public TestEnumerator GetEnumerator()
    {
        return new TestEnumerator();
    }
}

As you can see, there is no inheritance anywhere and no mention of IEnumerable and IEnumerator, but we can also use the following code:

IEnumerator Coroutine()
{
  foreach (var delay in new TestEnumerable())
  {
    Debug.Log($"{Time.time}");
    yield return delay;
  }
}
Everything works the same and without errors
Everything works the same and without errors

And so, having analyzed the coroutines, IEnumerator, IEnumerable and foreach, you should see an example of using this knowledge in practice:

More convenient to use Coroutine

Here I wanted to describe the implementation of a coroutine with a cancellation token (CancelationToken) and start / end events with the ability to pause execution, but I was late, and there is a ready-made solution on github, I advise you to study, although I do not completely agree with the implementation:

unity-task-manager/TaskManager.cs at master AdamRamberg/unity-task-manager (github.com)

I would be grateful for criticism and comments, I also advise you to look at my other articles.

Similar Posts

Leave a Reply