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

Time:11-15

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.

CodePudding user response:

Changes to data size is not the reason why structs should not mutate.

The main reason is that mutable structs are copied by value and not by reference. The typical example is using a struct as a property:

myObject.MyStructProperty.MyBoolfield = true;

MyStructProperty will return a copy and only this copy will be mutated, not the actual field in myObject.

If the struct is small, just create a copy instead, that is perfectly safe, and creating structs is super cheap. If there is only three properties this is most likely the case. Your performance concerns are most likely ungrounded, always measure before making assumptions about performance.

If the struct is very large, you can allow mutation, but you will need to be very careful and use references everywhere to avoid copies.

CodePudding user response:

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 what 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.

  • Related