首页 文章

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

提问于
浏览
10

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

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

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的类 .

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

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

7 回答

  • 0

    不,没有简单的方法可以使任何类型成为不可变的,特别是如果你想要"deep" immutability(即不能通过不可变对象到达可变对象的地方) . 您必须明确地将类型设计为不可变的 . 使类型不可变的通常机制是:

    • 声明(属性支持)字段 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;
        …
    }
    
  • 2

    另一种解决方案是使用动态代理 . 实体框架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();
        }
    }
    }
    
  • 0

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

    1. 如果您唯一的担心是在装配体外进行操作,请使用 internal setter . internal 将使您的属性仅适用于同一程序集中的类 . 例如:

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

    2. 我不喜欢在构造函数中包含大量参数 .

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

    最好的,尤利安

  • 8

    我建议有一个抽象基类型 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) 然后根据需要进行修改 .

  • 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();
    

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

  • 2

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

    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;
                        }
                    }
                }
    
                // ...
            }
    
  • 3

    这是另一种选择 . 使用 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) { ... }
    

相关问题