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

时间:2014-09-29 17:35:20

标签: c# .net class immutability

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

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

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>

7 个答案:

答案 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以及派生类型MutableMyObjectImmutableMyObject。让所有类型的构造函数接受ReadableMyObject,并让ReadableMyObject的所有属性设置器在更新其支持字段之前调用抽象ThrowIfNotMutable方法。此外,ReadableMyObject支持公共抽象AsImmutable()方法。

虽然这种方法需要为对象的每个属性编写一些样板,但这将是所需代码重复的范围。 MutableMyObjectImmutableMyObject的构造函数只是将接收到的对象传递给基类构造函数。类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) { ... }