General purpose immutable classes in C#
For larger types I will build a With
function that has arguments that all default to null
if not provided:
public sealed class A
{
public readonly X X;
public readonly Y Y;
public A(X x, Y y)
{
X = x;
Y = y;
}
public A With(X X = null, Y Y = null) =>
new A(
X ?? this.X,
Y ?? this.Y
);
}
Then use the named arguments feature of C# thus:
val = val.With(X: x);
val = val.With(Y: y);
val = val.With(X: x, Y: y);
I find int a much more attractive approach than lots of setter methods. It does mean that null
becomes an unusable value, but if you're going the functional route then I assume you're trying to avoid null
too and use options.
If you have value-types/structs as members then make them Nullable
in the With
, for example:
public sealed class A
{
public readonly int X;
public readonly int Y;
public A(int x, int y)
{
X = x;
Y = y;
}
public A With(int? X = null, int? Y = null) =>
new A(
X ?? this.X,
Y ?? this.Y
);
}
Note however, this doesn't come for free, there are N
null comparison operations per call to With
where N
is the number of arguments. I personally find the convenience worth the cost (which ultimately is negligible), however if you have anything that's particularly performance sensitive then you should fall back to bespoke setter methods.
If you find the tedium of writing the With
function too much, then you can use my open-source C# functional programming library: language-ext. The above can be done like so:
[With]
public partial class A
{
public readonly int X;
public readonly int Y;
public A(int x, int y)
{
X = x;
Y = y;
}
}
You must include the LanguageExt.Core
and LanguageExt.CodeGen
in your project. The LanguageExt.CodeGen
doesn't need to included with the final release of your project.
The final bit of convenience comes with the [Record]
attribute:
[Record]
public partial class A
{
public readonly int X;
public readonly int Y;
}
It will build the With
function, as well as your constructor, deconstructor, structural equality, structural ordering, lenses, GetHashCode
implementation, ToString
implementation, and serialisation/deserialisation.
Here's an overview of all of the Code-Gen features
For this exact case I am using Object. MemberwiseClone()
. The approach works for direct property updates only (because of a shallow cloning).
sealed class A
{
// added private setters for approach to work
public X x { get; private set;}
public Y y { get; private set;}
public class A(X x, Y y)
{
this.x = x;
this.y = y;
}
private A With(Action<A> update)
{
var clone = (A)MemberwiseClone();
update(clone);
return clone;
}
public A SetX(X nextX)
{
return With(a => a.x = nextX);
}
public A SetY(Y nextY)
{
return With(a => a.y = nextY);
}
}