user6440521
user6440521

Reputation:

Does the 'readonly' modifier create a hidden copy of a field?

The only difference between MutableSlab and ImmutableSlab implementations is the readonly modifier applied on the handle field:

using System;
using System.Runtime.InteropServices;

public class Program
{
    class MutableSlab : IDisposable
    {
        private GCHandle handle;

        public MutableSlab()
        {
            this.handle = GCHandle.Alloc(new byte[256], GCHandleType.Pinned);
        }

        public bool IsAllocated => this.handle.IsAllocated;

        public void Dispose()
        {
            this.handle.Free();
        }
    }

    class ImmutableSlab : IDisposable
    {
        private readonly GCHandle handle;

        public ImmutableSlab()
        {
            this.handle = GCHandle.Alloc(new byte[256], GCHandleType.Pinned);
        }

        public bool IsAllocated => this.handle.IsAllocated;

        public void Dispose()
        {
            this.handle.Free();
        }
    }

    public static void Main()
    {
        var mutableSlab = new MutableSlab();
        var immutableSlab = new ImmutableSlab();

        mutableSlab.Dispose();
        immutableSlab.Dispose();

        Console.WriteLine($"{nameof(mutableSlab)}.handle.IsAllocated = {mutableSlab.IsAllocated}");
        Console.WriteLine($"{nameof(immutableSlab)}.handle.IsAllocated = {immutableSlab.IsAllocated}");
    }
}

But they produce different results:

mutableSlab.handle.IsAllocated = False
immutableSlab.handle.IsAllocated = True

GCHandle is a mutable struct and when you copy it then it behaves exactly like in scenario with immutableSlab.

Does the readonly modifier create a hidden copy of a field? Does it mean that it's not only a compile-time check? I couldn't find anything about this behaviour here. Is this behaviour documented?

Upvotes: 32

Views: 1625

Answers (1)

Jon Skeet
Jon Skeet

Reputation: 1500495

Does the readonly modifier create a hidden copy of a field?

Calling a method or property on a read-only field of a regular struct type (outside the constructor or static constructor) first copies the field, yes. That's because the compiler doesn't know whether the property or method access would modify the value you call it on.

From the C# 5 ECMA specification:

Section 12.7.5.1 (Member access, general)

This classifies member accesses, including:

  • If I identifies a static field:
    • If the field is readonly and the reference occurs outside the static constructor of the class or struct in which the field is declared, then the result is a value, namely the value of the static field I in E.
    • Otherwise, the result is a variable, namely the static field I in E.

And:

  • If T is a struct-type and I identifies an instance field of that struct-type:
    • If E is a value, or if the field is readonly and the reference occurs outside an instance constructor of the struct in which the field is declared, then the result is a value, namely the value of the field I in the struct instance given by E.
    • Otherwise, the result is a variable, namely the field I in the struct instance given by E.

I'm not sure why the instance field part specifically refers to struct types, but the static field part doesn't. The important part is whether the expression is classified as a variable or a value. That's then important in function member invocation...

Section 12.6.6.1 (Function member invocation, general)

The run-time processing of a function member invocation consists of the following steps, where M is the function member and, if M is an instance member, E is the instance expression:

[...]

  • Otherwise, if the type of E is a value-type V, and M is declared or overridden in V:
    • [...]
    • 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.

Here's a self-contained example:

using System;
using System.Globalization;

struct Counter
{
    private int count;

    public int IncrementedCount => ++count;
}

class Test
{
    static readonly Counter readOnlyCounter;
    static Counter readWriteCounter;

    static void Main()
    {
        Console.WriteLine(readOnlyCounter.IncrementedCount);  // 1
        Console.WriteLine(readOnlyCounter.IncrementedCount);  // 1
        Console.WriteLine(readOnlyCounter.IncrementedCount);  // 1

        Console.WriteLine(readWriteCounter.IncrementedCount); // 1
        Console.WriteLine(readWriteCounter.IncrementedCount); // 2
        Console.WriteLine(readWriteCounter.IncrementedCount); // 3
    }
}

Here's the IL for a call to readOnlyCounter.IncrementedCount:

ldsfld     valuetype Counter Test::readOnlyCounter
stloc.0
ldloca.s   V_0
call       instance int32 Counter::get_IncrementedCount()

That copies the field value onto the stack, then calls the property... so the value of the field doesn't end up changing; it's incrementing count within the copy.

Compare that with the IL for the read-write field:

ldsflda    valuetype Counter Test::readWriteCounter
call       instance int32 Counter::get_IncrementedCount()

That makes the call directly on the field, so the field value ends up changing within the property.

Making a copy can be inefficient when the struct is large and the member doesn't mutate it. That's why in C# 7.2 and above, the readonly modifier can be applied to a struct. Here's another example:

using System;
using System.Globalization;

readonly struct ReadOnlyStruct
{
    public void NoOp() {}
}

class Test
{
    static readonly ReadOnlyStruct field1;
    static ReadOnlyStruct field2;

    static void Main()
    {
        field1.NoOp();
        field2.NoOp();
    }
}

With the readonly modifier on the struct itself, the field1.NoOp() call doesn't create a copy. If you remove the readonly modifier and recompile, you'll see that it creates a copy just like it did in readOnlyCounter.IncrementedCount.

I have a blog post from 2014 that I wrote having found that readonly fields were causing performance issues in Noda Time. Fortunately that's now fixed using the readonly modifier on the structs instead.

Upvotes: 32

Related Questions