Modern programming puts a big accent on a concept called immutability, which, in simple words, is the inability of an object to change or to be changed, once it is created. This has multiple reasons, but the most important ones state that:
- if a value should not change, we should be explicit about it, and not let it to interpretation and chance/mistakes
- the fewer values change in a program, the less chances of errors there are, and the easier to identify the root cause of errors is
- the readability of the code is improved, as the intent of not changing a value becomes obvious
- the compiler can make some performance optimizations, knowing that the value will never change
Immutability can be achieved in many ways, and has various degrees of flexibility. The strictest way is by using constants, which must be given a value directly upon declaration, which cannot later change. At the opposite spectrum, init only setter properties allow us to set them during declaration, in the constructor of their enclosing type, and in object initializers, which are ran immediately after the constructor finishes executing.
Between them, we have read-only members, which can be set only when declaring them, or in the constructor of their class. Example:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
using
System;
namespace
HelloWorld
{
class
Person
{
public readonly string name = "John Doe"; // can be set here
public readonly int age;
public Person(int age)
{
this.age = age; // can also be set here
}
private void SomeMethod()
{
name = "Jane Doe"; // this will cause a compiler error
}
}
class
Program
{
static void Main(string[] args)
{
Person person = new Person(20);
person.age = 10; // this will also cause a compiler error
Console.ReadLine();
}
}
}
|
Let's analyze the above example. We have two fields named name and age, which were declared using the readonly keyword. This means first of all that we can set their values during their declaration, as stated at the beginning of this lesson:
|
1
|
public readonly string name = "John Doe";
|
This also means that we can change their values in the constructor of their class:
|
1
2
3
4
|
public Person(int age)
{
this.age = age;
}
|
In fact, this is the way you will be using read-only fields in the vast majority of cases. This kind of pattern is usually used in conjunction with an advanced topic (of which we will learn much, much later), called dependency injection, because, if you think about it, this is what age represents for the Person class: a dependency without which the class cannot exist (cannot be instantiated). This is because we are asking age in the constructor of the class, which makes impossible instantiation of that class, without providing a value for age. So, if an instance of the Person class cannot be created without it, this means that age is a dependency of the Person class, hence the term dependency injection (we are "injecting" the dependency age into the readonly field age).
This is also the place where the role of the this keyword becomes clear, when we are referring to the field in the class age to assign it the value of the parameter age.
So, if readonly fields are primarily intended to have their values set through constructor parameters, and cannot be set outside of their class anyway, this also means that we should mark them as private (which we should do anyway, because their are variables, fields; data should be exposed outside classes through properties!):
|
1
2
|
private readonly string name = "John Doe";
private readonly int age;
|
Finally, you can notice how you get a compiler error both when trying to change the value of a readonly field after the constructor finished creating the instance:
|
1
2
3
4
|
private void SomeMethod()
{
name = "Jane Doe";
}
|
or from outside its class:
|
1
2
|
Person person = new Person(20);
person.age = 10;
|
Keep in mind that readonly keyword can only be applied to variables declared inside classes (fields). You cannot use it for local variables, or for other member types. For instance, we cannot mark a property as readonly, but we can achieve the same thing by not specifying a setter (read-only property), only a getter.
As with the case of records, remember that readonly does not refer to sub-members:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
namespace
HelloWorld
{
class
Student
{
public readonly decimal[] grades = new decimal[] { 10, 7, 9 };
private void SomeMethod()
{
grades = null; // compiler error, cannot change readonly members
grades[2] = 8; // members of readonly fields can be changed
}
}
}
|
In the above example, only the grades array itself is readonly. The values of its elements are not.