How C# Helps with Object-Oriented Design
So far, the principles you have learned are not language-specific. It is time to learn how to use C# for OOP. C# is a great language because it is full of some very useful features. It is not only one of the most productive languages to work with, but it also allows you to write beautiful, hard-to-break code. With a rich selection of keywords and languages features, you can model your classes completely the way you want, making the intentions crystal clear. This section will delve deep into C# features that help with object-oriented design.
Static
Up till now in this book, you have interacted mostly with static
code. This refers to code that does not need new classes and objects, and that can be called right away. In C#, the static modifier can be applied in five different scenarios—methods, fields, classes, constructors, and the using
statement.
Static methods and fields are the simplest application of the static
keyword:
public class DogsGenerator { public static int Counter { get; private set; } static DogsGenerator() { // Counter will be 0 anyways if not explicitly provided, // this just illustrates the use of a static constructor. Counter = 0; } public static Dog GenerateDog() { Counter++; return new Dog("Dog" + Counter); } }
Note
You can find the code used for this example at https://packt.link/748m3.
Here, you created a class called DogsGenerator
. A static class
cannot be initialized manually (using the new
keyword). Internally, it is initialized, but only once. Calling the GenerateDog
method returns a new Dog
object with a counter next to its name, such as Dog1
, Dog2
, and Dog3
. Writing a counter like this allows you to increment it from everywhere as it is public static
and has a setter. This can be done by directly accessing the member from a class: DogsGenerator.Counter++
will increment the counter by 1
.
Once again, note that this does not require a call through an object because a static class
instance is the same for the entire application. However, DogsGenerator
is not the best example of a static class
. That's because you have just created a global state. Many people would say that static
is inefficient and should be avoided because it might create unpredictable results due to being modified and accessed uncontrollably.
A public mutable state means that changes can happen from anywhere in the application. Other than being hard to grasp, such code is also prone to breaking in the context of applications with multiple threads (that is, it is not thread-safe).
Note
You will learn about threading in detail in Chapter 5, Concurrency: Multithreading Parallel and Async Code.
You can reduce the impact of a global state by making it publicly immutable. The benefit of doing so is that now you are in control. Instead of allowing a counter increment to happen from any place inside a program, you will change it within DogsGenerator
only. For the counter
property, achieving it is as simple as making the setter property private
.
There is one valuable use case for the static
keyword though, which is with helper functions. Such functions take an input and return the output without modifying any state internally. Moreover, a class that contains such functions is static
and has no state. Another good application of the static
keyword is creating immutable constants. They are defined with a different keyword (const
). The Math library is probably the best example of helper functions and constants. It has constants such as PI
and E
, static helper methods such as Sqrt
and Abs
, and so on.
The DogsGenerator
class has no members that would be applicable to an object. If all class members are static
, then the class should be static
as well. Therefore, you should change the class to public static class DateGenerator
. Be aware, however, that depending on static
is the same as depending on a concrete implementation. Although they are easy to use and straightforward, static dependencies are hard to escape and should only be used for simple code, or code that you are sure will not change and is critical in its implementation details. For that reason, the Math
class is a static class
as well; it has all the foundations for arithmetic calculations.
The last application of static
is using static
. Applying the static
keyword before a using
statement causes all methods and fields to be directly accessible without the need to call a class
. For example, consider the following code:
using static Math; public static class Demo { public static void Run() { //No need Math.PI Console.WriteLine(PI); } }
This is a static import feature in C#. By using static Math
, all static members can be accessed directly.
Sealed
Previously, you mentioned that inheritance should be handled with great care because the complexity can quickly grow out of hand. You can carefully consider complexity when you read and write code, but can you prevent complexity by design? C# has a keyword for stopping inheritance called sealed
. If it logically makes no sense to inherit a class, then you should mark it with the sealed
keyword. Security-related classes should also be sealed because it is critical to keep them simple and non-overridable. Also, if performance is critical, then methods in inherited classes are slower, compared to being directly in a sealed class. This is due to how method lookup works.
Partial
In .NET, it is quite popular to make desktop applications using WinForms
. The way WinForms
works is that you can design how your application looks, with the help of a designer. Internally, it generates UI code and all you have to do is double-click a component, which will generate event handler code. That is where the partial class comes in. All the boring, autogenerated code will be in one class and the code that you write will be in another. The key point to note is that both classes will have the same name but be in different files.
You can have as many partial classes as you want. However, the recommended number of partial classes is no more than two. The compiler will treat them as one big class, but to the user, they will seem like two separate ones. Generating code generates new class files, which will overwrite the code you write. Use partial
when you are dealing with autogenerated code. The biggest mistake that beginners make is using partial
to manage big complex classes. If your class is complex, it's best to split it into smaller classes, not just different files.
There is one more use case for partial
. Imagine you have a part of code in a class that is only needed in another assembly but is unnecessary in the assembly it is originally defined in. You can have the same class in different assemblies and mark it as partial
. That way, a part of a class that is not needed will only be used where it is needed and be hidden where it should not be seen.
Virtual
Abstract methods can be overridden; however, they cannot be implemented. What if you wanted to have a method with a default behavior that could be overridden in the future? You can do this using the virtual
keyword, as shown in the following example:
public class Human { public virtual void SayHi() { Console.WriteLine("Hello!"); } }
Here, the Human
class has the SayHi
method. This method is prefixed with the virtual keyword, which means that it can change behavior in a child class, for example:
public class Frenchman : Human { public override void SayHi() { Console.WriteLine("Bonjour!"); } }
Note
You can find the code used for this example at https://packt.link/ZpHhI.
The Frenchman
class inherits the Human
class and overrides the SayHi
method. Calling SayHi
from a Frenchman
object will print Bonjour
.
One of the things about C# is that its behavior is hard to override. Upon declaring a method, you need to be explicit by telling the compiler that the method can be overridden. Only virtual
methods can be overridden. Interface methods are virtual (because they get behavior later), however, you cannot override interface methods from child classes. You can only implement an interface in a parent class.
An abstract method is the last type of virtual method and is the most similar to virtual
in that it can be overridden as many times as you need (in child and grandchild classes).
To avoid having fragile, changing, overridable behavior, the best kind of virtual methods are the ones that come from an interface. The abstract
and virtual
keywords enable changing class behavior in child classes and overriding it, which can become a big issue if uncontrolled. Overriding behavior often causes both inconsistent and unexpected results, so you should be careful before using the virtual
keyword.
Internal
public
, private
, and protected
are the three access modifiers that have been mentioned. Many beginners think that the default class modifier is private
. However, private
means that it cannot be called from outside a class, and in the context of a namespace, this does not make much sense. The default access modifier for a class is internal
. This means that the class will only be visible inside the namespace it is defined in. The internal
modifier is great for reusing classes across the same assembly, while at the same time hiding them from the outside.