Abstract: 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. 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.
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:
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:
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.
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.
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 a such 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.
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.
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.
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”.
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 2 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 is it 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.
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.
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 bout thread safety issues.
Related concepts of interest recommended to the reader are “Value objects” and “Records” in C#”.
References
- https://en.wikipedia.org/wiki/Immutable_object
- https://ericlippert.com/2007/11/13/immutability-in-c-part-one-kinds-of-immutability/
- https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/struct
- https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/with-expression
- https://learn.microsoft.com/en-us/dotnet/csharp/write-safe-efficient-code
- https://stackoverflow.com/questions/57126134/is-this-a-defensive-copy-of-readonly-struct-passed-to-a-method-with-in-keyword
- https://levelup.gitconnected.com/defensive-copy-in-net-c-38ae28b828
- https://blog.paranoidcoding.com/2019/03/27/readonly-struct-breaking-change.html