Coroutine operates on copy of struct instead of original

Take a look at the following short script (can be tested in playmode by pressing Return/Enter):

using System.Collections;
using UnityEngine;

public class Tester : MonoBehaviour
{
    private struct MutableStruct
    {
        public int mutableValue;


        public IEnumerator FaultyCoroutine()
        {
            mutableValue = 42;
            Debug.Log($"Mutable Value was changed to {mutableValue}");

            yield return null; // arbitrary yield
        }
    }


    // cached mutable struct
    private MutableStruct cachedStruct = new();


    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Return))
        {
            cachedStruct.mutableValue = 0; // reset mutable value
            StartCoroutine(TestRoutine());
        }
    }


    private IEnumerator TestRoutine()
    {
        // Expected: 0
        // Actual: 0
        Debug.Log($"Initial Value: {cachedStruct.mutableValue}");

        // changes the member value to 42
        yield return cachedStruct.FaultyCoroutine();

        // Expected: 42
        // Actual: 0
        Debug.Log($"Observed Value: {cachedStruct.mutableValue}");
    }
}

What I expected to happen is that the nested coroutine on line 42 operates on the cached instance of the mutable struct. Instead I can only assume that the Coroutine is executing on a copy of the instance, unlike a normal method would.
Of course this could be avoided by making MutableStruct a class, though this still seems like unintentional behaviour to me, or at least something worth documenting. Also, the solution of “Just make this struct a class!” can’t always be applied equally well.

Describe it somehow decisively in points what you want to achieve. Currently, the code looks like a wrong logical use of Coroutines.

In short, if something is to set a value in an object, why use two Coroutines when one is enough.

Well, yes, this does not work for the simple reason how iterators work in C#. Has nothing to do with Unity. When you call a generator method that returns an iterator (IEnumerator), that IEnumerator is a compiler generated class that represents the logic / content of your method / coroutine. You can not “reference” a struct. In your case your “original” struct is probably the data stored in the field “cachedStruct” of your MonoBehaviour class. When you reach out to the struct and call the generator to create an iterator, there is absolutely no possible way that this iterator can reference that original struct that lives inside the MonoBehaviour field. For exactly the same reason, iterator / generator methods can’t take ref or out parameters. Parameters to such a method (that contains a yield statement) are turned into fields of the auto-generated class. Member methods always have an implicit this parameter of the instance they belong to. Methods of structs usually receive a “ref” this pointer. Though in case of a coroutine, that’s not possible in any shape or form.

So this is not a bug but simply the nature of structs and generator / iterators.

5 Likes

After some more research I can say you’re absolutely right.
I tend to forget that Unity’s Coroutines are not much more than a worse async/await hack that works in older .NET versions, aswell as providing the rarely used WaitForSeconds functionality. I was forced into using one because Unity hates accesses to its API from any threads other than the main one, so I’m using a Coroutine as a sort of “proxy” between an actual asynchronous operation and some functionality that has to be on the main thread (without creating a hundred state checks within a MonoBehaviour.Update()). I rewrote some parts of my code so the issue this post is about is of no concern anymore.
Thanks!

1 Like

Well, the same holds true for async / await :slight_smile: Try this struct in a plain C# project. I currently have a simply console application open and I just added this:

public struct TestStruct
{
    public int val;
    public async Task<string> DoSomething()
    {
        val++;
        return "Test " + val;
    }
}

Inside another async method I just do

    // in class
    public TestStruct s = new TestStruct()

    // in async method:
    Console.WriteLine("InitialVal: " + s.val);
    Console.WriteLine(await s.DoSomething());
    Console.WriteLine(await s.DoSomething());
    Console.WriteLine(await s.DoSomething());
    Console.WriteLine(await s.DoSomething());
    Console.WriteLine("FinalVal: " + s.val);

The output would be:

So as you can see, you have the same problem with async / await. In the end it works in a similar manner to normal iterators, but have much greater eco system around it (the concept of awaiters, Tasks, etc). async methods actually get also transformed into an internal iterator class or struct. Instead of IEnumerator it usually uses the IAsyncStateMachine interface. Just as with IEnumerators (methods with yield instructions), local variables and parameters of the “method” become field members of an internally generated class / struct. So even the “this” reference of the type that contains the async method has to be stored in that iterator state machine. That means that structs are copied into that iterator and therefore seperate from source.

3 Likes

Right. Makes sense! To clarify, I made some modifications to my code that completely avoided any iterator-copying-struct issues, so this whole thing is luckily not holding me back anymore. All of these behaviours are still pretty useful to know, though. Thank you for the examples!

1 Like