Updated on
The readonly modifier in C# stops a field from being assigned once its object is built. We can set a readonly field in its declaration, in a constructor or in an init accessor, and the compiler rejects every other assignment.
It also works on structs and on struct members, but it never freezes the object a field points to. Before we go into the details, let’s look at what mutability and immutability mean 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.
What Does the readonly Modifier Do in C#?
The readonly modifier makes a field assignable only while its object is being built. We can assign a readonly field in its declaration, in a constructor or in an init accessor of the same type, even more than once there. Once the object is built, the field keeps its value, and any other assignment is a compile-time error.
The compiler accepts the modifier in seven places. On a field, it blocks assignment after construction. On a struct declaration, it requires every instance field to be read-only. On a single struct member, it promises that the member doesn’t change the struct. On a ref return, a ref local, a ref field or a ref parameter, written as ref readonly, it blocks writes through that reference.
The readonly modifier never freezes an object. A readonly field that holds a list always points to the same list, but we can still add items to it.
On a class, a class property or a plain local variable, the compiler rejects readonly with error CS0106.
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 field 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 read it in any method or property of the class:
public double GetCircumference()
{
return 2 * Math.PI * _radius;
}
Impact on Value Types vs. Reference Types
A value type holds its data directly, so a value-type field holds the value itself rather than a reference to it. When we use the readonly modifier on a value-type field, the value can’t change 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
The compiler stops both lines with error CS0200, because X and Y have no setter.
What happens when a readonly field holds a struct with a method that changes it?
Let’s look at a small Counter struct:
public struct Counter
{
public int Value { get; private set; }
public void Increment()
{
Value++;
}
public readonly string Describe()
{
return $"Counted {Value} times";
}
}
The next class compiles without a single warning, and it is a trap, so don’t copy it into real code:
public class Tally
{
private readonly Counter _counter;
public int Total => _counter.Value;
public void Count()
{
_counter.Increment();
}
}
Now let’s call Count() twice:
var tally = new Tally();
tally.Count();
tally.Count();
Console.WriteLine($"Total: {tally.Total}");
Can you guess the output? It’s Total: 0.
Because _counter is readonly, the compiler can’t let Increment() change it. So it copies the struct, calls Increment() on the copy and throws the copy away. Remove readonly from the field and the same code prints Total: 2.
The field’s value never changed, so both increments were lost. Later in the article, we’ll see how a readonly struct makes this mistake impossible.
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. Let’s create a Playlist class that keeps its songs in a readonly list:
public class Playlist
{
private readonly List<string> _songs = new();
public int Count => _songs.Count;
public void Add(string song)
{
_songs.Add(song);
}
}
Adding two songs works without complaint:
var playlist = new Playlist();
playlist.Add("Bohemian Rhapsody");
playlist.Add("Hotel California");
Console.WriteLine($"Songs: {playlist.Count}");
The output is Songs: 2. What we can’t do is point the field at a different list. If we add a Reset() method that tries it:
public void Reset()
{
_songs = new List<string>();
}
We get the same CS0191 error we saw with _radius.
Microsoft Learn’s readonly reference page says a readonly reference-type field “must always refer to the same object. That object might not be immutable.”
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 read-only properties within the object.
How Do We Make a Property Read-Only in C#?
In a class, we can’t put readonly on a property. The compiler rejects public readonly int Age => _age; with error CS0106, “The modifier ‘readonly’ is not valid for this item”. So we make a property read-only by what we leave out of it.
A get-only auto-property, public int Age { get; }, can be set only in its declaration or in a constructor. An init accessor, added in C# 9, also allows an object initializer, and nothing after that. A private set hides the setter from callers, but the class itself can still change the value. A property with a custom getter and no setter has nothing to assign at all, and it computes its value on every read.
Structs are the exception. Since C# 8, we can mark a struct property, or only its get accessor, as readonly. That tells the compiler the property doesn’t change the struct.
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. If the difference between the _age field and the Age property is new to you, our article on properties and fields walks through it.
Init-Only Properties
Since C# 9, an init accessor gives us a property that callers can set once, in an object initializer:
public class Book
{
public required string Title { get; init; }
}
We set the title when we create the book:
var book = new Book { Title = "Dune" };
Console.WriteLine($"Title: {book.Title}");
After that line, book.Title = "Emma"; fails with error CS8852. The required keyword makes every caller set Title, so the property is never left unset. We cover the accessor in more depth in our article on init-only setters.
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 compiler error CS0272, because the set accessor is inaccessible. 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.
Readonly Modifier in Methods
In a class, there is no readonly method. If we add the modifier to a class method, we get the same CS0106 error as before.
Structs are different. Since C# 8, we can mark a struct method as readonly. Microsoft Learn says that on a struct member, readonly “means an instance member doesn’t modify the state of the structure”.
The Describe() method of our Counter struct is one of those. If it tried to assign Value, the compiler would stop it with error CS1604, “Cannot assign to ‘Value’ because it is read-only”.
Outside structs, a read-only method is still a useful design habit, even though no keyword enforces it.
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.
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 behaves like a pure function. It reads the read-only _radius field, changes nothing, and returns the same circumference every time we call it on the same circle.
Readonly Modifier in Classes
We can’t directly apply the readonly modifier to a class. If we try, public readonly class Circle fails with error CS0106. 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. Every instance field of a readonly struct must be readonly as well, and an auto-implemented property can’t have a set accessor. If they are not, we get an error: CS8340: Instance fields of readonly structs must be readonly. An auto-implemented property with a set accessor gets CS8341 instead. An init accessor is fine.
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.
We can still replace the whole value in a variable, though. point = new Point(5, 6); compiles, because readonly on the struct protects the struct’s fields, not the variable that holds it.
A readonly struct also can’t fall into the Tally trap from earlier. All of its members are readonly, so there is no method that could silently change a copy.
If we also want an == operator and a generated ToString(), a readonly record struct gives us both. Our article on record structs and record classes covers that.
Now that we have explored the readonly modifier in fields, properties, methods, classes, and structs, let’s put the rules side by side.
Where Can We Use the readonly Modifier in C#?
We can use readonly in seven kinds of declaration, and the compiler rejects it everywhere else.
It goes on a field, instance or static, in a class or a struct, or a static one in an interface. It goes on a struct declaration, including readonly record struct and readonly ref struct. It goes on an instance member of a struct: a method, a property, an indexer, an event or a single get or set accessor. And it goes after ref, as ref readonly, on a return, a local, a ref field and, since C# 12, on a parameter.
Almost everywhere else we get error CS0106, “The modifier ‘readonly’ is not valid for this item”. That covers a class declaration, a property or method of a class, and a plain local variable.
Some cases get their own errors. A static property or method of a struct gives CS8657. A plain parameter like readonly int a gives CS9190, because readonly has to follow ref.
Here are the main cases, with the error the compiler gives where readonly doesn’t belong:
Where we write readonly | Allowed? | What happens |
|---|---|---|
| An instance field of a class or struct | Yes | Assignable only in the declaration, a constructor or an init accessor, CS0191 anywhere else |
| A static field | Yes | Assignable only in the declaration or the static constructor |
A struct declaration (readonly struct, readonly record struct, readonly ref struct) | Yes | Every instance field must be readonly (CS8340), auto-properties can't have set (CS8341) |
| An instance method, property, indexer, event, or get or set accessor of a struct | Yes | The member can't assign the struct's fields (CS1604) |
A ref return, a ref local, a ref field or a ref parameter, as ref readonly | Yes | Writes through the reference are blocked |
| A class declaration | No | CS0106 |
| A property or method of a class | No | CS0106 |
A plain local variable (readonly int x) | No | CS0106 |
| A static property or method of a struct | No | CS8657 |
A plain parameter (readonly int a) | No | CS9190, readonly must come after ref |
So when CS0106 shows up on a class property, the fix is one of the read-only property forms from earlier. On a local variable, C# has no read-only form for a plain value, and a const works only for compile-time constants.
Best Practices
We should use the readonly modifier for fields that are designed not to change their values after initialization. This helps enforce immutability and prevents accidental modifications. In a struct, we can also mark the members that don’t change it as readonly.
To enforce immutability on collections, we should prefer the use of immutable collections such as ImmutableList, or ImmutableDictionary.
We should use read-only properties with custom getters for values we compute from other state. The getter runs on every read, so it suits cheap calculations. An expensive one is better computed once in the constructor and stored in a readonly field.
We should keep mutable structs out of readonly fields, or make the struct itself readonly. Otherwise, a method call on the field can change a copy and lose the write, as our Tally class did.
We don’t have to spot every candidate by hand. The IDE0044 code-style rule suggests adding readonly to a private field that is only assigned in its declaration or a constructor.
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.
Tested with .NET 10.0.10 (SDK 10.0.302) and C# 14.

That is useful!