Click here to Skip to main content
16,021,169 members
Articles / Programming Languages / C#

C#11 - Immutable Object Pattern

Rate me:
Please Sign up or sign in to vote.
5.00/5 (2 votes)
6 Feb 2023CPOL6 min read 11.3K   42   8   4
Basic tutorial on Immutable Object Pattern along with C# examples
This is a beginner’s tutorial on Immutable Object Pattern with examples in C#. We discuss topics like “Internal immutability” vs “Observational Immutability”.

1. Immutable Object Definition

An Immutable Object (Internal Immutability) in C# is an object whose internal state cannot be changed after it is created. That is different from an ordinary object (Mutable Object) whose internal state typically can be changed after creation. The immutability of a C# object is enforced during compile time. Immutability is a compile-time constraint that signals what a programmer can do through the normal interface of the object.

There is a small confusion since sometimes under Immutable Object, the following definition is assumed:

An Immutable Object (Observational Immutability) ([2]) in C# is an object whose public state cannot be changed after it is created. In this case, we do not care if the internal state of an object changes over time if the public, the observable state is always the same. To the rest of the code, it always appears as the same object, because that is how it is being seen.

2. Utility for Finding Object Addresses

Since we are going on to show in our examples objects both on stack and heap, in order to better show differences in behavior, we developed a small utility that will give us the address of the objects in question, so by comparing addresses, it will be easily seen if we are talking about the same or different objects. The only problem is that our address-finding-utility has a limitation, that is, it works ONLY for objects on the heap that do not contain other objects on the heap (references). Therefore, we are forced to use only primitive values in our objects, and that is the reason why I needed to avoid using C# string and am using only char types.

Here is that address-finding-utility. We created two of them, one for class-based objects and another for struct-based objects. The problem is that we want to avoid boxing of struct-based objects since that would give us an address on the heap of the boxed object, not on the stack of the original object. We use Generics to block incorrect usage of the utilities.

C#
public static Tuple<string?, string?>
    GetMemoryAddressOfClass<T1, T2>(T1 o1, T2 o2)
    where T1 : class
    where T2 : class
{
    //using generics to block structs, that would be boxed
    //so we would get address of a boxed object, not struct
    //works only for objects that do not contain references
    // to other objects
    string? address1 = null;
    string? address2 = null;

    GCHandle? handleO1 = null;
    GCHandle? handleO2 = null;

    if (o1 != null)
    {
        handleO1 = GCHandle.Alloc(o1, GCHandleType.Pinned);
    }

    if (o2 != null)
    {
        handleO2 = GCHandle.Alloc(o2, GCHandleType.Pinned);
    }

    if (handleO1 != null)
    {
        IntPtr pointer1 = handleO1.Value.AddrOfPinnedObject();
        address1 = "0x" + pointer1.ToString("X");
    }

    if (handleO2 != null)
    {
        IntPtr pointer2 = handleO2.Value.AddrOfPinnedObject();
        address2 = "0x" + pointer2.ToString("X");
    }

    if (handleO1 != null)
    {
        handleO1.Value.Free();
    }

    if (handleO2 != null)
    {
        handleO2.Value.Free();
    }

    Tuple<string?, string?> result = 
        new Tuple<string?, string?>(address1, address2);

    return result;
}

public static unsafe string? 
    GetMemoryAddressOfStruct<T1>(ref T1 o1)
    where T1 : unmanaged
{
    //In order to satisfy this constraint "unmanaged" a type must be a struct
    //and all the fields of the type must be unmanaged
    //using ref, so I would not get a value copy
    string? result = null;
    fixed (void* pointer1 = (&o1))
    {
        result = $"0x{(long)pointer1:X}";
    }

    return result;
}

3. Example of a Mutable Object (Class-based)

Here is an example of a mutable object, class-based, meaning it is on the managed heap. And there is a sample execution and mutation. And then, there is the execution result:

C#
public class CarClass
{
    public CarClass(Char? brand, Char? model, int? year)
    {
        Brand = brand;
        Model = model;
        Year = year;
    }

    public Char? Brand { get; set; }
    public Char? Model { get; set; }
    public int? Year { get; set; }
    public override string ToString()
    {
        return $"Brand:{Brand}, Model:{Model}, Year:{Year}";
    }
}    
//============================================
//===Sample code==============================
//class based objects
Console.WriteLine("-----");
Console.WriteLine("Mutation of mutable class object");
CarClass car1 = new CarClass('T', 'C', 2022);
Console.WriteLine($"Before mutation: car1={car1}");
car1.Model = 'A';
Console.WriteLine($"After  mutation: car1={car1}");
Console.WriteLine();

//--assigning class based objects
Console.WriteLine("-----");
Console.WriteLine("Assignment of mutable class object");
Console.WriteLine("From addresses you can see that assignment created ");
Console.WriteLine("two references pointing to the same object on heap ");
CarClass car3 = new CarClass('T', 'C', 1991);
CarClass car4 = car3;

Tuple<string?, string?> addresses1 = Util.GetMemoryAddressOfClass(car3, car4);
Console.WriteLine($"Address car3={addresses1.Item1}, Address car4={addresses1.Item2}");

Console.WriteLine($"Before mutation: car3={car3}");
Console.WriteLine($"Before mutation: car4={car4}");
car4.Model = 'Y';
Console.WriteLine($"After  mutation: car3={car3}");
Console.WriteLine($"After  mutation: car4={car4}");
Console.WriteLine();
//============================================
//===Result of execution======================
/*
-----
Mutation of mutable class object
Before mutation: car1=Brand:T, Model:C, Year:2022
After  mutation: car1=Brand:T, Model:A, Year:2022

-----
Assignment of mutable class object
From addresses you can see that assignment created
two references pointing to the same object on heap
Address car3=0x21E4F160280, Address car4=0x21E4F160280
Before mutation: car3=Brand:T, Model:C, Year:1991
Before mutation: car4=Brand:T, Model:C, Year:1991
After  mutation: car3=Brand:T, Model:Y, Year:1991
After  mutation: car4=Brand:T, Model:Y, Year:1991
*/

As we know very well, Class types have reference semantics ([3]), and an assignment is just an assignment of references, pointing to the same object. So, the assignment just copied a reference, and we have the case of two references pointed to the one object on the heap, and it doesn’t matter which reference we used, that one object was mutated.

4. Example of a Mutable Object (Struct-based)

Here is an example of a mutable object, struct-based, meaning it is on the stack. And there is a sample execution and mutation. And then, there is the execution result:

C#
public struct CarStruct
{
    public CarStruct(Char? brand, Char? model, int? year)
    {
        Brand = brand;
        Model = model;
        Year = year;
    }

    public Char? Brand { get; set; }
    public Char? Model { get; set; }
    public int? Year { get; set; }
    public override string ToString()
    {
        return $"Brand:{Brand}, Model:{Model}, Year:{Year}";
    }
}

//=============================================
//===Sample code===============================
//struct based objects
Console.WriteLine("-----");
Console.WriteLine("Mutation of mutable struct object");
CarStruct car5 = new CarStruct('T', 'C', 2022);
Console.WriteLine($"Before mutation: car5={car5}");
car5.Model = 'Y';
Console.WriteLine($"After  mutation: car5={car5}");
Console.WriteLine();

//--assigning struct based objects
Console.WriteLine("-----");
Console.WriteLine("Assignment of mutable struct object");
Console.WriteLine("From addresses you can see that assignment created ");
Console.WriteLine("two different object on the stack ");
CarStruct car7 = new CarStruct('T', 'C', 1991);
CarStruct car8 = car7;

string? address7 = Util.GetMemoryAddressOfStruct(ref car7);
string? address8 = Util.GetMemoryAddressOfStruct(ref car8);
Console.WriteLine($"Address car7={address7}, Address car8={address8}");

Console.WriteLine($"Before mutation: car7={car7}");
Console.WriteLine($"Before mutation: car8={car8}");
car8.Model = 'M';
Console.WriteLine($"After  mutation: car7={car7}");
Console.WriteLine($"After  mutation: car8={car8}");
Console.WriteLine();
//=============================================
//===Result of execution=======================
/*
Mutation of mutable struct object
Before mutation: car5=Brand:T, Model:C, Year:2022
After  mutation: car5=Brand:T, Model:Y, Year:2022

-----
Assignment of mutable struct object
From addresses you can see that assignment created
two different object on the stack
Address car7=0x2A7F79E570, Address car8=0x2A7F79E560
Before mutation: car7=Brand:T, Model:C, Year:1991
Before mutation: car8=Brand:T, Model:C, Year:1991
After  mutation: car7=Brand:T, Model:C, Year:1991
After  mutation: car8=Brand:T, Model:M, Year:1991
*/

As we know very well, structs have value semantics ([3]), and on assignment, an instance of the type is copied. That is different behavior from class-based objects, that is reference types, that is shown above. As we can see, the assignment created a new instance of an object, so the mutation affected only the new instance.

5. Example of an Immutable Object (Struct-based)

5.1. Method 1 – Read-Only Properties

You can make an Immutable object of a struct-based type by marking all public properties with readonly keyword. Such properties can be mutated ONLY during the construction phase of an object, after that are immutable. Setting properties during the initialization phase of the object is not possible in this case.

C#
public struct CarStructI1
{
    public CarStructI1(Char? brand, Char? model, int? year)
    {
        Brand = brand;
        Model = model;
        Year = year;
    }

    public readonly Char? Brand { get;  }
    public readonly Char? Model { get;  }
    public readonly int? Year { get;  }
    public override readonly string ToString()
    {
        return $"Brand:{Brand}, Model:{Model}, Year:{Year}";
    }
}
    
//------------------------------------
CarStructI1 car10 = new CarStructI1('T', 'C', 2022);
//next line will not compile, since is readonly property
//car10.Model = 'Y';
//next line will not compile, can not initialize readonly property
//CarStructI1 car11 = new CarStructI1() { Brand = 'A', Model = 'A', Year = 2000 };    

5.2. Method 2 – Init-Setter Properties

You can make an Immutable object of a struct-based type by marking all public properties with init keyword for a setter. Such properties can be mutated ONLY during the construction phase of an object and during the initialization phase of the object, after that are immutable. Setting properties during the initialization phase of the object is possible in this case.

C#
public struct CarStructI2
{
    public CarStructI2(Char? brand, Char? model, int? year)
    {
        Brand = brand;
        Model = model;
        Year = year;
    }

    public Char? Brand { get; init; }
    public Char? Model { get; init; }
    public int? Year { get; init; }
    public override readonly string ToString()
    {
        return $"Brand:{Brand}, Model:{Model}, Year:{Year}";
    }
}

//---------------------------------------
CarStructI2 car20 = new CarStructI2('T', 'C', 2022);
//next line will not compile, since is readonly property
//car20.Model = 'Y';
//this works now
CarStructI2 car21 = new CarStructI2() { Brand = 'A', Model = 'A', Year = 2000 };

5.3. Method 3 – Read-Only Struct

You can make an Immutable object of a struct-based type by marking the struct with readonly keyword. In such a struct, all properties must be marked as readonly and can be mutated ONLY during the construction phase of an object, after that are immutable. Setting properties during the initialization phase of the object is not possible in this case. I see no difference in this case from Method 1 above when all properties/methods are marked as readonly except it is easily seen on the struct level definition what is the intent of that struct, that is struct creator planned it to be immutable from the start.

C#
public readonly struct CarStructI3
{
    public CarStructI3(Char? brand, Char? model, int? year)
    {
        Brand = brand;
        Model = model;
        Year = year;
    }

    public Char? Brand { get;  }
    public Char? Model { get;  }
    public int? Year { get;  }
    public override string ToString()
    {
        return $"Brand:{Brand}, Model:{Model}, Year:{Year}";
    }
}

//--------------------------------------
CarStructI3 car30= new CarStructI3('T', 'C', 2022);
//next line will not compile, since is readonly property
//car30.Model = 'Y';
//next line will not compile, can not initialize readonly property
//CarStructI3 car31 = new CarStructI1() { Brand = 'A', Model = 'A', Year = 2000 };

6. Example of an Immutable Object (Class-based)

6.1. Method 1 – Read-Only Properties

You can make an Immutable object of a class-based type by making all public properties as read-only by removing setters. Such properties can be mutated only by private members of the class. Setting properties during the initialization phase of the object is not possible in this case.

C#
 public class CarClassI1
{
    public CarClassI1(Char? brand, Char? model, int? year)
    {
        Brand = brand;
        Model = model;
        Year = year;
    }

    public CarClassI1()
    { }

    public Char? Brand { get;  }
    public Char? Model { get;  }
    public int? Year { get;  }
    public override string ToString()
    {
        return $"Brand:{Brand}, Model:{Model}, Year:{Year}";
    }
}

//----------------------------------
CarClassI1 car50 = new CarClassI1('T', 'C', 2022);
//next line will not compile, since is readonly property
//car50.Model = 'Y';
//next line will not compile, can not initialize readonly property
//CarClassI1 car51 = new CarClassI1() { Brand = 'A', Model = 'A', Year = 2000 };

6.2. Method 2 – Init-Setter Properties

You can make an Immutable object of a class-based type by marking all public properties with init keyword for a setter. Such properties can be mutated ONLY during the construction phase of an object and during the initialization phase of the object, after that are immutable. Setting properties during the initialization phase of the object is possible in this case.

C#
public class CarClassI2
{
    public CarClassI2(Char? brand, Char? model, int? year)
    {
        Brand = brand;
        Model = model;
        Year = year;
    }

    public CarClassI2()
    { }

    public Char? Brand { get; init; }
    public Char? Model { get; init; }
    public int? Year { get; init; }
    public override string ToString()
    {
        return $"Brand:{Brand}, Model:{Model}, Year:{Year}";
    }
}

//------------------------------------------
CarClassI2 car60 = new CarClassI2('T', 'C', 2022);
//next line will not compile, since is readonly property
//car60.Model = 'Y';
//this works now
CarClassI2 car61 = new CarClassI2() { Brand = 'A', Model = 'A', Year = 2000 };

7. Internal Immutability vs Observational Immutability

The above cases were all cases of Internal Immutability Immutable objects. Let us give an example of one Observational Immutability Immutable object. The following is such an example. We basically cache the result of a long price calculation. The object always reports the same state, so it satisfies Observational Immutability, but its internal state changes, so it does not satisfy Internal Immutability.

C#
public class CarClassI1
{
    public CarClassI1(Char? brand, Char? model, int? year)
    {
        Brand = brand;
        Model = model;
        Year = year;
    }
    public Char? Brand { get; }
    public Char? Model { get; }
    public int? Year { get; }
    public int? Price
    {
        get
        {
            // not thread safe
            if (_price== null)
            {
                LongPriceCalcualtion();
            }
            return _price;  
        }
    }

    private int? _price = null;

    private void LongPriceCalcualtion()
    {
        _price = 0;
        Thread.Sleep(1000); //long features calculation
        _price += 10_000;
        Thread.Sleep(1000); //long engine price calculation
        _price += 10_000;
        Thread.Sleep(1000); //long tax calculation
        _price += 10_000;
    }
    public override string ToString()
    {
        return $"Brand:{Brand}, Model:{Model}, Year:{Year}, Price:{Price}";
    }
}

//=============================================
//===Sample code===============================
CarClassI1 car50 = new CarClassI1('T', 'C', 2022);

Console.WriteLine($"The 1st object state: car50={car50}");
Console.WriteLine($"The 2nd object state: car50={car50}");

//=============================================
//===Result of execution=======================
/*
The 1st object state: car50=Brand:T, Model:C, Year:2022, Price:30000
The 2nd object state: car50=Brand:T, Model:C, Year:2022, Price:30000
*/

8. Thread Safety and Immutability

Internal Immutability Immutable objects are trivially thread-safe. That follows from the simple logic that all shared resources are read-only, so there is no chance of threads interfering with each other.

Observational Immutability Immutable objects are not necessarily thread-safe, and the above example shows that. Getting state invokes some private thread-unsafe methods, and the final result is not thread-safe. If approached from two different threads, the above object might report different states.

9. Immutable Object (Struct-based) and Nondestructive Mutation

If you want to reuse an Immutable object, you are free to reference it as many times as you want, because it is guaranteed not to change. But what if you want to reuse some of the data of an Immutable object, but modify it a bit? That is why they invented Nondestructive Mutation. In C# language, now you can use with keyword to do it. Typically, you would want to preserve most of the state of an Immutable object but change just some properties. Here is how it can be done in C#10 and after that.

C#
public struct CarStructI2
{
    public CarStructI2(Char? brand, Char? model, int? year)
    {
        Brand = brand;
        Model = model;
        Year = year;
    }

    public Char? Brand { get; init; }
    public Char? Model { get; init; }
    public int? Year { get; init; }
    public override readonly string ToString()
    {
        return $"Brand:{Brand}, Model:{Model}, Year:{Year}";
    }
}

//=============================================
//===Sample code===============================
//struct based objects
Console.WriteLine("-----");
Console.WriteLine("Nondestructive Mutation of immutable struct object");
CarStructI2 car7 = new CarStructI2('T', 'C', 1991);
CarStructI2 car8 = car7 with { Brand = 'A' };

string? address1 = Util.GetMemoryAddressOfStruct(ref car7);
string? address2 = Util.GetMemoryAddressOfStruct(ref car8);
Console.WriteLine($"Address car7={address1}, Address car8={address2}");

Console.WriteLine($"State: car7={car7}");
Console.WriteLine($"State: car8={car8}");
Console.WriteLine();

//=============================================
//===Result of execution=======================
/*
-----
Nondestructive Mutation of immutable struct object
Address car7=0xC4A4FCE420, Address car8=0xC4A4FCE410
State: car7=Brand:T, Model:C, Year:1991
State: car8=Brand:A, Model:C, Year:1991
*/

10. Immutable Object (Class-based) and Nondestructive Mutation

For class-based Immutable objects, they didn’t extend the C# language with the new with keyword, but the same functionality can still be easily custom programmed. Here is an example:

C#
public class CarClassI4
{
    public CarClassI4(Char? brand, Char? model, int? year)
    {
        Brand = brand;
        Model = model;
        Year = year;
    }

    public Char? Brand { get;  }
    public Char? Model { get;  }
    public int? Year { get;  }

    public CarClassI4 NondestructiveMutation 
        ( Char? Brand=null,  Char? Model = null, int? Year=null)
    {
        return new CarClassI4(
            Brand ?? this.Brand, Model ?? this.Model, Year ?? this.Year);
    }
    public override string ToString()
    {
        return $"Brand:{Brand}, Model:{Model}, Year:{Year}";
    }
}

//=============================================
//===Sample code===============================
//class based objects
Console.WriteLine("-----");
Console.WriteLine("Nondestructive Mutation of immutable class object");
CarClassI4 car1 = new CarClassI4('T', 'C', 1991);
CarClassI4 car2 = car1.NondestructiveMutation(Model:'M');


Tuple<string?, string?> addresses2 = Util.GetMemoryAddressOfClass(car1, car2);
Console.WriteLine($"Address car1={addresses2.Item1}, Address car2={addresses2.Item2}");

Console.WriteLine($"State: car1={car1}");
Console.WriteLine($"State: car2={car2}");
Console.WriteLine();
//=============================================
//===Result of execution=======================
/*
-----
Nondestructive Mutation of immutable class object
Address car1=0x238EED63FA8, Address car2=0x238EED63FC8
State: car1=Brand:T, Model:C, Year:1991
State: car2=Brand:T, Model:M, Year:1991
*/

11. Conclusion

Immutable object pattern is very popular and is frequently used. Here, we gave an introduction to creating immutable structs and classes in C# and some interesting examples.

We discussed Internal immutability vs Observational Immutability and talked about thread safety issues.

Related concepts of interest recommended to the reader are Value objects and Records in C#.

12. References

13. History

  • 7th February, 2023: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Serbia Serbia
Mark Pelf is the pen name of just another Software Engineer from Belgrade, Serbia.
My Blog https://markpelf.com/

Comments and Discussions

 
QuestionInteresting but, where is this related to C# 11 ? Pin
tbayart23-Feb-23 2:08
professionaltbayart23-Feb-23 2:08 
AnswerRe: Interesting but, where is this related to C# 11 ? Pin
Mark Pelf 27-Feb-23 16:32
mvaMark Pelf 27-Feb-23 16:32 
GeneralReal world case Pin
OKarpov7-Feb-23 22:53
professionalOKarpov7-Feb-23 22:53 
QuestionGood topic Pin
cmk7-Feb-23 3:26
cmk7-Feb-23 3:26 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.