Search code examples
c#clr

Use case to understand why a list of strings should be declared as readonly


I am trying to understand what use cases would require me to declare a List<string> as a ReadOnly type.

An associated question with this is: How much memory upon instantiation of a list gets allocated?


Solution

  • The main reason to mark a field as readonly is so that you know that regular code cannot have swapped the list reference. One key scenario where that might matter is if you have other code in the type that is performing synchronization against the list using a lock(theListField). Obviously if someone swaps the list instance: things will break. Note that in most types that have a list/collection, it isn't expected to change the instance, so this readonly asserts that expectation. A common pattern is:

    private List<Foo> _items = new List<Foo>();
    public List<Foo> Items => _items;
    

    or:

    public List<Foo> Items {get;} = new List<Foo>();
    

    In the first example, it should be perfectly fine to mark that field as readonly:

    private readonly List<Foo> _items = new List<Foo>();
    

    Marking a field as readonly has no impact on allocations etc. It also doesn't make the list read-only: just the field. You can still Add() / Remove() / Clear() etc. The only thing you can't do is change the list instance to be a completely different list instance; you can, of course, still completely change the contents. And read-only is a lie anyway: reflection and unsafe code can modify the value of a readonly field.

    There is one scenario where readonly can have a negative impact, and that relates to large struct fields and calling methods on them. If the field is readonly, the compiler copies the struct onto the stack before calling the method - rather than executing the method in-place in the field; ldfld + stloc + ldloca (if the field is readonly) vs ldflda (if it isn't marked readonly); this is because the compiler can't trust the method not to mutate the value. It can't even check whether all the fields on the struct are readonly, because that isn't enough: a struct method can rewrite this:

    struct EvilStruct
    {
        readonly int _id;
        public EvilStruct(int id) { _id = id; }
        public void EvilMethod() { this = new EvilStruct(_id + 1); }
    }
    

    Because the compiler is trying to enforce the readonly nature of a field, if you have:

    readonly EvilStruct _foo;
    //...
    _foo.EvilMethod();
    

    it wants to ensure that the EvilMethod() can't overwrite _foo with a new value. Hence the gymnastics and the copy on the stack. Usually this has negligible impact, but if the struct is atypically large, then this can cause a performance problem. The same issue of guaranteeing that the value doesn't change also applies to the new in argument modifier in C# 7.2:

    void(in EvilStruct value) {...}
    

    where the caller wants to guarantee that it doesn't change the value (this is actually a ref EvilStruct, so changes would be propagated).

    This issue is resolved in C# 7.2 by the addition of the readonly struct syntax - this tells the compiler that it is safe to invoke the method in-situ without having to make the extra stack copy:

    readonly struct EvilStruct
    {
        readonly int _id;
        public EvilStruct(int id) { _id = id; }
        // the following method no longer compiles:
        // CS1604   Cannot assign to 'this' because it is read-only
        public void EvilMethod() { this = new EvilStruct(_id + 1); }
    }
    

    This entire scenario doesn't apply to List<T>, because that is a reference type, not a value type.