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.
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.