C# 有没有一种简单的方法来创建类的不可变版本?

C# 有没有一种简单的方法来创建类的不可变版本?,c#,.net,class,immutability,C#,.net,Class,Immutability,有没有一种简单的方法使实例不可变 让我们举一个例子,我有一个类包含很多数据字段(只有数据,没有行为): 创建示例: MyObject CreationExample(String someParameters) { var obj = new MyObject { Title = "foo" // lots of fields initialization }; // even more fields initialization

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

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

创建示例:

MyObject CreationExample(String someParameters)
{
    var obj = new MyObject
    {
        Title = "foo"
        // lots of fields initialization
    };

    // even more fields initialization
    obj.Author = "bar";

    return obj;
}
但是现在我已经完全创建了我的对象,我不希望对象再是可变的(因为数据使用者永远不需要更改状态),所以我希望这样:

但是如果我想要这种行为,我需要创建另一个具有完全相同字段但没有setter的类

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


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

如果参数太多,并且不想使用参数构造构造函数……这里有一个选项

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

            // ...
        }

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

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;
    }
}
由于构造函数是操纵作者和标题的唯一方法,因此类在构造之后实际上是不可变的

// 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();
编辑:

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

如果您需要向类中添加新属性,那么返回使用构建器而不是硬编码对象实例化的单元测试就容易多了(我不知道该如何称呼它,请原谅我的术语)。

嗯,我将列举我对这一点的第一个想法

1。如果您唯一担心的是程序集外部的操作,请使用
内部设置器<代码>内部
将使您的属性仅对同一程序集中的类可用。例如:

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

    // ...
}
2.我不认为在构造函数中包含大量参数一定是个坏主意

3。您可以在运行时生成另一个类型,它是您的类型的只读版本。我可以详细说明一下,但我个人认为这太过分了


最好的,Iulian

不,没有简单的方法可以使任何类型不可变,特别是如果您想要“深度”不可变(即,通过不可变对象无法访问任何可变对象)。您必须显式地将类型设计为不可变的。使类型不可变的常用机制有:

  • 声明(属性备份)字段
    readonly
    。(或者,从C#6/Visual Studio 2015开始,使用。)
  • 不要公开属性设置器,只公开getter

  • 要初始化(属性备份)字段,必须在构造函数中初始化它们。因此,将(属性)值传递给构造函数

  • 不要公开可变对象,例如基于默认可变类型的集合(如
    t[]
    List
    字典
    ,等等)

    如果需要公开集合,请在防止修改的包装器中返回集合(例如,
    .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;
        …
    }
    
    class FooBuilder//用于准备不可变对象的可变版本
    {
    公共整数X{get;set;}
    公共列表Ys{get;set;}
    公共Foo Build()
    {
    返回新的Foo(x,ys);
    }
    }
    类Foo//不可变版本
    {
    公共Foo(整数x,列表ys)
    {
    这个.x=x;
    this.ys=新列表(ys);//创建副本,不要使用原始列表
    }//因为那是我们无法控制的
    私有只读INTX;
    私有只读列表;
    …
    }
    

我建议使用抽象基类型
ReadableMyObject
以及派生类型
MutableMyObject
ImmutableMyObject
。让所有类型的构造函数接受一个
ReadableMyObject
,并让
ReadableMyObject
的所有属性设置器在更新其支持字段之前调用一个抽象
ThrowIfNotMutable
方法。另外,让
ReadableMyObject
支持公共摘要
AsImmutable()
方法

尽管这种方法需要为对象的每个属性编写一些样板文件,但这将是所需代码复制的范围。
MutableMyObject
ImmutableMyObject
的构造函数只需将接收到的对象传递给基类构造函数。类
MutableMyObject
应实现
ThrowIfNotMutable
以不执行任何操作,并
AsImmutable()
以返回
新的不可变myobject(此)。类
ImmutableByObject
应实现
ThrowIfNotMutable
以引发异常,并
AsImmutable()
返回该异常


接收ReadableMyObject并希望保留其内容的代码应调用其AsImmutable()方法并存储生成的ImmutableMyObject。接收到ReadableMyObject并希望稍微修改版本的代码应该调用newmutablemyobject(theObject),然后根据需要修改它。

这里是另一个选项。声明具有
受保护的
成员的基类和重新定义成员的派生类
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;
    …
}
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) { ... }
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();
    }
}
}