Class modifiers in C#
It's easy to overlook class modifiers in C#, using them can make a huge difference to our coding style and safety.
It’s often easy to overlook class modifiers when we’re coding in C#. I’ve found myself regularly simply flagging something as public for the sake of convenience. Class modifiers don’t end with public and private though. Let’s dig into some of the class and property modifiers we find in C# 12.
Public
public is an access modifier that allows access unrestricted. This class can be instanced anywhere, whether it’s inside the same namespace or not. It’s the simplest of modifiers and often our default.
A simple example of an application that reads a driver’s name:
|
|
We can see how we can easily instantiate the Driver class and read the Driver’s name.
Output looks as follows:
|
|
Private
private is another access modifier and the near-antithesis of public. Classes and properties marked with private are only accessible within their parent class. Outside of that, they are inaccessible even to their derived classes (unlike their friend protected).
Though it is primarily used on a property level, as I will show you in an example down below, you can also use it on inner (or sub) classes. In other words, you do get private classes but only if they are nested inside other classes.
Let’s make our driver’s details a bit more private through the use of a private setter:
|
|
It’s a minor change but what we’re effectively telling the compiler is that though we can publically view this property, we cannot change it publically - the change needs to happen inside the Driver class. Once we change it we’ll notice instantly that our original code breaks, we can no longer use the line:
|
|
What we do instead is set a public accessor for the property and use that instead:
|
|
Using the SetName method we are exposing a way to update the name but within our own control. We could similarly do it via the constructor. Let’s chance the whole property to private and use the constructor to read the value instead - along with some validation for our driver’s name:
|
|
What we’ve done now is implemented a bit of a null guard into the constructor of the Driver class. If we find a null or an empty string, we throw an ArgumentNullException which gets picked up in the app. We also introduced a new public GetName method that will return the name, thus eliminating any public access to the property. Output looks as follows:
|
|
Protected
protected is yet another access modifier. Not dissimilar to private it limits access to it’s derived classes only:
|
|
Simply enough, if we made the property private, it would be inaccessible from the Driver derivative. This is a great way for sharing access within inheritance while keeping fields “protected”.
Internal
internal is the last of our basic access modifiers. It dictates that access is shared but only within the same assembly (dll). This is to say, if you’re coding a library and you have a field you don’t want to be shared when that library is referenced in another project, you can flag it with internal.
Let’s add another assembly, called Council. It’s the Council’s job to keep track of how many drivers are on the street but they don’t need to know the names of those drivers. We change our original Driver class as follows:
|
|
and the Council assembly can be implemented as below:
|
|
By changing the GetName method to internal it becomes declaratively public but only within it’s own assembly. It is inaccessibly to the Council assembly.
Protected Internal
protected internal is a more complex access modifier - allowing derived classes, in other assemblies, to access the property. That is to say, if we moved our person class to the Council assembly we could mark Name as protected internal and still use it in our derived class:
|
|
We can now simply reference the Council assembly in our Streets assembly and we’d have no errors.
Private Protected
The reverse of protected internal, private protected limits access within derived types within it’s own assembly only. In other words, if we changed our Council assembly as follows:
|
|
We’d not only get an Error CS0122 in our Streets assembly, but we’d also be unable to directly access that value outside of derived classes within the Council assembly.
Abstract
abstract is a decorater indicating a class that:
- Cannot be instantiated.
- May contain abstract methods and accessors (not properties).
- A non-abstract class derived from an abstract class must include implementations of abstract methods and accessors. Imagine a class designed to describe derived classes. For instance, let’s assign our drivers a vehicle. We know what a vehicle should look like in that it should have some wheels and be able to honk it’s horn:
|
|
Seems like it’s descriptive even though we make the assumption that every vehicle’s horn sounds the same. We can now create two derivatives of Vehicle:
|
|
We are instantly greeted by Abstract inherited member 'int Streets.Vehicle.GetNumberOfWheels()' is not implemented but that’s easy enough to solve:
|
|
Notice the override keyword. It indicates that this method is to override an abstract or virtual method or accessor. Now, let’s give our Driver class the ability to own more than one Vehicle:
|
|
I’ve also overridden the ToString method to give us all the details for the driver. Now, all we have to do in our Main method is the following:
|
|
Output looks as follows:
|
|
A similar keyword to abstract is virtual but we’ll come back to that later.
Async
The async decorator goes hand in hand with await. It indicates that a method is asynchronous. It is used to do potentially long-running calls without blocking the caller’s thread.
For instance, if you’re making a call to an API, there’s no point the thread you used should sit there and do nothing until you get a response back - it can continue working thanks to async methods.
This is a much bigger topic to discuss and not something I’ll cover in this article, perhaps in a later one. In the meanwhile, you can always go and read up on it here: https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/
Const
const is an age old keyword indicating a value is constant and once set will remain at the set value. It makes the value immutable. Should you try to change the value, the compiler will kick up a fuss and prevent it from happening:
|
|
In the above code, lo-and-behold, I made a mistake, instead of incrementing retryAttempts I’ve tried to increment retryLimit which would lead me to a bad loop. Fortunately, past me was clever enough to implement it as a const and the compiler kicked up a fuss so I could fix it as below:
|
|
Override
We’ve come across override already as part of the abstract modifier. We’ll also use it below as part of the virtual modifier. It allows overriding an abstract or virtual member.
Readonly
The readonly modifier works similarly to const. While const is a compile-time constant, readonly is a runtime constant. This means readonly variables can be instanced, static, or reference types while const variables are implicitly static and limited to basic types.
Sealed
sealed forms part of inheritance. When a class is specified as sealed it means that no other class can be derived from it. We can also use the sealed modifier on overrides to prevent derivatives from overriding specific methods or accessors. Let’s implement an SUV class, derived from our Car class, only we never want a car that has more than 4 wheels so we seal it:
|
|
Now the SUVs will be forced to have 4 wheels. If you want to drive a car with 6, you’ll probably have to register it as a lorry instead.
Static
static is a fairly common modifier. It is used to declare static members that belong to the type itself, instead of an instance of the type. You may have noticed, our Main method is static, meaning it is a method on the Program type and we don’t need to instance Program to access it.
When we make a method or accessor static it will not have access to any instanced properties of the class. For instance, we can make our Horn() method static and everything will still work fine, that’s simply because it doesn’t need to access anything instanced:
|
|
Unsafe
unsafe is less frequently used but it allows the use of pointers. A very simple example, straight from learn.microsoft.com would be the below:
|
|
Which would output the square of 5 which is 25. Generally, when something is marked as unsafe it won’t compile unless you specify the -unsafe parameter to the compiler https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-options/language#allowunsafeblocks. Unsafe code is not verifiable by the common language runtime and I have never seen it used at large scale.
Virtual
Finally we get to the virtual keyword. Not dissimilar to abstract a virtual method or accessor can be overridden as well but it allows us a base method, property, or accessor (unlike an abstract variant).
Let’s modify our Horn() method:
|
|
What we’re telling the compiler here is if a derivative of this class wants, it can override Horn() but if it’s not overridden, do what the base class says.
Now we can override this in our derivatives:
|
|
And we modify our Main to add an SUV to our driver’s collection:
|
|
The output now looks as follows:
|
|
It’s worth noting that SUV gets the override implementation of Car in this case.
Volatile
The volatile keyword is useful for working in a multi-threaded environment where some threads might be executing at the same time. When you declare a field as volatile you’re telling the compiler to exclude that piece from certain types of optimisation. You can see more in my write-up on the Singleton Design Pattern.
Honorable mentions
I haven’t made mention of the extern or event modifiers as they’re extremely niche and I’ve used them so rarely (if ever) that I would just be copy-pasting online documentation. I would prefer to spend some more time learning about them before returning and updating this post.
Conclusion
I hope that with the knowledge above you’re armed a bit better to create cleaner, more maintainable code.