Immutability is one of those programming subjects we hear about fairly regularly at the moment. It’s supposed to be one of those principles which make parallel programming easier, and since we’re currently in the world of 4-core machines, parallel is big right now.
Unfortunately it’s really hard in C# to create immutable objects. Or at least, C# isn’t really designed for it, so if you try to write an app using immutable data structures, you may find yourself writing a lot of boilerplate just to adhere to this particular paradigm. Which means at some point you’ll stop bothering, because it’s just too much work.
So, in the interests of reinventing the wheel, I’ve written a small code generator which creates immutable structs, which have ‘mutator’ functions. Mutators are methods which return whole new objects with just one property changed. For instance, if you have an immutable rectangle defined like this;
var firstRect = new Salamander.Models.Rectangle(100, 100, 100, 100);
then you can create a new object will all the same properties except one like this;
var secondRect = firstRect.WithTop(50);
Anyway, the system consists of two files; the generator itself [(Immutable.ttinc)](https://stevecooper.org/documents/Immutable.ttinc) and a file which defines the immutable types to create [(myImmutableTypes.tt)](https://stevecooper.org/documents/myImmutableTypes.tt). You’ll need to include them both in your c# project, then right-click the `.tt` file and choose ‘Run Custom Tool’
## To define your own custom types
You’ll need to look inside the `.tt` file and write a line which describes the type you’d like to create — basically just a namespace-qualified type name, and a list of fields;
GenerateImmutable(“Db.Person: string FirstName; string SecondName”, “A Person”);
The second parameter is a class-level xml comment
## Immutable.ttinc
<#+
private class Member
{
public string Type;
public string Name;
}
void GenerateImmutable(string definition)
{
GenerateImmutable(definition, string.Empty);
}
void GenerateImmutable(string definition, string comment)
{
var typeBuilder = new System.Text.StringBuilder();
int indexOfColon = definition.IndexOf(':');
if (indexOfColon == -1)
{
Error("No colon; expected, eg 'Rectangle: ‘”);
}
string ns = “”;
string typeName = definition.Split(‘:’)[0];
string members = definition.Split(‘:’)[1];
//
// NAMESPACE
//
int lastNsDot = typeName.LastIndexOf(“.”);
if (lastNsDot != -1)
{
ns = typeName.Substring(0, lastNsDot);
typeName = typeName.Substring(lastNsDot+1);
}
if (String.IsNullOrEmpty(ns) == false)
{
typeBuilder.Append(“namespace “).AppendLine(ns).AppendLine(“{“);
}
//
// TYPE DEFINITION
//
typeBuilder.Append(” ///
“);
typeBuilder.Append(” public partial struct “).AppendLine(typeName).AppendLine(” {“);
var memberList = members
.Split(‘;’)
.Select(x => x.Trim())
.Where(x => x.Length > 0)
.Select(x => x.Split(‘ ‘))
.Select(arr => new Member { Type = arr[0], Name=arr[1] })
;
//
// CONSTRUCTOR
//
typeBuilder.Append(” ///
“).AppendLine();
typeBuilder.Append(” public “).Append(typeName).Append(“(“);
foreach(var memberItem in memberList)
{
typeBuilder.Append(memberItem.Type).Append(” “).Append(memberItem.Name).Append(“,”);
}
typeBuilder.Length = typeBuilder.Length-1; // remove trailing comma
typeBuilder.AppendLine(“)”);
typeBuilder.AppendLine(” {“);
foreach(var memberItem in memberList)
{
typeBuilder
.Append(” this.”)
.Append(memberItem.Name)
.Append(” = “)
.Append(memberItem.Name)
.Append(“;”)
.AppendLine();
}
typeBuilder.AppendLine(” }”);
//
// fields
//
typeBuilder.AppendLine(” // fields”);
foreach(var memberItem in memberList)
{
typeBuilder
.AppendFormat(” public readonly {0} {1};”, memberItem.Type, memberItem.Name)
.AppendLine();
}
typeBuilder.AppendLine();
foreach(var memberItem in memberList)
{
typeBuilder
.AppendLine(” ///
“)
.AppendFormat(” public {0} With{1}({2} new{1})”, typeName, memberItem.Name, memberItem.Type)
.AppendLine(” {“)
.AppendFormat(” var newItem = new {0}(“, typeName)
;
foreach(var memberItemParam in memberList)
{
if (memberItem.Name == memberItemParam.Name)
{
typeBuilder.Append(“new”).Append(memberItem.Name).Append(“,”);
}
else
{
typeBuilder.Append(“this.”).Append(memberItemParam.Name).Append(“,”);
}
}
typeBuilder.Length = typeBuilder.Length-1; // remove trailing comma
typeBuilder.AppendLine(“);”);
typeBuilder.AppendLine(” return newItem;”);
typeBuilder.AppendLine(” }”);
typeBuilder.AppendLine();
}
typeBuilder.AppendLine(” }”);
if (String.IsNullOrEmpty(ns) == false)
{
typeBuilder.AppendLine(“}”);
}
Write(typeBuilder.ToString());
}
#>
## myImmutableTypes.tt
2 responses to “Immutable objects in C#”
Great. I have been bitten by non-immutable types in a couple different ways – once where I inadvertently changed the contents of a cached object (dumb on me, but immutables would have prevents such brash actions) and every time I expose “default” instances (primarily for lookup table entries associated with a property from one of my model classes).
Of course migrating the necessary code to immutable structs is a huge task in of itself… and the places where I’d find the most benefits for immutable types already contain rich class hierarchies that are difficult to clone without a DeepCopy implementation.
[…] ICloneable reference type is pretty much required, though any immutable reference type could get by without cloning because any changes gives you a new copy of that […]