简体   繁体   English

C# 中的通用不可变类

[英]General purpose immutable classes in C#

I am writing code in a functional style in C#.我在 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 .注意: 出于本站点其他地方讨论原因,我不想使用struct


Update: I have since discovered this is called a "copy and update record expression" in F#.更新:我发现这在 F# 中被称为“复制和更新记录表达式”。

For larger types I will build a With function that has arguments that all default to null if not provided:对于较大的类型,我将构建一个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
        );
}

Then use the named arguments feature of C# thus:然后使用 C# 的命名参数特性:

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.我发现 int 比许多 setter 方法更具吸引力。 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.这确实意味着null变成了一个不可用的值,但如果你走的是功能路线,那么我假设你也在试图避免null并使用选项。

If you have value-types/structs as members then make them Nullable in the With , for example:如果您将值类型/结构作为成员,则在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
        );
}

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

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 .如果您觉得编写With函数过于繁琐,那么您可以使用我的开源 C# 函数式编程库: 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.您必须在项目中包含LanguageExt.CoreLanguageExt.CodeGen The LanguageExt.CodeGen doesn't need to included with the final release of your project. LanguageExt.CodeGen不需要包含在项目的最终版本中。

The final bit of convenience comes with the [Record] attribute:最后一点方便来自[Record]属性:

[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.它将构建With函数,以及您的构造函数、解构函数、结构相等、结构排序、镜头、 GetHashCode实现、 ToString实现和序列化/反序列化。

Here's an overview of all of the Code-Gen features 以下是所有 Code-Gen 功能的概述

For this exact case I am using Object. MemberwiseClone()对于这个确切的情况,我使用的是Object. MemberwiseClone() Object. MemberwiseClone() . 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); 
    } 
 }

You may use following pattern (don't know if it pass, but you asked for less redundant version, anyway you may get an idea):您可以使用以下模式(不知道是否通过,但您要求减少冗余版本,无论如何您可能会有所了解):

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

This way you decrease amount of parameters in constructor of A by passing reference of existing object, set all properties and set then only one new inside some A method.通过这种方式,您可以通过传递现有对象的引用来减少 A 的构造函数中的参数数量,设置所有属性,然后在某个 A 方法中只设置一个新的。

I would use the builder pattern in combination with some extensions methods.我会将构建器模式与一些扩展方法结合使用。 The basic idea is to have a ToBuilder method to initialize an A into an ABuilder , modify the builder using a fluent interface, then complete the builder to get the new instance.基本思想是有一个ToBuilder方法将A初始化为ABuilder ,使用流畅的接口修改构建器,然后完成构建器以获取新实例。 This approach may even reduce garbage in some cases.在某些情况下,这种方法甚至可以减少垃圾。

The immutable class:不可变类:

public sealed class A
{
    readonly int x;

    public int X
    {
        get { return x; }
    }

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

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

Useful extension methods:有用的扩展方法:

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

It is used like this:它是这样使用的:

var a = new A(10);

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

It's not perfect.这并不完美。 You need to define an extra class with all of the properties of the original, but the usage syntax is quite clean and it maintains the simplicity of the origin type.您需要使用原始类型的所有属性定义一个额外的类,但使用语法非常干净,并且保持了原始类型的简单性。

There is an elegant efficient solution to this - see project With有一个优雅的高效解决这个-见项目

With With your class can simply become:随着With你的类可以简单地变为:

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

and you can do:你可以这样做:

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM