【问题标题】:Is there an easy way to make an immutable version of a class?有没有一种简单的方法来制作一个类的不可变版本?
【发布时间】:2023-04-09 03:07:02
【问题描述】:

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

举个例子,我有一个类包含很多数据字段(只有数据,没有行为):

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

那么有没有自动生成这个不可变类的方法呢?或者另一种允许在创建期间可变但在初始化后不可变的方法?

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

【问题讨论】:

  • 您应该编辑您的问题以明确表示您对解决问题的解决方案感兴趣,即您有很多不想在构造函数调用中初始化的字段(对于某些原因)。在我的脑海中,我想说如果你真的想这样做,你可以使用一个可变的帮助类,它可以保存所有数据项来构造不可变类。初始化那个助手类,然后通过传入助手类的实例来构造你的不可变类。我不确定这是否是一个很好的模式。
  • 搜索 Eric Lippert 的优秀系列作品“popsicle immutability”
  • @AlexeiLevenkov:我看到 Lippert 提到“Popsicle 不变性”的博客文章(这是一个很棒的想法),但我没有看到任何文章中他谈到了实现它的模式.
  • 相关问题:How to freeze a popsicle in .NET (make a class immutable)。 @Alexei:+1 供参考。
  • 如果一个干净的只读实现的参数太多,也许你的类需要重构?

标签: c# .net class immutability


【解决方案1】:

不,没有简单的方法可以使 any 类型不可变,尤其是如果您想要“深度”不可变性(即无法通过不可变对象访问可变对象)。您必须明确地将您的类型设计为不可变的。使类型不可变的常用机制如下:

  • 声明(属性支持)字段readonly。 (或者,从 C# 6 / Visual Studio 2015 开始,使用 read-only auto-implemented properties。)
  • 不公开属性设置器,只公开获取器。

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

  • 不要公开可变对象,例如基于默认可变类型(如T[]List<T>Dictionary<TKey,TValue> 等)的集合。

    如果您需要公开集合,请将它们返回到防止修改的包装器中(例如 .AsReadOnly()),或者至少返回内部集合的新副本。

  • 使用建造者模式。下面的例子太琐碎了,无法做到模式公正,因为通常建议在需要创建非平凡对象图的情况下使用它;然而,基本的想法是这样的:

    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】:

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

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

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

    2.我不同意在你的构造函数中有很多参数是一个坏主意。

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

    最好的,尤利安

    【讨论】:

    • 此外,如果可能的话,您可以尝试重构为更小的类。前任。代替“Address1, address2, city, state, zip”属性,创建一个代表“Address”的对象,并将所有这些属性作为属性。当然,这可能会使对象构造函数变得更加困难,但这就是您可以使用构建器(我相信其他模式)的目的。
    【解决方案3】:

    作为另一种解决方案,您可以使用动态代理。 Entity Framework http://blogs.msdn.com/b/adonet/archive/2009/12/22/poco-proxies-part-1.aspx 使用了类似的方法。这是使用Castle.DynamicProxy 框架的示例。此代码基于 Castle Dynamic proxy (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();
        }
    }
    }
    

    【讨论】:

    • 你也可以使用ProxyGenerator.CreateClassProxyWithTarget(existingObject,interceptor)。然后,您将不需要实现 IFreezable 接口并为 IsFrozen 添加任何其他逻辑。在这种情况下,您所要做的就是预先设置所有必填字段,然后调用生成器以返回代理。
    【解决方案4】:

    我建议使用抽象基类型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】:

      您在问题中暗示了一种方式,但我不确定这是否不适合您:

      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;
          }
      }
      

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

      编辑:

      正如 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();
      

      如果您需要向您的类添加新属性,返回到使用构建器而不是硬编码对象实例化的单元测试要容易得多(我不知道该怎么称呼它,所以请原谅我的条件)。

      【讨论】:

      • 有很多字段(超过 10 个),将所有字段作为参数传递似乎不是一个好主意。 (大多数字段都有可接受的默认值)
      • 此解决方案适用于这种特殊情况,但不适用于一般情况。假设我们有一个class C 和一个属性List&lt;string&gt; Foos { get; protected set; }。即使只有它的 getter 是公共的,C 类型的对象也可以修改,因为它们的 Foos 集合可以修改:var c = new C(…); c.Foos.Add(new Foo(…)); 也就是说,这些对象不是“深度”不可变的。
      • @lulian 似乎不是个好主意?有用。您可以将默认值指定为 null。
      • 我的小回答来回产生了这么多...去我吗? :O
      • @stakx 这并不总是关于你。 +1 是关于答案而不是你的切线。
      【解决方案6】:

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

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

      【讨论】:

      • 有很多字段(超过 10 个),将它们全部作为参数传递似乎不是一个好主意。 (大多数字段都有可接受的默认值)
      • @anopse 好吧,我更新了我的答案......不是很好的解决方案,但可能会对你有所帮助
      • 那怎么是不可变的?
      • 一旦设置了属性,它们将不会被再次设置,因为它会在设置之前检查 IsNullOrWhiteSpace。无论如何,这是一个穷人不可变类....你别无选择
      • 好的,但是它将如何处理默认值?
      【解决方案7】:

      这是另一种选择。声明一个包含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) { ... }
      

      【讨论】:

        猜你喜欢
        • 2013-11-24
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-09-19
        • 1970-01-01
        • 1970-01-01
        • 2016-08-30
        相关资源
        最近更新 更多