29

All over Stack Overflow and the internet I see that it is a good design principle to keep structs immutable. Unfortunately, I never see any implementation that actually causes these structs to be truly immutable.

Assuming that a struct does not have any reference types inside it, how do I actually make a struct immutable? That is, how do I prevent the mutation of any of its primitive field (perhaps by a compile-time/runtime exception)?

I wrote a simple test attempting make a struct immutable, but not even using the System.ComponentModel.ImmutableObjectAttribute worked:

class Program
{
    static void Main(string[] args)
    {
        ImmutableStruct immStruct1 = new ImmutableStruct();
        Console.WriteLine(immStruct1); //Before mutation.

        immStruct1.field1 = 1;
        immStruct1.field2 = "Hello";
        immStruct1.field3 = new object();
        Console.WriteLine(immStruct1); //After 1st mutation.

        immStruct1.field1 = 2;
        immStruct1.field2 = "World";
        immStruct1.field3 = new object();
        Console.WriteLine(immStruct1); //After 2nd mutation.

        Console.ReadKey();
    }
}

[ImmutableObject(true)]
struct ImmutableStruct
{
    public int field1;
    public string field2;
    public object field3;

    public override string ToString()
    {
        string field3String = "null";
        if (field3 != null)
        {
            field3String = field3.GetHashCode().ToString();
        }
        return String.Format("Field1: {0}, Field2: {1}, Field3: {2}", field1, field2, field3String);
    }
}
Nicholas Miller
  • 4,205
  • 2
  • 39
  • 62
  • 1
    A bit OT, but is there some specific reason you need a struct instead of just using a class? I haven't found a usage case where a struct is preferable over a class in many years of LOB programming. – StingyJack Apr 21 '16 at 20:54
  • Mark fields as readonly? Also, have you seen this: http://stackoverflow.com/questions/29974301/should-a-c-sharp-struct-have-only-read-only-properties – Pawel Apr 21 '16 at 20:54
  • The struct in mind is similar to C++'s bitfield structs. The struct is instantiated, and my desire is that the struct will never be changed -- there is a promise being made that I've not modified the data by accident. – Nicholas Miller Apr 21 '16 at 20:57

2 Answers2

36

Make the fields private readonly and pass the initial values in the constructor

public struct ImmutableStruct
{
    private readonly int _field1;
    private readonly string _field2;
    private readonly object _field3;

    public ImmutableStruct(int f1, string f2, object f3)
    {
        _field1 = f1;
        _field2 = f2;
        _field3 = f3;
    }

    public int Field1 { get { return _field1; } }
    public string Field2 { get { return _field2; } }
    public object Field3 { get { return _field3; } }
}

Starting with C#6.0 (Visual Studio 2015) You can use getter only properties

public struct ImmutableStruct
{
    public ImmutableStruct(int f1, string f2, object f3)
    {
        Field1 = f1;
        Field2 = f2;
        Field3 = f3;
    }

    public int Field1 { get; }
    public string Field2 { get; }
    public object Field3 { get; }
}

Note that readonly fields and getter only properties can be initialized either in the constructor or, in classes, also with field or property initializers public int Field1 { get; } = 7;.

It is not guaranteed that the constructor is run on a struct. E.g. if you have an array of structs, you must then call the initializers explicitly for each array element. For arrays of reference types all the elements are first initialized to null, which makes it obvious that you have to call new on each element. But it is easy to forget it for value types like structs.

var immutables = new ImmutableStruct[10];
immutables[0] = new ImmutableStruct(5, "hello", new Person());
immutables[1] = new ImmutableStruct(6, "world", new Student());
...

Starting with C# 7.2, you can use Read-only structs


Starting with C# 9.0 there is yet another option: the Init-Only Properties. Read-only fields and get-only auto implemented properties can be initialized in a constructor and in the field or property initializer but not in an object initializer.

This is the motivation for introducing init-only properties. They replace the set accessor by an init accessor. This extends the mutation phase from the actual object creation to the entire object construction phase including object initializers and with expressions (also a new C# 9.0 feature).

public string Name { get; init; }

Usage:

var x = new ImmutableStruct { Name = "John" }; // Okay

x.Name = "Sue"; // Compiler error CS8852: Init-only property or indexer
                // 'ImmutableStruct.Name' can only be assigned in an object
                // initializer, or on 'this' or 'base' in an instance constructor
                // or an 'init' accessor.

C# 10.0 (Visual Studio 2022) introduces Record structs, Parameterless struct constructors and field initializers.

readonly record struct Point(int X, int Y);

This generates X and Y properties with get and init accessors.

Olivier Jacot-Descombes
  • 104,806
  • 13
  • 138
  • 188
  • 3
    Field3 is still considered mutable because it is a reference type. You must return a copy of the object in the getter to keep the instance immutable. – Crowcoder Apr 21 '16 at 21:12
  • 4
    Or make the referenced object itself immutable. `String` is a reference type but immutable. Since structs represent value types, having a reference to a mutable reference type is probably not a good idea either. – Olivier Jacot-Descombes Apr 21 '16 at 21:15
  • Nice answer! It works and is up to date! By the way, if you have a property initializer, it can only occur in a class. Otherwise you receive the error: `"cannot have instance property or field initializers in structs"`. – Nicholas Miller Apr 21 '16 at 21:24
  • Ok, makes sense, since initialization code is not guaranteed to run (e.g. in arrays). – Olivier Jacot-Descombes Apr 21 '16 at 21:41
  • String operations return new strings, never change it in place. Making Field3 also immutable is fine, but what if you don't control that Type? Like a SqlConnection for instance? – Crowcoder Apr 21 '16 at 21:46
  • 2
    I think whether you consider the struct to be immutable or not depends on whether you are only interested in the reference equality of the object or if you mind the object state as well. It might be enough for you to know that the struct always returns the same connection object. – Olivier Jacot-Descombes Apr 21 '16 at 21:52
3

Keep your immutable data private:

struct ImmutableStruct
{
    private int field1;
    private string field2;
    private object field3;

    public ImmutableStruct(int f1, string f2, object f3)
    {
        field1 = f1;
        field2 = f2;
        field3 = f3;
    }

    public int Field1 => field1;
    public string Field2 => field2;
    public object Field3 => field3;
}

Or less cluttered:

struct ImmutableStruct
{
    public ImmutableStruct(int f1, string f2, object f3)
    {
        Field1 = f1;
        Field2 = f2;
        Field3 = f3;
    }

    public int Field1 { get; }
    public string Field2 { get; }
    public object Field3 { get; }
}
Quality Catalyst
  • 6,531
  • 8
  • 38
  • 62