是否有一种简单的方法可以使实例不可变?
让我们举个例子,我有一个包含大量数据字段的类(只有数据,没有行为):
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;
}
但是现在我已经完全创建了我的对象,我不想让对象变得可变(因为数据使用者永远不需要改变状态),所以我想要像{{3 }}:
var immutableObj = obj.AsReadOnly();
但是如果我想要这种行为,我需要创建另一个具有完全相同字段但没有setter的类。
那么有没有自动生成这个不可变类的方法呢?或者在创建过程中允许可变性的另一种方法,但是一旦初始化就不可变?
我知道字段可以标记为" readonly",但是对象将在类之外初始化,并且将所有字段作为构造函数参数传递似乎是一个坏主意(参数太多)。 / p>
答案 0 :(得分:8)
不,没有简单的方法可以使任何类型不可变,尤其是如果你想要“深度”不变性(即不能通过不可变对象到达可变对象的地方)。您必须明确地将类型设计为不可变的。使类型不可变的常用机制是:
readonly
。 (或者,从C#6 / Visual Studio 2015开始,使用read-only auto-implemented properties。)不要暴露属性设置器,只暴露getter。
要初始化(属性支持)字段,必须在构造函数中初始化它们。因此,将(property)值传递给构造函数。
不要暴露可变对象,例如基于默认类型可变类型的集合(如T[]
,List<T>
,Dictionary<TKey,TValue>
等。)。
如果您需要公开集合,请将其返回到阻止修改的包装器中(例如.AsReadOnly()
),或者至少返回内部集合的新副本。
使用Builder模式。以下示例对于模式正义来说太微不足道了,因为通常建议在需要创建非平凡对象图的情况下使用它;尽管如此,基本的想法是这样的:
class FooBuilder // mutable version used to prepare immutable objects
{
public int X { get; set; }
public List<string> Ys { get; set; }
public Foo Build()
{
return new Foo(x, ys);
}
}
class Foo // immutable version
{
public Foo(int x, List<string> ys)
{
this.x = x;
this.ys = new List<string>(ys); // create a copy, don't use the original
} // since that is beyond our control
private readonly int x;
private readonly List<string> ys;
…
}
答案 1 :(得分:3)
作为另一种解决方案,您可以使用动态代理。实体框架http://blogs.msdn.com/b/adonet/archive/2009/12/22/poco-proxies-part-1.aspx使用了相似的方法。以下是使用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<TFreezable>() where TFreezable : class, new()
{
var freezableInterceptor = new FreezableObjectInterceptor();
var proxy = _generator.CreateClassProxy<TFreezable>(freezableInterceptor);
return proxy;
}
}
class Program
{
static void Main(string[] args)
{
var rex = FreezableObjectFactory.CreateInstance<Pet>();
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();
}
}
}
答案 2 :(得分:2)
您在问题中暗示某种方式,但我不确定这不适合您:
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所提到的,我也非常喜欢使用构建器 - 特别是因为它使单元测试变得更容易。对于上面的类,您可以使用以下构建器: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 R. R. Martin", "Default Title"
MyObject obj2 = new MyObjectBuilder
.WithAuthor("George R. R. Martin")
.Build();
如果您需要在课程中添加新属性,那么回到单元测试会更容易,这些单元测试是从构建器而不是硬编码对象实例中消耗的(我不知道是什么打电话给它,请原谅我的条款。)
答案 3 :(得分:2)
嗯,我将列举我对此的第一个想法...
1。如果你唯一的担心是在你的程序集之外进行操作,请使用internal
setter。 internal
将使您的属性仅适用于同一程序集中的类。例如:
public class X
{
// ...
public int Field { get; internal set; }
// ...
}
2. 我不同意在构造函数中包含大量参数一定是个坏主意。
3. 您可以在运行时生成另一种类型的只读版本。我可以详细说明这一点,但我个人认为这是过度的。
Best,Iulian
答案 4 :(得分:2)
我建议使用抽象基类型ReadableMyObject
以及派生类型MutableMyObject
和ImmutableMyObject
。让所有类型的构造函数接受ReadableMyObject
,并让ReadableMyObject
的所有属性设置器在更新其支持字段之前调用抽象ThrowIfNotMutable
方法。此外,ReadableMyObject
支持公共抽象AsImmutable()
方法。
虽然这种方法需要为对象的每个属性编写一些样板,但这将是所需代码重复的范围。 MutableMyObject
和ImmutableMyObject
的构造函数只是将接收到的对象传递给基类构造函数。类MutableMyObject
应该实现ThrowIfNotMutable
什么都不做,AsImmutable()
返回new ImmutableMyObject(this);
。类ImmutableByObject
应该实现ThrowIfNotMutable
以抛出异常,并AsImmutable()
实现return this;
。
收到ReadableMyObject
并希望保留其内容的代码应调用其AsImmutable()
方法并存储生成的ImmutableMyObject
。收到ReadableMyObject
并希望稍微修改后的版本的代码应调用new MutableMyObject(theObject)
,然后根据需要进行修改。
答案 5 :(得分:0)
好吧,如果你有太多的参数,你不想做带参数的构造函数....这里有一个选项
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;
}
}
}
// ...
}
答案 6 :(得分:0)
这是另一种选择。声明一个具有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) { ... }