Есть ли простой способ сделать неизменяемую версию класса?

Есть ли простой способ сделать экземпляр неизменным?

Давайте сделаем пример, у меня есть класс, содержащий множество полей данных (только данные, никакого поведения):

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.

Итак, есть ли какой-либо автоматический способ создания этого неизменяемого класса? Или другой способ разрешить изменчивость во время создания, но неизменяемый после инициализации?

Я знаю, что поля могут быть помечены как "readonly", но объект будет инициализирован вне класса, и передача всех полей в качестве параметров конструктора кажется плохой идеей (слишком много параметров).

Ответ 1

Нет, нет простого способа сделать какой-либо тип неизменным, особенно если вы не хотите "глубокой" неизменности (т.е. где невозможно изменить изменяемый объект через неизменяемый объект). Вам нужно будет явно создавать свои типы для неизменяемости. Обычные механизмы для создания типов неизменяемых:

  • Объявить (свойство-backing) поля readonly. (Или, начиная с С# 6/Visual Studio 2015, используйте свойства, автоматически реализованные только для чтения.)
  • Не выставляйте объекты свойств, только геттеры.

  • Чтобы инициализировать (свойство-backing) поля, вы должны инициализировать их в конструкторе. Поэтому передайте (свойство) значения конструктору.

  • Не выставляйте изменяемые объекты, такие как коллекции, основанные на типах изменяемых по умолчанию (например, 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

В качестве другого решения вы можете использовать Dynamic Proxy. Подобный подход использовался для Entity Framework http://blogs.msdn.com/b/adonet/archive/2009/12/22/poco-proxies-part-1.aspx. Вот пример, как вы можете это сделать, используя Castle.DynamicProxy framework. Этот код основан на исходном примере от 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();
    }
}
}

Ответ 3

Хм, я перечислил свою первую мысль об этом...

1. Используйте установщики internal, если ваше единственное беспокойство - это манипуляция вне вашей сборки. internal сделает ваши свойства доступными для классов только в одной сборке. Например:

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

    // ...
}

2. Я не согласен с тем, что обязательно иметь плохую идею иметь множество параметров в вашем конструкторе.

3. Вы можете создать другой тип во время выполнения, который является версией вашего типа только для чтения. Я могу подробно остановиться на этом, но лично я думаю, что это слишком много.

Лучший, Юлийский

Ответ 4

Я бы предложил иметь абстрактный базовый тип ReadableMyObject вместе с производными типами MutableMyObject и ImmutableMyObject. Создайте конструкторы для всех типов ReadableMyObject и попросите все средства определения свойств для ReadableMyObject вызвать абстрактный метод ThrowIfNotMutable перед обновлением своего поля поддержки. Кроме того, ReadableMyObject поддерживает открытый абстрактный метод AsImmutable().

Хотя этот подход потребует написания некоторого шаблона для каждого свойства вашего объекта, это будет степень дублирования кода. Конструкторы для MutableMyObject и ImmutableMyObject просто передают полученный объект конструктору базового класса. Класс 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;
    }
}

Из-за того, что конструктор является единственным способом манипулирования вашим Автором и Типом, класс по сути является неизменным после построения.

EDIT:

как упоминалось выше, я тоже большой поклонник использования строителей, особенно потому, что он упрощает модульное тестирование. Для вышеуказанного класса у вас может быть такой строитель, как:

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

Если вам когда-либо понадобится добавить новые свойства в свой класс, гораздо проще вернуться к вашим модульным тестам, которые потребляют от построителя, а не от экземпляра объекта жесткого кодирования (я не знаю, как его называть, поэтому pardon мои условия).

Ответ 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;
                    }
                }
            }

            // ...
        }

Ответ 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) { ... }