When you first start out with C#, you’ll surely begin by creating a class. This is true whether you’re creating the code by hand or having a tool do it for you. The class is one of the most basic elements of the C# language. It serves many purposes, but the main purpose is for grouping related functions.
There’s more to a class than meets the eye. You can use a class in several ways depending on what style or paradigm of coding you’re using. Also, teams and even codebases often have different naming and casing conventions. In this post, you’ll learn more about the C# class. You’ll also learn about the importance of some best practices, such as following code conventions and using certain patterns.
With that said, let’s get started by outlining a basic class.
Basic Class Construct
For better organization, it’s best to stick to one class per file. This makes the codebase easier to take in.
Imports (using statements) go at the top of the file, in alphabetical order. This convention makes it easier to manage. Remove any imports that you don’t need to keep the file as clean and focused as possible.
A class can and should be defined in a namespace. It is optional but highly recommended. Two classes with the same name will “collide” and your code will not compile. Namespaces keep them unique.
Next, the class declaration itself has some level of accessibility. This tells the compiler whether the class should be exposed outside of the assembly (the DLL file that it makes when compiled/built). The class name should be PascalCased.
Here’s a sample class declaration:
namespace SubMain.CSharp.Examples { public class Empty { } }
This is an empty class. It’s actually completely useless as is, but it will compile. You don’t see any imports at the top because there is no reason to import anything. In the next section, we’ll add a method to the class to make it useful.
Method Declarations
Next, we’ll want to declare some methods in order to have the class do something. The whole purpose of a class is to provide access to methods. Methods are declared within a class as follows:
using System; namespace SubMain.CSharp.Examples { public class Calendar { DateTime GetNow() { return DateTime.Now; } } }
This class has a single method named GetNow. The method signature says that it takes no parameters and returns a DateTime type of object. I added the “using System;” statement at the top so that I would not have to fully qualify DateTime. Without the using statement, the method signature would look like this:
System.DateTime GetNow()
This fully qualified name isn’t so bad, but when your code becomes littered with fully qualified names it can become an eyesore. It’s better to use the using statement when you can. Sometimes, you have to qualify a name to avoid collisions.
Data
Another main purpose of a class is to contain data. This is a critical aspect of the object-oriented programming paradigm. Since C# was designed for object-oriented programming, it’s important to understand OOP concepts when working with C#. I can only touch on them here because it’s an in-depth topic. From a class declaration perspective, data is a class member and it’s private (only accessible from the class itself). Here’s some data in our time class:
using System; namespace SubMain.CSharp.Examples { public class Calendar { private int _offsetDays = -1; DateTime GetNow() { return DateTime.Now.AddDays(_offsetDays); } } }
Here we have an integer called?_offsetDays as a private member of the Calendar class. At this point, we would need to make an instance of the class to call the GetNow method. This is called instantiation. Let’s look at how to instantiate an instance next.
Instantiation
In order to use a class that contains data, we need to instantiate (or create an instance of) the type. When we instantiate a class, we create a container for the data that’s separate from other instances. This instance data is the state of the object instance. And methods are used to manipulate or change the state of the instance.
A Counterexample
As is, we could use our Calendar class without creating an instance if we change a few things around. All we would need to do is mark the _offsetDays member and the GetNow method as static like this:
using System; namespace SubMain.CSharp.Examples { public class Calendar { private static int _offsetDays = -1; public static DateTime GetNow() { return DateTime.Now.AddDays(_offsetDays); } } }
Both the class member and the method are now static. Notice also that I’ve added the public access modifier to GetNow. Without this modifier, it would only be accessible from within the class, which makes it pretty useless at this point.
And in order to use this class, we call it from another class like this:
using System; namespace SubMain.CSharp.Examples { public class Program { public static void Main(string[] args) { Console.WriteLine(Calendar.GetNow()); Console.ReadKey(); } } }
This counterexample calls the static method GetNow on the Calendar class.
An Example
In order to create an instance, we need to rely on a few code constructs: a variable declaration, the assignment operator, the new operator, and the constructor method of the class. Putting all those together, we get the following:
using System; namespace SubMain.CSharp.Examples { public class Program { public static void Main(string[] args) { var cal = new Calendar(); Console.ReadKey(); } } }
This sample will create an instance of the Calendar class and assign a pointer to the instance to the cal variable. That instance will NOT expose the GetNow method because that method is still static. We can change that by removing the static keyword from the method declaration.
public DateTime GetNow() { ... }
This will turn the method back into an instance method. Now we can call it from our cal instance like this:
Console.WriteLine( cal.GetNow() );
Calling an instance method like this will allow that method access to the members of the class. However, in this case, our class member is also static. This can present some problems if we allow that data to be modified.
Modifying Data
Instance methods are for state manipulation. Let’s say we add a method to increment _offsetDays. The method looks like this:
public void IncrementOffsetDays() { _offsetDays++; }
And in order to call it, we would use the class instance like this:
cal.IncrementOffsetDays();
This would change the state of _offsetDays to 0.
But what would happen if we had another instance of Calendar and called IncrementOffsetDays on that one too?
var cal = new Calendar(); var cal2 = new Calendar(); cal.IncrementOffsetDays(); cal2.IncrementOffsetDays();
Because _offsetDays is static, it belongs to the global scope rather than to an instance scope. This means that changes to it stack up. Sometimes this is what we want, but often it’s not.
Think about it like this:
Suppose every individual person used the same age counter to tally their age. When it’s someone’s birthday, that single age counter gets pushed up a year. We’d all be trillions of years old using this approach. We clearly need our own age counters! This is where the instance member comes into play.
Instance State
Class instances have their own state space. This space holds instance members. As an example, let’s build a class for our birthday use case from the previous section.
namespace SubMain.CSharp.Examples { public class Person { private int _ageYears; public Person(int ageYears) { _ageYears = ageYears; } public void CelebrateBirthday() { _ageYears++; } } }
This class has an age in years. The only way to change the age is to call CelebrateBirthday on an instance of the class. But first, we need to create an instance, passing the age into the constructor.
var gramma = new Person(88); gramma.CelebrateBirthday();
When we call CelebrateBirthday on an instance of Person, it will increment its _ageYears member. But so what? At this point we can’t access the member nor can we use it in any way. So how do we do make use of this variable that’s only available inside this class instance?
Accessing State
When it comes to accessing state to make use of your class instances, you have a few options. None of them should involve allowing an external class to update the state directly.
DO NOT DO THIS:
public class Person { // WARNING! // DO NOT MAKE PUBLIC LIKE THIS, // FOR COUNTER-EXAMPLE ONLY public int _ageYears; ... }
If you do this, you’ll start to have “warped” code where everything suddenly knows everything about all other classes. Instead, use methods to access the state. This way you keep all the logic in one place and reduce the chance of the instance getting into an invalid state.
So what can you do?
DO THIS INSTEAD:
You can create methods that use the state and return conclusions. For example, we may want to know if the person is old enough to access certain material. In that case, we can ask the instance if the person is older than x as follows:
public class Person { ... public bool IsOlderThan(int ageLimitYears) { return _ageYears > ageLimitYears; } }
And now we have a class that’s useful! It can be a little tricky at times to avoid exposing state outside a class, but it’s worth the effort to have all the logic contained! The details of this topic are a bit more advanced so I won’t go down that rabbit hole right now. But, if you need a class that’s only for passing data around there’s something for that too: Properties.
Properties
You’ll likely encounter properties on a daily basis. Eventually, you may wish you never had. They’re useful for exposing state in certain types of class designs. There’s a design pattern called a data-transfer object (DTO) which is used for giving and receiving data between two contexts.
You would use a DTO whenever you get data from a web call or when you need to expose data to a template. Those are reasonable and good uses of properties. In fact, you should have entire objects (classes or structs) that are nothing but properties. Here’s a view model class with only properties:
public class PersonViewModel { public int Id { get; set; } public string Name { get; set; } public Years Age { get; set; } }
The properties on the PersonViewModel are public (they don’t have to be) and have getters (get) and setters (set). Each of those can have their own access modifiers. Also, you don’t need both get and set. For more advanced usage, you can even add a body to these. Here are some examples of the possibilities:
{ private string _disposition; public PersonViewModel(int id, string name) { Id = id; Name = name; } // can only set in constructor or initialization public int Id { get; } // can set from anywhere inside a class instance public string Name { get; private set; } // set a default during class initialization public Years Age { get; set; } = 42; // only returns a value // as programmed in the body of the getter public string Occupation { get { return "Programmer"; } } // a full property has bodies for the getter and the setter public string Disposition { get { // if not set (null), return a default return _disposition ?? "neutral"; } set { _disposition = value; } } }
This is not an exhaustive example of the possibilities when it comes to properties. With regard to properties, you’re really just condensing a get method and a set method. It’s a bit of syntactic sugar that C# allows.
Closing Out
To wrap up this post, I’d just like to reiterate that this is only the tip of the iceberg when it comes to understanding a C# class. You’ve seen the basics like how and where to declare a class. We’ve looked at statics, constructors, and accessors. You should now have a working knowledge of members, methods, and properties. And finally, you have a taste of naming conventions, patterns, and other best practices. There’s much more to learn, so be sure to check out other posts on C# such as this guide to operators. While you’re at it, there’s a whole series on CodeIt.Right rules like this helpful explanation of why you need await.
Learn more how CodeIt.Right can help you automate code reviews and improve the quality of your code.