The true meaning of readonly for value types

August 27, 2008 at 5:47 PMAndre Loker

I've been trying to create a simple and lightweight wrapper around List<T> and List<T>.Enumerator. The only thing I needed was the ability to enumerate over the items of the list. The list is supposed to be enumerated very(!) often, so I wanted the enumerator to be struct instead of a class. Using a value type enumerator avoids any heap allocation during enumerator. List<T>.Enumerator is a value type just because of the same reason.

So here's my first approach:

   1: /// <summary>
   2: /// A wrapper around an <see cref="List{T}"/> that only allows enumeration. 
   3: /// </summary>
   4: public struct ReadOnlyList<T> {
   5:   private readonly List<T> items;
   7:   /// <summary>
   8:   /// Initializes a new instance of the <see cref="ReadOnlyList{T}"/> struct.
   9:   /// </summary>
  10:   /// <param name="list">The list.</param>
  11:   public ReadOnlyList(List<T> list) {
  12:     items = list;
  13:   }
  15:   /// <summary>
  16:   /// Gets the enumerator.
  17:   /// </summary>
  18:   /// <returns></returns>
  19:   public Enumerator GetEnumerator() {
  20:     return new Enumerator(items);
  21:   }
  23:   #region Nested type: Enumerator
  24:   /// <summary>
  25:   /// A light weight enumerator for <see cref="ReadOnlyList{T}"/>
  26:   /// </summary>
  27:   public struct Enumerator {
  28:     private readonly List<T>.Enumerator enumerator;
  30:     /// <summary>
  31:     /// Initializes a new instance of the <see cref="ReadOnlyList&lt;T&gt;.Enumerator"/> struct.
  32:     /// </summary>
  33:     /// <param name="items">The items.</param>
  34:     public Enumerator(List<T> items) {
  35:       enumerator = items.GetEnumerator();
  36:     }
  38:     /// <summary>
  39:     /// Gets the current item.
  40:     /// </summary>
  41:     /// <value>The current item.</value>
  42:     public T Current {
  43:       get { return enumerator.Current; }
  44:     }
  46:     /// <summary>
  47:     /// Moves to the next element in the enumeration.
  48:     /// </summary>
  49:     /// <returns></returns>
  50:     public bool MoveNext() {
  51:       return enumerator.MoveNext();
  52:     }
  53:   }
  54:   #endregion
  55: }

This looks perfectly reasonable if you ask me. Here's a unit test to check that the enumerator works:

   1: [Test]
   2: public void Enumerator_CanEnumerateList() {
   3:   var list = new List<int> {0, 1, 2, 3};
   4:   var enumerator = new ReadOnlyList<int>.Enumerator(list);
   5:   var expected = 0;
   6:   while(enumerator.MoveNext()) {
   7:     Assert.AreEqual(expected, enumerator.Current);
   8:     expected++;
   9:   }
  10:   Assert.AreEqual(list.Count, expected);
  11: }

The test should pass, right? After all the Enumerator struct is a mere wrapper aroung List<T>.Enumerator. Funny thing is: it fails! Do you see why? As a hint, here's what fails: the second time the loop runs enumerator.Current is 0 although enumerator.MoveNext() has returned true. But why? It has taken me quite a while to find the reason.

To the Bat Mobile Debugger!

Of course, first thing I did was to debug the code. List<T>.Enumerator.MoveNext() looks something like this:

   1: public bool MoveNext() {
   2:     List<T> list = this.list;
   3:     if ((this.version == list._version) && (this.index < list._size)) {
   4:         this.current = list._items[this.index];
   5:         this.index++;
   6:         return true;
   7:     }
   8:     return this.MoveNextRare();
   9: }

MoveNextRare() is not important here, but what is important is that when the method returns true, index should have been increased. However, according to the debugger, the value of enumerator.enumerator.index doesn't change after the call to MoveNext(), although it returned true. Weird, huh? Somehow the fields did not get updated.

So I dug deep into the code. I opened ReadOnlyList<T>.Enumerator.MoveNext() in Reflector and here's what I got:

   1: .method public hidebysig instance bool MoveNext() cil managed
   2: {
   3:     .maxstack 1
   4:     .locals init (
   5:         [0] valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<!T> CS$0$0000)
   6:     L_0000: ldarg.0 
   7:     L_0001: ldfld valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<!0> Moonbow.Util.ReadOnlyList`1/Enumerator<!T>::enumerator
   8:     L_0006: stloc.0 
   9:     L_0007: ldloca.s CS$0$0000
  10:     L_0009: call instance bool [mscorlib]System.Collections.Generic.List`1/Enumerator<!T>::MoveNext()
  11:     L_000e: ret 
  12: }

Now it became clear that my MoveNext() method created a local copy of the enumerator field and called MoveNext on the copy. This of course meant that the original fields of the List<T>.Enumerator wouldn't get updated. And now you might see where my mistake was? Right: ReadOnlyList<T>.Enumerator.enumerator is marked as readonly! My idea was that the enumerator object will not be reassigned in any method of my Enumerator, so I could as well make the field readonly. After removing the readonly modifier the IL of my MoveNext() method was much more like I expected:

   1: .method public hidebysig instance bool MoveNext() cil managed
   2: {
   3:     .maxstack 8
   4:     L_0000: ldarg.0 
   5:     L_0001: ldflda valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<!0> Moonbow.Util.ReadOnlyList`1/Enumerator<!T>::enumerator
   6:     L_0006: call instance bool [mscorlib]System.Collections.Generic.List`1/Enumerator<!T>::MoveNext()
   7:     L_000b: ret 
   8: }

The conclusion

Here's the moral of the story:

If you declare a value type field as readonly, every call made to this field is actually made to a local copy. Any changes made in those calls are not stored in the readonly field. This is different from reference type fields where calls will be made on the field itself, potentially causing changes in the referenced object.

If this has been obvious for you: kudos, I didn't know that. Rreadonly fields are "really" readonly for value types, it means more than prohibiting assignments to the field. I did not find a word about that in the specification. The MSDN only mentions that assignments to readonly fields are only allowed in the declartion or in the constructor:

The readonly keyword is a modifier that you can use on fields. When a field declaration includes a readonly modifier, assignments to the fields introduced by the declaration can only occur as part of the declaration or in a constructor in the same class.

There is however a bug report that deals with this issue. Is has been closed as "By Design", though. It references the part in the specs that defines this behaviour: it's in section 7.4.4, function member invocation, bullet list 2, item 2, subitem 2.

If E is not classified as a variable, then a temporary local variable of E’s type is created and the value of E is assigned to that variable. E is then reclassified as a reference to that temporary local variable. The temporary variable is accessible as this within M, but not in any other way. Thus, only when E is a true variable is it possible for the caller to observe the changes that M makes to this.

By the way: it does not matter whether the type that declares the readonly value type fiels is a struct or a class by itself.

Posted in: C#

Tags: ,