我在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);
}
}
这是一个简单的例子,但想象一个更大的类,有更多的成员。
问题是构建这些修改后的副本非常冗长。大多数方法只更改一个值,但我必须将未更改值的所有传递给构造函数。
在使用修饰符方法构造不可变类时,是否存在避免所有此类样板的模式或技术?
注意:我不想对reasons discussed elsewhere on this site使用struct
。
更新:我发现这在F#中被称为“复制和更新记录表达式”。
答案 0 :(得分:12)
对于较大的类型,我将构建一个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
并使用选项。
如果您有值类型/结构作为成员,那么在Nullable
中创建With
,例如:
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
);
}
但请注意,这不是免费的,每次调用N
时都会有With
个空比较操作,其中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.Core
和LanguageExt.CodeGen
。 LanguageExt.CodeGen
不需要包含在项目的最终版本中。
答案 1 :(得分:2)
对于这个确切的情况,我使用的是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);
}
}
答案 2 :(得分:0)
您可以使用以下模式(不知道它是否通过,但您要求的冗余版本较少,无论如何您可能会得到一个想法):
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方法中只设置一个新的。
答案 3 :(得分:0)
我会将构建器模式与一些扩展方法结合使用。基本思想是使用ToBuilder
方法将A
初始化为ABuilder
,使用流畅的界面修改构建器,然后完成构建器以获取新实例。在某些情况下,这种方法甚至可以减少垃圾。
不可变类:
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 :(得分:0)
对此有一个优雅而有效的解决方案-请参见项目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);