【问题标题】:General purpose immutable classes in C#C# 中的通用不可变类
【发布时间】:2016-11-29 06:16:42
【问题描述】:

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

例如:

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

这是一个简单的例子,但是想象一个更大的类,有更多的成员。

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

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

注意:我不想将struct 用于reasons discussed elsewhere on this site


更新:我后来发现这在 F# 中被称为“复制和更新记录表达式”。

【问题讨论】:

  • 我会质疑是否需要拥有这些“setter”,因为您已经为所有内容编写了它们。实际上,您应该对类型执行复杂的操作,而不仅仅是任意“设置”一个值。对于一个通用的对,它只需要一个构造函数。
  • @Servy 抱歉,这是示例的限制。大多数修饰符更复杂,或特定于任务。请想象我举了一个更好的例子:)
  • 是的。使用 F#。现在是认真的。 C# 首先是有状态的 OOP 语言。虽然它确实具有一些函数式特性,但它远不是一种好的函数式语言。
  • 今天的语言中没有太多可以在不可变类型上构建“with”方法的语法。如果您查看 Roslyn 源代码,您会发现有很多这样的方法,其中包含样板代码。正如 Andrey 在上面的评论中所建议的那样,样板是由工具生成的;这本身就表明该语言可能缺少一个特征。设计团队意识到了缺点;如果您想提倡功能以改进这一点,请考虑参与 github 上的 Roslyn 论坛。
  • 如前所述,使用 F# 创建类,然后根据需要从 C# 中使用它们。

标签: c# oop design-patterns functional-programming immutability


【解决方案1】:

对于较大的类型,我将构建一个 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 并使用选项。

如果您有值类型/结构作为成员,则在 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 实现和序列化/反序列化。

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

【讨论】:

  • 这不适用于你想设置(with)为空的字段
  • @kofifus,也许阅读了答案?我明确说明了这一点,以及它真的没什么大不了的原因。如果您需要和可选值,请使用 Option 类型并获得大量类型安全
  • 我没看到你在哪里提到,如果 X 是一个引用类型(即 MyClass)并且我想将它“与”它为 null 我不能
  • “这确实意味着 null 变成了一个不可用的值,但如果你走的是功能路线,那么我假设你也试图避免 null 并使用选项。”
【解决方案2】:

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

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

你可以这样做:

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

【讨论】:

    【解决方案3】:

    我会将构建器模式与一些扩展方法结合使用。基本思路是有一个ToBuilder方法将A初始化为ABuilder,使用流畅的接口修改builder,然后完成builder得到新的实例。在某些情况下,这种方法甚至可以减少垃圾。

    不可变类:

    public sealed class A
    {
        readonly int x;
    
        public int X
        {
            get { return x; }
        }
    
        public A(int x)
        {
            this.x = x;
        }
    }
    

    构建器类:

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

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

    【讨论】:

      【解决方案4】:

      对于这个确切的情况,我使用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); 
          } 
       }
      

      【讨论】:

      • 您的字段(现在的属性)不再是 readonly,这不是太好。由于成员少,它的代码也会比 OP 的版本多,性能也稍差(MemberwiseClone 本身速度稍慢,另外它复制了至少一个字段太多,该委托也需要分配、调用和集)。对于大多数应用程序来说,所有这些点可能都无关紧要,但如果从头开始设计系统,请记住这一点。
      • 具有私有设置器的属性可以被认为是“只读的”(不是在谈论反射)。应在更广泛的背景下考虑性能。万一这很重要,您可以内联 With 方法以避免 lambda 分配。代码将变得更加乏味,但仍然比手动编码更不容易出错。实际上,在我的项目中,我使用的是具有 10 多个属性的类的内联版本,其好处是显而易见的。
      【解决方案5】:

      您可以使用以下模式(不知道它是否通过,但您要求较少冗余的版本,无论如何您可能会有所了解):

       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 方法中只设置一个新的。

      【讨论】:

      • 但是现在你在它们的构造函数之外改变类型。
      • 是的,这对你真的很糟糕吗?您询问了较少冗余模式以减少构造函数中的参数数量。对不起,如果我误解了你。
      • 这个答案的问题是,如果你想让事情变得可变,那么有一个更简单的方法来实现它:只需克隆整个对象并设置你想要更改的字段。跨度>
      • @sdgfsdh:这就是这个答案所展示的。克隆只是调用 copy-ctor。
      • @Great.And.Powerful.Oz 不,这是一个构造函数,后面跟着几个赋值。
      猜你喜欢
      • 2014-10-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-06-21
      • 2011-02-02
      • 1970-01-01
      • 2015-01-07
      相关资源
      最近更新 更多