Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

People also ask

What is the purpose of readonly modifier?

The readonly modifier prevents the field from being replaced by a different instance of the reference type. However, the modifier doesn't prevent the instance data of the field from being modified through the read-only field.

What is the purpose of readonly in C#?

The readonly keyword can be used to define a variable or an object as readable only. This means that the variable or object can be assigned a value at the class scope or in a constructor only. You cannot change the value or reassign a value to a readonly variable or object in any other method except the constructor.

What is a read only property?

Read only means that we can access the value of a property but we can't assign a value to it. When a property does not have a set accessor then it is a read only property. For example in the person class we have a Gender property that has only a get accessor and doesn't have a set accessor.

What is the syntax for making a property read only?

C# Readonly Keyword Syntax Following is the syntax of defining read-only fields using readonly keyword in c# programming language. readonly data_type field_name = "value"; If you observe the above syntax, we used a readonly keyword to declare a read-only variable in our application.


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.