Search code examples
c#genericsimmutability

How to Ensure Immutability of a Generic


This example is in C# but the question really applies to any OO language. I'd like to create a generic, immutable class which implements IReadOnlyList. Additionally, this class should have an underlying generic IList which is unable to be modified. Initially, the class was written as follows:

public class Datum<T> : IReadOnlyList<T>
{
    private IList<T> objects;
    public int Count 
    { 
        get; 
        private set;
    }
    public T this[int i]
    {
        get
        {
            return objects[i];
        }
        private set
        {
            this.objects[i] = value;
        }
    }

    public Datum(IList<T> obj)
    {
        this.objects = obj;
        this.Count = obj.Count;
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }
    public IEnumerator<T> GetEnumerator()
    {
        return this.objects.GetEnumerator();
    }
}

However, this isn't immutable. As you can likely tell, changing the initial IList 'obj' changes Datum's 'objects'.

static void Main(string[] args)
{
    List<object> list = new List<object>();
    list.Add("one");
    Datum<object> datum = new Datum<object>(list);
    list[0] = "two";
    Console.WriteLine(datum[0]);
}

This writes "two" to the console. As the point of Datum is immutability, that's not okay. In order to resolve this, I've rewritten the constructor of Datum:

public Datum(IList<T> obj)
{
    this.objects = new List<T>();
    foreach(T t in obj)
    {
        this.objects.Add(t);
    }
    this.Count = obj.Count;
}

Given the same test as before, "one" appears on the console. Great. But, what if Datum contains a collection of non-immutable collection and one of the non-immutable collections is modified?

static void Main(string[] args)
{
    List<object> list = new List<object>();
    List<List<object>> containingList = new List<List<object>>();
    list.Add("one");
    containingList.Add(list);
    Datum<List<object>> d = new Datum<List<object>>(containingList);
    list[0] = "two";
    Console.WriteLine(d[0][0]);
}

And, as expected, "two" is printed out on the console. So, my question is, how do I make this class truly immutable?


Solution

  • You can't. Or rather, you don't want to, because the ways of doing it are so bad. Here are a few:

    1. struct-only

    Add where T : struct to your Datum<T> class. structs are usually immutable, but if it contains mutable class instances, it can still be modified (thanks Servy). The major downside is that all classes are out, even immutable ones like string and any immutable class you make.

    var e = new ExtraEvilStruct();
    e.Mutable = new Mutable { MyVal = 1 };
    Datum<ExtraEvilStruct> datum = new Datum<ExtraEvilStruct>(new[] { e });
    e.Mutable.MyVal = 2;
    Console.WriteLine(datum[0].Mutable.MyVal); // 2
    

    2. Create an interface

    Create a marker interface and implement it on any immutable types you create. The major downside is that all built-in types are out. And you don't really know if classes implementing this are truly immutable.

    public interface IImmutable
    {
        // this space intentionally left blank, except for this comment
    }
    public class Datum<T> : IReadOnlyList<T> where T : IImmutable
    

    3. Serialize!

    If you serialize and deserialize the objects that you are passed (e.g. with Json.NET), you can create completely-separate copies of them. Upside: works with many built-in and custom types you might want to put here. Downside: requires extra time and memory to create the read-only list, and requires that your objects are serializable without losing anything important. Expect any links to objects outside of your list to be destroyed.

    public Datum(IList<T> obj)
    {
        this.objects =
          JsonConvert.DeserializeObject<IList<T>>(JsonConvert.SerializeObject(obj));
        this.Count = obj.Count;
    }
    

    I would suggest that you simply document Datum<T> to say that the class should only be used to store immutable types. This sort of unenforced implicit requirement exists in other types (e.g. Dictionary expects that TKey implements GetHashCode and Equals in the expected way, including immutability), because it's too difficult for it to not be that way.