.net 有没有一种简单的方法来创建一个类的不可变版本?

6l7fqoea  于 2023-05-02  发布在  .NET
关注(0)|答案(8)|浏览(168)

有没有一种简单的方法可以使一个示例成为不可变的?
让我们来做一个例子,我有一个类,里面有很多数据字段(只有数据,没有行为):

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。
那么,有没有什么自动的方法来生成这个不可变的类呢?还是另一种允许在创建期间可变但一旦初始化就不可变的方法?
我知道字段可以标记为“只读”,但是对象将在类外部初始化,并且将所有字段作为构造函数参数传递似乎是一个坏主意(太多参数)。

ou6hu8tu

ou6hu8tu1#

不,没有简单的方法使 * 任何 * 类型不可变,特别是如果你想要“深度”不变性(即。即,其中不可变对象不能通过不可变对象到达)。您必须显式地将类型设计为不可变的。使类型不可变的常用机制如下:

  • 声明(属性支持)字段readonly。(或者,从C# 6 / Visual Studio 2015开始,使用只读自动实现的属性。)
  • 不要公开属性setter,只公开getter。
  • 为了初始化(属性支持)字段,必须在构造函数中初始化它们。因此,将(属性)值传递给构造函数。
  • 不要暴露可变对象,例如基于默认可变类型的集合(如T[]List<T>Dictionary<TKey,TValue>等)。).

如果你需要公开集合,要么在一个防止修改的 Package 器中返回它们(例如:例如.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;
    …
}
kognpnkq

kognpnkq2#

嗯,我会列举我对这个问题的第一个想法。..

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

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

    // ...
}

**2.**我不同意在构造函数中有很多参数一定是个坏主意。
**3.**您可以在运行时生成另一个类型,该类型是您的类型的只读版本。我可以详细说明这一点,但我个人认为这是矫枉过正。

最好的,尤利安

r8uurelv

r8uurelv3#

作为另一种解决方案,您可以使用动态代理。Entity Framework 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();
    }
}
}
4ktjp1zp

4ktjp1zp4#

我建议使用抽象基类型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),然后根据需要修改它。

kmynzznz

kmynzznz5#

你在你的问题中暗示了一种方法,但我不确定这对你来说是否不是一种选择:

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

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

raogr8fs

raogr8fs6#

好吧,如果你有太多的参数,而你不想用参数构造函数。。。。这里有一个选择

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

            // ...
        }
aiazj4mn

aiazj4mn7#

还有一个选择声明一个具有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) { ... }
n1bvdmb6

n1bvdmb68#

一种简单(但不太安全)的方法是定义一个getter-only接口,并在构建对象后使用它。这类似于将List<T>转换为IReadOnlyList<T>。另一个好处是您不需要复制对象。是的,对象的用户仍然可以向下转换以获得可变对象(只要MyObject保持public),但我认为这足以表达“这是一个只读对象”的意图。“

interface IMyObject
{
    String Title { get; }
    String Author { get; }
}

class MyObject : IMyObject
{
    // ... unchanged ...
}

// treat returned object as read-only
IMyObject BuildMyObject()
{
    return new MyObject() { Title = "foo", Author = "bar" };
}

相关问题