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.Core
和 LanguageExt.CodeGen
。 LanguageExt.CodeGen
不需要包含在项目的最终版本中。
最后一点便利来自 [Record]
属性:
[Record]
public partial class A
{
public readonly int X;
public readonly int Y;
}
它将构建 With
函数,以及您的构造函数、解构函数、结构等式、结构顺序、镜头、GetHashCode
实现、ToString
实现和 serialisation/deserialisation.
对此有一个优雅高效的解决方案 - 请参阅项目 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);
我正在用 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.Core
和 LanguageExt.CodeGen
。 LanguageExt.CodeGen
不需要包含在项目的最终版本中。
最后一点便利来自 [Record]
属性:
[Record]
public partial class A
{
public readonly int X;
public readonly int Y;
}
它将构建 With
函数,以及您的构造函数、解构函数、结构等式、结构顺序、镜头、GetHashCode
实现、ToString
实现和 serialisation/deserialisation.
对此有一个优雅高效的解决方案 - 请参阅项目 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);