Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

General purpose immutable classes in C#

I am writing code in a functional style in C#. Many of my classes are immutable with methods for returning a modified copy of an instance.

For example:

sealed class A
{
    readonly X x;
    readonly Y y;

    public class A(X x, Y y)
    {
        this.x = x;
        this.y = y;
    }

    public A SetX(X nextX)
    {
        return new A(nextX, y);
    }

    public A SetY(Y nextY)
    {
        return new A(x, nextY);
    }
}

This is a trivial example, but imagine a much bigger class, with many more members.

The problem is that constructing these modified copies is very verbose. Most of the methods only change one value, but I have to pass all of the unchanged values into the constructor.

Is there a pattern or technique to avoid all of this boiler-plate when constructing immutable classes with modifier methods?

Note: I do not want to use a struct for reasons discussed elsewhere on this site.


Update: I have since discovered this is called a "copy and update record expression" in F#.

like image 382
sdgfsdh Avatar asked Jul 25 '16 19:07

sdgfsdh


2 Answers

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

like image 158
louthster Avatar answered Oct 18 '22 19:10

louthster


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); 
    } 
 }
like image 25
dadhi Avatar answered Oct 18 '22 19:10

dadhi