Search code examples
c#unity-game-enginec#-8.0unity-editor

C# Struct mutability: Is a boolean ok to mutate?


I'm in the process of changing what was formerly a Class into a Struct, as part of an editor patch within Unity. I've read a lot of advice on using structs being "Don't allow mutable structs", due to poor copy behaviour resulting in modified copies, and being hard to track. As I understand, a result of being stack-based and having no data overhead.

However, I'd like to clarify that with the specific case. Is a boolean value okay to mutate within a struct, since the data size can never change? The particular boolean property could theoretically be modified with frequency, so if it's likely to cause memory problems I'll have to implement some other way to track that parameter that elsewhere.

Extra notes, in case of unexpected relevance:

  • The class has three properties, one of which is a boolean.
  • The two non-boolean properties will not be mutable.

Solution

  • The mantra "mutable structs are evil" is in itself a poor reflection on what the actual issue is. A more accurate version would be "secretly mutable structs in immutable locations are evil". Such locations include non-ref properties or method returns, readonly fields, or any non-variable expressions in general.

    The common source of surprise for people is that they forget that structs and classes are different, and must be handled differently. It is not correct to say that structs are stack-based, rather they are "indirectionless". Instances of classes have their own identity ‒ they are passed around indirectly, via references. Instances of structs are values; they don't have their own identity, their identity is the identity of the variable they are stored in.

    Contrary to the other answer, this code is not an issue:

    myObject.MyStructProperty.MyBoolfield = true;
    

    The compiler raises CS1612 in this situation, protecting you from the actual most common source of errors. It understands that this is (in case of mutating a property, most likely) a useless mutation of a temporary value, and since C# 7, using a ref property can give you what you want anyway.

    The actual issue are methods. C# didn't have readonly methods prior to version 8, meaning the compiler would always have to assume that a method could potentially modify the value (having to make defensive copies for readonly fields), yet still such methods have to be callable in all situations, since they as well may be useful just for their return value.

    myObject.MyStructProperty.FlipBoolfield();
    

    The compiler cannot warn you about this situation, since it doesn't know that FlipBoolfield secretly mutates the value. If MyStructProperty is non-ref, the mutation happens on a temporary copy of the value, and the changes are lost.

    All in all, simply don't mutate structs through methods. Mark all struct methods readonly, but keep mutable properties and fields if you want to.

    Since this is in the context of Unity, the engine actually uses a lot of mutable structs (and fields) everywhere, so you don't risk running into the error anyway.

    Simply put, this is fine:

    public struct DayDuration
    {
        public int Days;
    }
    

    This is not fine:

    public struct DayDuration
    {
        public int Days;
    
        // secret struct mutation
        public void AddDays(int count)
        {
            Days += count;
        }
    }
    

    This is fine again:

    public struct DayDuration
    {
        public int Days;
    
        // explicit ref parameter
        public static void AddDays(ref DayDuration duration, int count)
        {
            duration.Days += count;
        }
    }
    

    Or even better:

    public struct DayDuration
    {
        public int Days;
    }
    
    public static class DayDurationExtensions
    {
        // amazing to use and warns against non-mutable locations
        public static void AddDays(this ref DayDuration duration, int count)
        {
            duration.Days += count;
        }
    }
    

    You also seem to be particularly confused about the "overhead" of structs/classes. Since structs lack additional indirection, accessing them is faster, as the CPU doesn't have to go through a reference, and the GC doesn't have to be invoked at all. Be aware however that assigning a struct instance (value) to a different location without ref will copy all the data, so you won't get any memory optimization from it.