C# 中的通用不可变 类

General purpose immutable classes in C#

我正在用 C# 编写函数式代码。我的许多 classes 是不可变的,具有返回实例的修改副本的方法。

例如:

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);
    }
}

这是一个微不足道的例子,但想象一个更大的 class,有更多的成员。

问题是构建这些修改后的副本非常冗长。大多数方法只更改一个值,但我必须将 all 未更改的值传递给构造函数。

在使用修饰符方法构造不可变 classes 时,是否有一种模式或技术可以避免所有这些样板代码?

注意:我不想对 reasons discussed elsewhere on this site 使用 struct


更新:我发现这在 F# 中称为 "copy and update record expression"。

你可以使用下面的模式(不知道是否通过,但是你要求的版本不那么冗余,反正你可能会有一个想法):

 public class Base
    {
        public int x { get; protected set; }
        public int y { get; protected  set; }

        /// <summary>
        /// One constructor which set all properties
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        public Base(int x, int y)
        {
            this.x = x;
            this.y = y;
        }

        /// <summary>
        /// Constructor which init porperties from other class
        /// </summary>
        /// <param name="baseClass"></param>
        public Base(Base baseClass) : this(baseClass.x, baseClass.y)
        {
        }

        /// <summary>
        ///  May be more secured constructor because you always can check input parameter for null
        /// </summary>
        /// <param name="baseClass"></param>
        //public Base(Base baseClass)
        //{
        //    if (baseClass == null)
        //    {
        //        return;
        //    }

        //    this.x = baseClass.x;
        //    this.y = baseClass.y;
        //}
    }

    public sealed class A : Base
    {
        // Don't know if you really need this one
        public A(int x, int y) : base(x, y)
        {
        }

        public A(A a) : base(a)
        {
        }

        public A SetX(int nextX)
        {
            // Create manual copy of object and then set another value
            var a = new A(this)
            {
                x = nextX
            };

            return a;
        }

        public A SetY(int nextY)
        {
            // Create manual copy of object and then set another value
            var a = new A(this)
            {
                y = nextY
            };

            return a;
        }
    }

通过这种方式,您可以通过传递现有对象的引用来减少 A 的构造函数中的参数数量,设置所有属性,然后只在某个 A 方法中设置一个新的。

对于这种情况,我使用的是 Object. MemberwiseClone()。该方法仅适用于直接 属性 更新(因为浅克隆)。

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); 
    } 
 }

我会结合使用构建器模式和一些扩展方法。基本思想是使用 ToBuilder 方法将 A 初始化为 ABuilder,使用流畅的界面修改构建器,然后完成构建器以获取新实例。在某些情况下,这种方法甚至可以减少垃圾。

不可变的class:

public sealed class A
{
    readonly int x;

    public int X
    {
        get { return x; }
    }

    public A(int x)
    {
        this.x = x;
    }
}

建设者class:

public sealed class ABuilder
{
    public int X { get; set; }

    public ABuilder(A a)
    {
        this.X = a.X;
    }

    public A Build()
    {
        return new A(X);
    }
}

有用的扩展方法:

public static class Extensions
{
    public static ABuilder With(this ABuilder builder, Action<ABuilder> action)
    {
        action(builder);

        return builder;
    }

    public static ABuilder ToBuilder(this A a)
    {
        return new ABuilder(a) { X = a.X };
    }
}

它是这样使用的:

var a = new A(10);

a = a.ToBuilder().With(i => i.X = 20).Build();

它并不完美。您需要使用原始类型的所有属性定义一个额外的 class,但使用语法非常简洁,并且保持了原始类型的简单性。

对于较大的类型,我将构建一个 With 函数,如果未提供,所有参数都默认为 null

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
        );
}

然后使用 C# 的命名参数功能:

val = val.With(X: x);

val = val.With(Y: y);

val = val.With(X: x, Y: y);

我发现 int 比许多 setter 方法更有吸引力。这确实意味着 null 变成了一个不可用的值,但是如果您要走功能路线,那么我假设您也在尝试避免 null 并使用选项。

如果您有 value-types/structs 作为成员,则在 With 中将他们设为 Nullable,例如:

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
        );
}

但是请注意,这不是免费的,每次调用 With 都会有 N 次空值比较操作,其中 N 是参数的数量。我个人认为这种便利值得付出代价(最终可以忽略不计),但是如果您有任何对性能特别敏感的东西,那么您应该回退到定制的 setter 方法。

如果你觉得写 With 函数太乏味了,那么你可以使用我的 open-source C# functional programming library: language-ext。以上可以这样完成:

[With]
public partial class A
{
    public readonly int X;
    public readonly int Y;

    public A(int x, int y)
    {
        X = x;
        Y = y;
    }
}

您必须在项目中包含 LanguageExt.CoreLanguageExt.CodeGenLanguageExt.CodeGen 不需要包含在项目的最终版本中。

最后一点便利来自 [Record] 属性:

[Record]
public partial class A
{
    public readonly int X;
    public readonly int Y;
}

它将构建 With 函数,以及您的构造函数、解构函数、结构等式、结构顺序、镜头、GetHashCode 实现、ToString 实现和 serialisation/deserialisation.

Here's an overview of all of the Code-Gen features

对此有一个优雅高效的解决方案 - 请参阅项目 With

有了 With,您的 class 可以简单地变成:

sealed class A : IImmutable 
{
    public readonly X x;
    public readonly Y y;

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

你可以这样做:

using System.Immutable;
var o = new A(0, 0);
var o1 = o.With(a => a.y, 5);