14 min read

Classes

Crash Course

Introduction

Take a breath. Maybe grab a coffee or a Coke. This is going to be a long one, but a very important one. The topic of classes–the focus of this tutorial–is perhaps the central topic of C# programming. Everything we’ve done up to this point is just building the foundation for this tutorial. Everything we do after is just icing on the top of the cake.

As we saw in the previous tutorial, in C#, you can make new types. While enumerations gave us the basic flavor, the most powerful version of defining our own types is a class.

When you’re writing a program that spans 40 lines, it is pretty easy to just pack it all into a method or four and call it a day.

When you are making a program that spans 4 million lines… well… we need some better tools to organize our code.

The prevailing model for organizing anything but the most trivial programs is called object-oriented programming . The idea is to break the whole program down into small, individual pieces called an object . Each piece is responsible for some small job in the system, and each program contains many objects that each focus on their own job. The objects work together–sometimes called collaboration by calling the methods they each provide.

To illustrate, imagine the game of Chess. You need to be able to represent the board, the pieces, the way the players pick their moves, and something in your code would need to be able to tell if a particular move is legal, and also detect when a player has won (among many other things).

By coming up with a scheme that allows the whole thing to be broken down into smaller elements, we can focus on one part at a time. We can build an object whose purpose in life is to track the state of the board–which pieces are in which spots. We can build an object that represents a pawn, a rook, a king, etc. We can build an object that represents the player and gets their desired moves.

Very often, we end up with objects that have an analog in the physical world, as is the case with the board, chess pieces, and players, above. Sometimes, some of our objects are a bit more abstract–we might decide to give the responsibility of detecting if a player won the game to an object that doesn’t have a direct real-world representation.

Sometimes, an object is unique in its role in the system. It does one thing, and there is only one thing in the system that does it. Perhaps the win checker object and the board are examples of that. In other cases, there are many objects that do the same thing. If we have objects to represent or track pawns, we will end up with 16 total of them to play a single game of chess. They’re each unique objects, but they all behave in the same general way.

In the C# world, we would say that all 16 pawns fit into the same class. (Think of it like a class of ships, or even like a car model. Many exist, but they all come from the same blueprint.)

In C#, we can create objects like this, but it always starts by defining a class first. Every object belongs to a single class. By defining the class, we define how objects of that class behave and work–what data they need to represent their current state, and what methods or jobs they need to be able to perform.

Defining a New Class

All of this description is useful, but it’s time to get practical. How do we actually make objects and classes?

While we could run with the chess example, let’s start a bit simpler: objects that represent players in a game.

Let’s suppose that we have a multi-player game, and each player has a name, a point total, and a number of lives left. Our game may have many objects that represent the different players.

We start by defining a class to represent players, and the data and tasks that we expect these players to need to track over time. Like we saw in the enumerations tutorial, there are some rules about where we can place a new type definition, such as a class.

If we’re using top-level statements, then types must go at the bottom of our main file, after all of our other statements and methods. If we’re using the traditional approach, with public static void Main, then new class definitions must go entirely above or below the Program class (but inside the namespace). Alternatively, we could make a new file for the new class we are making.

A class definition is easier to show than describe, so here’s the bare minimum version of a class that could be used to represent a player:

class Player
{
    public string _name;
    public int _points;
    public int _livesLeft;
}

Before we analyze this code, I want to point out one key thing: we’ve done a lot of things in this code that aren’t typically done–even things that are considered bad practice by C# programmers. We’re taking a few shortcuts here, but we’ll resolve them shortly.

Defining a class starts with the class keyword, which indicates we’re about to define a new class. After that comes the name of the new class. I chose Player. Like with variables and methods, you want to pick a name that will help you understand what it does. And like methods, the convention is to use UpperCamelCase names, capitalizing the first letter of every word, including the first.

Inside of a set of curly braces, we place all of the members of the class. Most of these are either going to be variables or methods, but in this specific case, they’re all variables.

We’ve applied the public modifier on to each of these. For the moment, we’ll just put that on everything (and explain what’s going on there in a moment).

These variables are a bit different from the local variables and parameters that we have seen in the past. Local variables and parameters only exist for as long as the method is running. Any variable that you place directly in a class is called a field , and they live for as long as the object lives for. Furthermore, each object that belongs to this class will have their own copy–their own set of variables. So if we have two players, they’ll each get their own name, points, and lives left.

Note, also, that each of these start with an underscore (_). There’s nothing magical about the underscore, but it is conventional to start field names with an underscore like this. It helps differentiate them from similarly-named local variables and parameters.

Using a Class

With a class defined, we can create new objects of that class. These objects are also often called instances of the class.

To start, we can make a variable whose type is this new class type:

Player player1;
Player player2;

To create new instances of the Player class, we use the new keyword:

player1 = new Player();

Once we have access to a Player instance, we can start using its methods and fields. We do that by using the member access operator or dot operator (.).

player1._name = "Leeroy Jenkins";
player1._livesLeft = 3;
player1._points = 2000;

Console.WriteLine(player1._name + " has " + player1._points + " points.");

Adding Methods

Each class can define methods. These are operations that objects of the class can perform by request. They can use the fields in the class, as well as any parameters or local variables that the method itself has.

For example, lets add a method to our Player class:

class Player
{
    public string _name;
    public int _points;
    public int _livesLeft;

    public void AddPoints(int amount)
    {
        _points += amount;

        if (_points >= 1000) // Probably better if this were a while loop instead...
        {
            _points -= 1000;
            _livesLeft++;
        }
    }
}

This AddPoints method adds some number of points, as expected. It also has the side effect of “cashing in” 1000 points for an extra life automatically.

These methods are very much the same concept as the methods we saw earlier. But in this case, you’ll want to put that public keyword on there so we can use it in the next step. (We’ll explain why in a moment.)

Also note that we do not put the static keyword on here. We’ll touch on that in a moment as well.

You can see that this AddPoints method has access to its own amount parameter, but also has access to the object’s _points and _livesLeft fields.

With the addition of this method, we can write other code that does this:

Player player1 = new Player();
player1.AddPoints(1000);

This will give the player 1000 points, which will also have the side effect of giving them an extra life, because they earned it with their 1000 points.

Constructors

Besides methods and fields, classes can also contain another type of member called a constructor . A constructor is a special method whose purpose is to get the object into a good starting state.

Let’s say we want to ensure all players have a name, start with zero points, and begin with five lives. We can add this constructor to the Player class:

class Player
{
    public string _name;
    public int _points;
    public int _livesLeft;

    public Player()
    {
        _name = "Player 1";
        _points = 0;
        _livesLeft = 5;
    }

    public void AddPoints(int amount)
    {
        // ...
    }
}

You might think that constructor looks a whole lot like a method. It essentially is. The only real differences is that constructors must match the name of the class they are in (and methods cannot match the name of the class), and there is no return type–not even void.

This constructor gives the player a fixed name, starts them at 0 points, and 5 lives.

Now, when we do new Player(), this code will run and set up the player correctly.

We actually probably don’t want to make every player be called "Player 1", so perhaps we could swap this constructor for one with a parameter:

public Player(string name)
{
    _name = name;
    _points = 0;
    _livesLeft = 5;
}

With that name parameter in there, we have to supply a name when we create a new instance:

Player player1 = new Player("Leeroy Jenkins");
Console.WriteLine(player1._name + " has " + player1._points + " points.");

note

A class can have many constructors, if you need them.

public and private

So far, we’ve blindly slapped a public on everything. Doing that allows things outside the class to have access to it.

For some things, that’s exactly what we want. For other things, that’s not good.

From our earlier code, we know that if the player scores 1000 points, they are rewarded with an extra life.

But consider this code:

Player player = new Player("Leeroy Jenkins");
player._points += 1500;

We just gave the player enough points that it should have given them an extra life, but because we didn’t use the AddPoints method, that won’t happen.

Now, we could just always remember to call the AddPoints method. But that’s not easy to remember when you have a program with 30 classes and 5000 lines of code.

As an alternative, we could use the private keyword on things, which makes them usable within the class, but not from outside:

class Player
{
    private string _name;
    private int _points;
    private int _livesLeft;

    // Constructor here.

    public void AddPoints(int amount)
    {
        _points += amount;

        if (_points >= 1000) // Probably better if this were a while loop instead...
        {
            _points -= 1000;
            _livesLeft++;
        }
    }
}

The AddPoints method still has access to those fields, even though they’re private. But the outside world will not.

This code will no longer work:

Player player = new Player("Leeroy Jenkins");
player._points += 1500;

In fact, it is generally considered a bad idea to let the outside world have direct access to an object’s data. By keeping them private and defining methods to allow careful, controlled access to the data, each object can guarantee that the data doesn’t change in ways that break things.

But now that these fields are marked private, the outside world has no way to see them.

Earlier, we had a line like this:

Console.WriteLine(player1._name + " has " + player1._points + " points.");

With the fields being private, that isn’t even possible anymore.

To address that without making them public again, the traditional thing to do is to make methods that allow controlled access to the data:

class Player
{
    private string _name;
    private int _points;
    private int _livesLeft;

    // Constructor goes here.

    // AddPoints goes here.

    public string GetName() => _name;
    public int GetPoints() => _points;
    public int GetLivesLeft() => _livesLeft;
}

Those last three methods simply return the value currently in the fields.

note

These three methods are using an expression body instead of a block body. That has nothing to do with classes, specifically. It is just a way to write a method that can be described entirely as an expression, as we saw in the methods tutorial.

The static Keyword

We’ve worked with several classes before, but what we’ve just starting making feels a little different.

We’ve used the Console, Convert, and Math classes. But we’ve never done new Console() or new Math().

Why not?

99% of the time, when you make a class, you’re defining how each instance of the class–each object whose type is that class–works. You’re defining a blueprint.

When you do that, you want each object to have its own data and each method to work with the specific object’s data.

You want to be able to make two or twenty instances of the Player class and have each of them have their own name, points, and lives without affecting the others.

But occasionally, you build things that don’t need to vary from instance to instance, but that are shared across all instances of the class. The static keyword is used in these situations. By applying the static keyword to a field, we define a field that is shared by all instances instead of giving each their own. Or you could say that the field is owned by the class as a whole instead of by any single instance.

By applying the static keyword to a method, we indicate that we don’t want it to attach itself to specific instances–and we ensure that we don’t accidentally use a field that is different from one instance to another.

Some classes have no instance-specific fields or methods. Basically, they’re just a collection of methods. Console, Convert, and Math all fit into this category.

Somewhere out there is a definition of Console that looks something like this:

class Console
{
    public static void WriteLine(string text) { /* ... */ }
    public static void WriteLine(int number) { /* ... */ }
    // More here.
}

When a method or field is static, then you access it through the class name instead of through an instance, which is why we’ve been calling Console.WriteLine("...").

So just remember: static means it belongs to the class and that each instance will not get its own copy.

note

If you ask five programmers to write a C# program that counts to 10, you’ll get five very similar programs. When it comes to the basic mechanisms of writing code, there’s not a ton of variability. There is only so many ways to write a for loop.

However, ask five programmers to choose how to split up a program like Pac-Man into different objects and you’ll get at least six different answers. There are many ways you could take a large program and break it down into small chunks. There are lots of options, and it is usually not as obvious when one style is better than another.

This concept of breaking large programs into small pieces and figuring out what each piece is responsible for, and how the pieces work together is called object-oriented design . It is a massive topic on its own, and is way beyond what we can cover here. But with some practice and tinkering, your skills will begin to grow stronger in time. (And it really does take a career to master it. Even the veterans keep learning new techniques and tradeoffs.)