In this article, we are going to learn about the readonly modifier in C#.

However, before we go into detail on this topic, it is important to understand the concepts of mutability and immutability in programming.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!
To download the source code for this article, you can visit our GitHub repository.

Let’s begin!

Mutable vs. Immutable in C#

In C#, immutability refers to the inability to change the state of an object once we create it.

When an object is mutable, we can change its state. Thus, it is flexible but potentially prone to unintended changes.

Immutable objects provide a level of thread safety since we can’t modify them after creation. However, we must create a new instance to have an updated version of an immutable object, which can be less efficient.

Hence, immutable objects are preferable for situations where data consistency is critical, whereas mutable objects offer more flexibility in cases where frequent modifications are necessary.

Readonly Modifier in C#

We use the readonly modifier to make a variable immutable. 

Once initialized, the value of a readonly variable can’t be changed throughout the object’s lifetime. It ensures that the variable remains constant after initialization, enforcing immutability on the variable.

Let’s understand how the readonly modifier works:

public class Circle
{
    private readonly double _radius;

    public Circle(double radius)
    {
        _radius = radius;
    }
}

Here, we can set the value of the _radius field at the time of declaration, or within the constructor. However, if we try to set the field value outside the constructor:

public void ModifyRadius()
{
    _radius = _radius + 2;
}

We get a compiler error:

Error CS0191 A readonly field cannot be assigned to (except in a constructor or init-only setter of the type in which the field is defined or a variable initializer)

In C#, we also have the const  keyword that helps us enforce a form of immutability. However, it works differently from the readonly modifier. It’s typically useful for values that are universal constants, like mathematical constants.

The value of a const variable is directly embedded into the compiled code, and any attempt to modify it, even in the constructor, results in a compile-time error.

Use Readonly Modifier in C# With Fields

To declare a readonly field, we use the readonly keyword followed by the type and field name.

We can initialize the field either at the time of declaration:

private readonly double _radius = 2.5;

Or within the constructor of the class:

public Circle(double radius)
{
    _radius = radius;
}

Once set, we can’t modify the value of this field elsewhere in the class. However, we can use them in any method or property of the class:

public double GetCircumference()
{
    return 2 * Math.PI * _radius;
}

Impact on Value Types vs. Reference Types

Value types are stored directly on the stack that holds the entire object. When we use the readonly modifier in C# on value type fields, it ensures that the value of the field can’t be modified after initialization.

Let’s create a read-only struct Point (later on in the article, we will explain how readonly keyword affects the structure):

public readonly struct Point
{
    private readonly int _x;
    private readonly int _y;

    public int X => _x;
    public int Y => _y;

    public Point(int x, int y)
    {
        _x = x;
        _y = y;
    }
}

Here, we have read-only fields _x and _y. We encapsulate them within the properties X and Y respectively.

The readonly modifier ensures that once we create the Point object, we can’t modify the value of its fields.

Also, our properties encapsulate the private read-only fields and those properties are also only-getters (readonly), so if we try to modify their values, we get a compilation error:

var point = new Point(2, 3);
Console.WriteLine($"Coordinates: X - {point.X}, Y - {point.Y}");

point.X = 6; //error
point.Y = 10; //error

Reference types, on the other hand, store a reference to an object on the heap. The readonly modifier ensures that the reference to the object can’t be changed after initialization.

However, we can still modify the state of the object itself as we saw in the previous Circle class example. We couldn’t change the value of the _radius field outside the constructor but could still use the field value to calculate the circumference without any issues.

It’s important to note that the readonly modifier only makes the reference to the object immutable, and not the actual object itself. If we want to create a fully immutable object, we need to design the object itself to be immutable, such as by using readonly fields or properties within the object.

Readonly Modifier in Properties

We can’t use the readonly modifier with properties to create read-only properties. However, we can make a property read-only in other ways.

Readonly Auto-Implemented Properties

With this approach, we use auto-implemented properties with the get accessor only. We omit the set accessor, making the property read-only:

public int Age { get; }

Here, we can only set the value of the property at the time of declaration or within the constructor of the class.

We can also have a similar implementation of a public readonly property:

public class Person
{
    private readonly int _age;
           
    public int Age => _age;

    public Person(int age)
    {
        _age = age;
    }
}

Here, we can assign the value to the field inside the constructor and use the Age property to access the value of the _age field. The Age property is only-getter so it makes it readonly.

Readonly Properties With Private Setters

Here, we use public property with a private setter:

public class Person
{
    public string Name { get; private set; }
    public int Age { get; }

    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }

    public void ChangeName(string changedName)
    {
        Name = changedName;
    }
}

This restricts the property from any external modification:

var person = new Person("Jack", 21);
Console.WriteLine($"Name: {person.Name}");

person.Name = "Emily";

So, directly trying to change the Name property gives us a compiler error. However, we can still set the property within the class or constructor:

person.ChangeName("Emily");

As the ChangeName() method is present within the Person class, we can change the property value using this method. Thus, it is important to note that this approach makes a property read-only only from the perspective of the consumer. Internally, we can still change the state of such property.

Readonly Properties With Custom Getters

We can also create read-only properties with custom getters:

public double Area
{
    get { return Math.PI * _radius * _radius; }
}

This allows us to calculate the value of the property based on some logic every time we access it.

We looked at various ways to create a read-only property with or without using the readonly modifier. Let’s also understand how it differs from a read-write property.

Readonly Modifier in Methods

In C#, the concept of readonly is primarily used for fields and properties to create read-only data, and it does not have a direct counterpart for methods.

However, we have related concepts such as “read-only methods” and “pure functions” to explore immutability in methods.

Readonly Methods and Pure Functions

A read-only method refers to a method that does not modify the state of the object it belongs to. In other words, a read-only method does not change the values of any fields or properties of the class. Instead, it operates only on its input parameters.

A pure function is a special type of read-only method that produces the same output for the same input and has no side effects. It solely depends on its input parameters and avoids modifying any external state or variables.

The GetCircumference() method we discussed earlier is an example of a pure function as it calculates the circumference of a circle using the read-only _radius field without modifying any value.

Readonly Modifier in Classes

We can’t directly apply the readonly modifier to a class. However, we can make a class read-only by making all its members immutable. For example, using only read-only fields and properties within the class.

When we make the class read-only, we can only initialize it once during construction, and can’t modify its state afterward.

Read-only classes are useful in specific scenarios where we want to ensure that the state of an object remains constant and can’t be replaced.  This can be particularly valuable in multithreaded environments when we need to guarantee that the object won’t change unexpectedly.

Readonly Modifier in Structs

When we apply the readonly modifier to a struct, it indicates that instances of the struct are immutable, meaning that their values cannot be changed after initialization. All the fields and properties of the readonly struct must be readonly as well. If they are not, we will get an error: CS8340: Instance fields of readonly structs must be readonly.

We can still create multiple instances of the struct:

var point = new Point(2, 3);
var otherPoint = new Point(1, 4);

However, we can’t modify the value of the struct members.

Now that we have explored the usage of the readonly modifier in fields, properties, methods, classes, and structs, let’s look into some essential best practices for using it.

Best Practices

We should use the readonly modifier for fields or properties that are designed not to change their values after initialization. This helps enforce immutability and prevents accidental modifications.

To enforce immutability on collections, we should prefer the use of immutable collections such as ImmutableList, or ImmutableDictionary.

We should use readonly properties with custom getters for values that are computed based on business logic but do not change over the object’s lifetime. This allows us to cache computed values while not modifying the object’s state.

While readonly modifier helps with immutability, it does not automatically guarantee thread safety. If the code involves multithreading scenarios, we should ensure proper synchronization when needed.

Not everything needs to be immutable. We should try to strike a balance between mutability and immutability by using mutable objects where we require flexibility.

Conclusion

In this article, we’ve looked at the readonly modifier in C# and its impact on fields, properties, methods, classes, and structs. Understanding when and how to use readonly allows us to enforce immutability and prevent unintended modifications, leading to a more manageable codebase.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!