是否有一种简单的方法来创建一个类的不可变版本?

有一种简单的方法可以使实例不可变吗?

让我们举个例子,我有一个包含大量数据字段的类(只有数据,没有行为):

class MyObject { // lots of fields painful to initialize all at once // so we make fields mutable : public String Title { get; set; } public String Author { get; set; } // ... } 

创建示例:

 MyObject CreationExample(String someParameters) { var obj = new MyObject { Title = "foo" // lots of fields initialization }; // even more fields initialization obj.Author = "bar"; return obj; } 

但是现在我已经完全创建了我的对象,我不希望该对象再次变得可变(因为数据使用者永远不需要改变状态),所以我想要像List.AsReadOnly这样的东西:

 var immutableObj = obj.AsReadOnly(); 

但是如果我想要这种行为,我需要创建另一个具有完全相同字段但没有setter的类。

那么有没有自动生成这个不可变类的方法呢? 或者在创建过程中允许可变性的另一种方法,但一旦初始化就不可变?

我知道字段可以标记为“只读”,但是对象将在类之外初始化,并且将所有字段作为构造函数参数传递似乎是一个坏主意(参数太多)。

不,没有简单的方法可以使任何类型不可变,尤其是如果你想要“深层”不变性(即不能通过不可变对象到达可变对象)。 您必须明确地将类型设计为不可变的。 使类型不可变的常用机制是:

  • readonly声明(属性支持)字段。 (或者,从C#6 / Visual Studio 2015开始,使用只读自动实现的属性 。)
  • 不要暴露属性设置器,只暴露getter。

  • 要初始化(属性支持)字段,必须在构造函数中初始化它们。 因此,将(property)值传递给构造函数。

  • 不要暴露可变对象,例如基于默认类型可变类型的集合(如T[]ListDictionary等)。

    如果需要公开集合,可以在一个阻止修改的包装器中返回它们(例如.AsReadOnly() ),或者至少返回一个内部集合的新副本。

  • 使用Builder模式。 以下示例对于模式正义来说太微不足道了,因为通常建议在需要创建非平凡对象图的情况下使用它; 尽管如此,基本思想是这样的:

     class FooBuilder // mutable version used to prepare immutable objects { public int X { get; set; } public List Ys { get; set; } public Foo Build() { return new Foo(x, ys); } } class Foo // immutable version { public Foo(int x, List ys) { this.x = x; this.ys = new List(ys); // create a copy, don't use the original } // since that is beyond our control private readonly int x; private readonly List ys; … } 

另一种解决方案是您可以使用动态代理。 entity framework使用了相似的方法http://blogs.msdn.com/b/adonet/archive/2009/12/22/poco-proxies-part-1.aspx 。 以下是使用Castle.DynamicProxy框架完成此操作的Castle.DynamicProxy 。 此代码基于Castle Dynamic代理的原始示例( http://kozmic.net/2008/12/16/castle-dynamicproxy-tutorial-part-i-introduction/

 namespace ConsoleApplication8 { using System; using Castle.DynamicProxy; internal interface IFreezable { bool IsFrozen { get; } void Freeze(); } public class Pet : IFreezable { public virtual string Name { get; set; } public virtual int Age { get; set; } public virtual bool Deceased { get; set; } bool _isForzen; public bool IsFrozen => this._isForzen; public void Freeze() { this._isForzen = true; } public override string ToString() { return string.Format("Name: {0}, Age: {1}, Deceased: {2}", Name, Age, Deceased); } } [Serializable] public class FreezableObjectInterceptor : IInterceptor { public void Intercept(IInvocation invocation) { IFreezable obj = (IFreezable)invocation.InvocationTarget; if (obj.IsFrozen && invocation.Method.Name.StartsWith("set_", StringComparison.OrdinalIgnoreCase)) { throw new NotSupportedException("Target is frozen"); } invocation.Proceed(); } } public static class FreezableObjectFactory { private static readonly ProxyGenerator _generator = new ProxyGenerator(new PersistentProxyBuilder()); public static TFreezable CreateInstance() where TFreezable : class, new() { var freezableInterceptor = new FreezableObjectInterceptor(); var proxy = _generator.CreateClassProxy(freezableInterceptor); return proxy; } } class Program { static void Main(string[] args) { var rex = FreezableObjectFactory.CreateInstance(); rex.Name = "Rex"; Console.WriteLine(rex.ToString()); Console.WriteLine("Add 50 years"); rex.Age += 50; Console.WriteLine("Age: {0}", rex.Age); rex.Deceased = true; Console.WriteLine("Deceased: {0}", rex.Deceased); rex.Freeze(); try { rex.Age++; } catch (Exception ex) { Console.WriteLine("Oups. Can't change that anymore"); } Console.WriteLine("--- press enter to close"); Console.ReadLine(); } } } 

嗯,我将列举我对此的第一个想法……

1.如果您唯一的担心是在assembly体外进行操作,请使用internal制定者。 internal将仅使您的属性可用于同一程序集中的类。 例如:

 public class X { // ... public int Field { get; internal set; } // ... } 

2.我不同意在构造函数中包含大量参数一定是个坏主意。

3.您可以在运行时生成另一种类型的只读版本。 我可以详细说明这一点,但我个人认为这是过度的。

最好的,尤利安

我建议有一个抽象基类型ReadableMyObject以及派生类型MutableMyObjectImmutableMyObject 。 让所有类型的构造函数接受ReadableMyObject ,并让ReadableMyObject所有属性设置器在更新其支持字段之前调用抽象ThrowIfNotMutable方法。 另外,让ReadableMyObject支持公共抽象AsImmutable()方法。

虽然这种方法需要为对象的每个属性编写一些样板,但这将是所需代码重复的范围。 MutableMyObjectImmutableMyObject的构造函数将简单地将接收到的对象传递给基类构造函数。 MutableMyObject类应该实现ThrowIfNotMutable什么都不做,而AsImmutable()返回new ImmutableMyObject(this);ImmutableByObject类应该实现ThrowIfNotMutable来抛出exception,而AsImmutable() return this;

接收ReadableMyObject并希望保留其内容的代码应调用其AsImmutable()方法并存储生成的ImmutableMyObject 。 接收ReadableMyObject并希望稍微修改版本的代码应该调用new MutableMyObject(theObject) ,然后根据需要修改它。

您在问题中暗示了一种方式,但我不确定这不是您的选择:

 class MyObject { // lots of fields painful to initialize all at once // so we make fields mutable : public String Title { get; protected set; } public String Author { get; protected set; } // ... public MyObject(string title, string author) { this.Title = title; this.Author = author; } } 

由于构造函数是操作Author和Title的唯一方法,因此该类在构造后实际上是不可变的。

编辑:

正如stakx所提到的,我也非常喜欢使用构建器 – 特别是因为它使unit testing变得更容易。 对于上面的类,您可以拥有一个构建器,例如:

 public class MyObjectBuilder { private string _author = "Default Author"; private string _title = "Default title"; public MyObjectBuilder WithAuthor(string author) { this._author = author; return this; } public MyObjectBuilder WithTitle(string title) { this._title = title; return this; } public MyObject Build() { return new MyObject(_title, _author); } } 

这样,您可以使用默认值构造对象,或者根据需要覆盖它们,但构建后无法更改MyObject的属性。

 // Returns a MyObject with "Default Author", "Default Title" MyObject obj1 = new MyObjectBuilder.Build(); // Returns a MyObject with "George RR Martin", "Default Title" MyObject obj2 = new MyObjectBuilder .WithAuthor("George RR Martin") .Build(); 

如果你需要为你的类添加新的属性,那么回到你从构建器消耗的unit testing而不是从硬编码的对象实例化中更容易(我不知道该怎么称呼它,所以请原谅我的术语) 。

好吧,如果你有太多的参数,你不想做带参数的构造函数….这里有一个选项

 class MyObject { private string _title; private string _author; public MyObject() { } public String Title { get { return _title; } set { if (String.IsNullOrWhiteSpace(_title)) { _title = value; } } } public String Author { get { return _author; } set { if (String.IsNullOrWhiteSpace(_author)) { _author = value; } } } // ... } 

这是另一种选择。 声明具有protected成员的基类和派生类,该类重新定义成员以使其成为公共成员。

 public abstract class MyClass { public string Title { get; protected set; } public string Author { get; protected set; } public class Mutable : MyClass { public new string Title { get { return base.Title; } set { base.Title = value; } } public new string Author { get { return base.Author; } set { base.Author = value; } } } } 

创建代码将使用派生类。

 MyClass immutableInstance = new MyClass.Mutable { Title = "Foo", "Author" = "Your Mom" }; 

但是对于所有需要不变性的情况,请使用基类:

 void DoSomething(MyClass immutableInstance) { ... }