简体   繁体   中英

Is there an easy way to make an immutable version of a class?

Is there an easy way to make an instance immutable?

Let's do an example, I have a class holding a lots of data fields (only data, no behavior):

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

    // ...
}

Example of creation:

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

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

    return obj;
}

But now that I have fully created my object, I don't want the object to be mutable anymore (because the data consumer will never need to change the state), so I would like something like that List.AsReadOnly :

var immutableObj = obj.AsReadOnly();

But if I want this behavior, I need to make another class that have exactly the same fields but without setter.

So is there any automatic way to generate this immutable class ? Or another way to allow mutability during creation but immutable once initialized ?

I know that fields can be marked as "readonly", but the object will be initialized outside of the class, and passing all fields as constructor parameters seems like a bad idea (too much parameters).

No, there is no easy way to make any type immutable, especially not if you want "deep" immutability (ie where no mutable object can be reached through the immutable object). You will have to explicitly design your types to be immutable. The usual mechanisms to make types immutable are these:

  • Declare (property-backing) fields readonly . (Or, starting with C# 6 / Visual Studio 2015, use read-only auto-implemented properties .)
  • Don't expose property setters, only getters.

  • In order to initialize (property-backing) fields, you must initialize them in the constructor. Therefore, pass the (property) values to the constructor.

  • Don't expose mutable objects, such as collections based on mutable-by-default types (like T[] , List<T> , Dictionary<TKey,TValue> , etc.).

    If you need to expose collections, either return them in a wrapper that prevents modification (eg .AsReadOnly() ), or at the very least return a fresh copy of the internal collection.

  • Use the Builder pattern. The following example is too trivial to do the pattern justice, because it's usually recommended in cases where non-trivial object graphs need to be created; nevertheless, the basic idea is something like this:

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

As another solution you can use Dynamic Proxy. Alike approach was used for Entity Framework http://blogs.msdn.com/b/adonet/archive/2009/12/22/poco-proxies-part-1.aspx . Here is example how you can do it using Castle.DynamicProxy framework. This code is based on original example from 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();
    }
}
}

You kind of hinted at a way in your question, but I'm not sure if this is not an option for you:

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

Due to the constructor being the only way of manipulating your Author and Title, the class is in effect immutable after construction.

EDIT:

as stakx mentioned, I too am a big fan of using builders - especially because it makes unit testing easier. For the above class you could have a builder such as:

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

This way you can construct your objects with default values, or override them as you please, but MyObject's properties can't be changed after construction.

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

If you ever need to add new properties to your class, it's much easier to go back to your unit tests that consume from a builder rather than from a hardcoded object instantiation (i don't know what to call it, so pardon my terms).

Hmm I will enumerate my first thought on this...

1. Use internal setters if your only worry is manipulation outside of your assembly. internal will make your properties available to classes in the same assembly only. For example:

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

    // ...
}

2. I don't agree that it's necessarily a bad idea to have lots of parameters in your constructor.

3. You could generate another type at runtime that is a read-only version of your type. I can elaborate on this, but personally I think this is overkill.

Best, Iulian

I would suggest having an abstract base type ReadableMyObject along with derived types MutableMyObject and ImmutableMyObject . Have constructors for all the types accept a ReadableMyObject , and have all the property setters for ReadableMyObject call an abstract ThrowIfNotMutable method before updating their backing field. Additionally, have ReadableMyObject support a public abstract AsImmutable() method.

Although this approach will require writing some boilerplate for each property of your object, that will be the extent of the required code duplication. The constructors for MutableMyObject and ImmutableMyObject will simply pass the received object to the base-class constructor. Class MutableMyObject should implement ThrowIfNotMutable to do nothing, and AsImmutable() to return new ImmutableMyObject(this); . Class ImmutableByObject should implement ThrowIfNotMutable to throw an exception, and AsImmutable() to return this; .

Code which receives a ReadableMyObject and wants to persist its contents should call its AsImmutable() method and store the resulting ImmutableMyObject . Code which receives a ReadableMyObject and wants a slightly-modified version should call new MutableMyObject(theObject) and then modify that as required.

Well, if you have too many parameters and you dont want to do constructors with parameters....here is an option

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

            // ...
        }

Here's another option. Declare a base class with protected members and a derived class that redefines the members such that they are public.

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

Creating code will use the derived class.

MyClass immutableInstance = new MyClass.Mutable { Title = "Foo", "Author" = "Your Mom" };

But for all cases where immutability is expected, use the base class:

void DoSomething(MyClass immutableInstance) { ... }

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM