There are a few ways you may choose to implement a read-only property in C#. Some options are better than others, but how do you know which is the best? Let’s take a look at what you could potentially use and what the pros/cons are:
Naive approach
public class Reader { private string _filename = "Inputfile.txt"; public string Filename { get { return _filename; } } }
At first glance, the code above should be ideal. It gives you an explicitly read-only property and correctly hides the internal backing variable. So, what’s wrong?
Internal variable is visible, as well as the property
This makes the code a little more brittle to changes. In the implementation for Reader, developers could use either _filename or the property Filename. If in the future, the code was amended to make the Filename property virtual, all usages of _filename would be immediate bugs. You always want the code to be as resilient as possible. Of course, you should always use the property in code, but mistakes are very easy to make and overlook. It’s much better to make it impossible to write incorrect code – ensuring you end up in the pit of success.
_filename is mutable!
While the property itself is read-only, the backing variable is mutable! Our implementation of Reader is free to change the property as much as it likes. This might be desirable, but generally, we want the state to be as fixed as possible.
Better approach, but not ideal
public class Reader { private const string _filename = "Inputfile.txt"; public string Filename => _filename; }
The property accessor is now more concise, and the backing store is read-only. This is good, but it could be better.
Use of const is risky
What? const is risky? In this case, it could be. Const works by substituting all uses of the variable with the explicit value directly into the bytecode at compilation time. This means const is super-fast because there’s no memory usage and no memory access. The drawback is if the const value is changed, but an assembly referencing this assembly isn’t re-compiled. If this happens the other assembly will still see the ‘old’ value. In this example, this could happen if another assembly extended the Reader class it wouldn’t see changes to the private const variable unless it was also re-compiled. To make it even more confusing – it would likely ‘see’ the changes with Debug builds, but not with Release builds. The simple fix to this const difficulty is to use readonly instead of const for values that could change, such as settings. Genuine constants, such as PI, should remain as const.
Better again, but still not ideal
public class Reader { private static readonly string _filename = "Inputfile.txt"; public string Filename => _filename; }
The code is getting a little more reliable here. The filename state variable is now static readonly so it’s memory usage is low, while still offering flexibility to changes – even if consuming assemblies aren’t recompiled with every change we make to our assembly.
As an aside, read-only state variables should be static wherever possible. If the variable isn’t static, it’ll be created and initialised with every single instance of the class – rather than just once across all instances.
Ideal approach
public class Reader { public string Filename { get; } = "Inputfile.txt"; }
This is the best solution possible because it gives us maximum flexibility, with the minimum amount of potential issues going forward.
Good Locality
The initialisation happens with the property declaration ensuring high code locality. Before the property could be in a completely different area of the code file to the actual implementation of the private backing state.
No access to backing variable / state
The backing variable is now hidden from us, so we can’t accidentally use it instead of the property. If we make the property virtual, we can’t accidentally use the backing variable by mistake elsewhere in the code.
Easy to change to constructor initialisation
We can just remove the initialiser and put initialisation into the constructor. The property is still read-only, and the backing variable is still invisible. Easy!