Classes Part 2: Creating Them

Classes Part 2: Creating Them

The Crash Course

  • Variables can be added to a class—these are called instance variables.
  • You can add as many constructors to a class as you want.
  • You can also add methods to a class which operate on the instance variables that the class has.
  • The private keyword, attached to a method or instance variable, means that only stuff inside the class can see it.
  • The public keyword, attached to a method or instance variable, means that anything (inside or outside of the class) can use it.
  • This tutorial also covers scope, name hiding, and the this keyword, all of which are very similar to C++ and Java.

Introduction

In the previous tutorial, we learned what a class is, what an object is, and how to use them. Now, we'll move ahead and see how to create your own classes.

The ability to create your own classes is very important. This tutorial will give you a good understanding of the basics of creating your own classes—something you'll probably be doing a lot of.

In this tutorial, we'll create a simple Player class, which we could use to represent a player in a game.

This is a big tutorial, and a lot of ground is covered. But this is the highlight of the Crash Course—what we've been building up to and what everything later builds upon. This is what you came for, even if you didn't realize it.

Take your time understanding this tutorial, and you'll be in good shape.

Creating a New Class

With what we learned in the previous tutorial, you should have a good idea of what a class is and how objects work. Hopefully, you even took some time to play around with Random instances yourself to get a feel for them.

But it is time for us to start making our own classes—something we'll be doing a lot of as we make C# programs.

As we saw with enumerations, type definitions, including classes, should either go in their own file or at the bottom of Program.cs after your main method and its contained methods.

Defining a Basic Class

To get started, we'll begin with only the barebones definition of a class. Remember, these go at the bottom of the file with other type definitions, not above or intermixed with other statements in your main method.

To begin a Player class, we can start with something like this:

Console.WriteLine("Hello, World!");
 
class Player
{
}

Nothing too crazy yet, right? We use the class keyword to indicate we're starting a class definition, then give it a name. After that is a set of curly braces where we'll put things that live in the class or in instances of the class.

Making an instance of Player

Next, let's make an instance of our new class. We can do that by modifying our main method:

Player player1 = new Player();
Player player2 = new Player();

We've now got two variables of the type Player, and each contains its own instance. (That's a single class, but two instances.)

However, that's about all we can do with a Player so far. Our class definition is empty, so there's really not anything a player is capable of yet. Let's fix that.

Adding Fields

We can add items to a class in any order we want, but perhaps the most obvious starting point is to think about what data instances of our class will need to do their job. We then make variables for that data. For example, let's say our players need to keep track of their name, the number of lives they have left, and their current point total.

We can add in fields for those easily with something like this code:

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

These three things are a special type of variable called a field.

The public thing, we're going to just slap in there for the moment. We won't keep it that way for long, but this ensures that other code can see and use them.

Like other variables, these fields have a type and a name. The type can be anything we want or need, though building classes that build upon the simple built-in types is a common pattern.

What's up with the underscores on those names? Well… most fields will begin with an underscore for reasons we'll be able to more clearly see later. I'm just establishing the pattern now.

One thing to point out is that these fields do not need to be initialized to be used. Fields are automatically initialized to their "zero" values. For an int, that is the value 0. For a string… well since strings are reference types, that gives it a reference that points to nothing—something called a null reference. But more on that later.

With these three fields added to the mix, we can start doing stuff with them in our main method:

player1._name = "HAL 9000";
player1._livesLeft = 5;
player1._score = 0;
 
player2._name = "Tron";
player2._livesLeft = 5;
player2._score = 0;

Or even:

Console.WriteLine(player1._name);

Note that we defined the three fields in the class, but each instance gets its own set of variables. Our HAL 9000 player is a distinct player from Tron, with its own name, score, and lives left.

Adding Methods

Objects are not just about data. They also can be asked to perform tasks. This is done by having the class define a method and then calling the method on an instance of the class. These tasks could be questions about their current state or asking them to perform some task. Lets add a few methods to illustrate:

class Player
{
    public string _name;
    public int _points;
    public int _livesLeft;
 
    public void AddPoints(int amount)
    {
        int previousThousand = _points / 1000; // INTEGER DIVISION! 990 / 1000 will be 0!
        _points += amount;
        int newThousand = _points / 1000;
 
        if (newThousand > previousThousand)
        {
            _livesLeft++;
        }
    }
 
    public void Kill()
    {
        _livesLeft--;
        if (_livesLeft == 0)
        {
            _points = 0;
        }
    }
}

These methods are a bit different from ones we've seen before. They have the public thing on them like our fields. We're still coming to that, but again, we'll slap that on our methods for the moment.

There is no static on these. We'll revisit that in a moment as well.

These methods use UpperCamelCase, with an initial uppercase letter. This is the common pattern in C# and also holds true for class and enumeration names.

These methods also don't just use their parameters and local variables (though they can have them). They also use the fields contained in the class. That is extremely common for methods in a class.

The logic in that AddPoints method is tricky. If that code doesn't make much sense, it's probably fine right now. But the effect is that every time a player earns 1000 points, they should gain an extra life. All of that extra code is there to achieve that. The Kill method also has some fancy logic in it to ensure that when you run out of lives, your score is reset.

Why do those methods do those things? No particular reason other than it makes them have a bit more meat to them and sets us up for some important discussions in a moment.

With these methods added, we can now call them:

player1.AddPoints(50);
player1.AddPoints(1500);
player2.Kill();

Constructors

In addition to fields (variables) and methods, we can also define constructors in our classes. If we don't define any constructors ourselves, the compiler will generate a very basic one for us, which is what has happened so far.

A constructor has the job of getting new instances into a good starting state. The specifics of that will depend on the class. For example, we might say that players should really come into existence with a real name, zero points, and five lives. We can add a constructor to our class to ensure new players start that way:

class Player
{
    public string _name;
    public int _points;
    public int _livesLeft;
 
    public Player()
    {
        _name = "New Player";
        _points = 0;
        _livesLeft = 5;
    }
 
    public void AddPoints(int amount)
    {
        int previousThousand = _points / 1000; // INTEGER DIVISION! 990 / 1000 will be 0!
        _points += amount;
        int newThousand = _points / 1000;
 
        if (newThousand > previousThousand)
        {
            _livesLeft++;
        }
    }
 
    public void Kill()
    {
        _livesLeft--;
        if (_livesLeft == 0)
        {
            _points = 0;
        }
    }
}

The constructor was added after the fields but before the methods, which is a common place to put constructors.

By adding a constructor of our own, the compiler will no longer inject its own basic default constructor. It will use ours.

Note that constructors must (a) be named identical to the class name and (b) do not have a return type at all. No void and no other type.

But let's suppose we don't want new players to start with the default name "New Player", but that we want whoever is making the new player to provide a specific name instead. That can be done by adding a parameter to the constructor, as we've done with other methods in the past.

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

Changing the constructor to this will break our old code for making new Player instances because now we need to supply the name. That's easy to fix:

Player player1 = new Player("HAL 9000");
Player player2 = new Player("Tron");

Now that our class has fields, methods, and constructors, we've made our first functional and complete class! That's a huge milestone, and we should celebrate with cake. Or maybe by making a Cake class.

Access Modifiers: private and public

We've punted on a bunch of topics and it is time to circle back to them. First, there's that public thing.

When something is marked public, it ensures that the outside world can see and use it. That's a good thing, right?

Not always. With objects, one of the big looming threats is that one object could potentially mess with another in ways that it really shouldn't be able to.

If you recall the logic we put into the AddPoints method, we specifically wanted to award extra lives whenever the player collected enough points. Similarly, we wanted to blow away their point total if they ran out of lives in the Kill method.

With the way we've got things set up, it is entirely possible for somebody to write code like this:

player1._points += 2000;

According to our rules, a million points should have awarded the player with two new lives! But this code fails to do that.

Most objects will have some sort of rules to enforce. If everything can directly reach in and mess with its data, it is going to be tough for the objects to enforce these rules.

This is usually handled by making fields private instead of public. When something is private, it can be modified from within the class but not outside of the class. Then the class provides methods that allow the outside world to request changes to the fields rather than directly changing them.

Step 1, then, is to change all of our fields to be private:

private string _name;
private int _points;
private int _livesLeft;

This will break our earlier code that was changing life counts, points, and names. Some of that, we could just delete. We already added the name in the constructor, and perhaps we're okay locking in a player's name at the start. We also have methods to add points and kill a player, reducing the number of lives they have. But if those fields all become private, do we need to add any other methods to allow the outside world to still do what it needs to do?

In this case, I think the answer is yes. At a minimum, the outside world probably wants to at least be able to see the current name, point total, and life count. Since we want the fields to be private, we had better add methods to allow the outside world to get those current values, which might look something like this:

public string GetName() { return _name; }
public int GetPoints() { return _points; }
public int GetLivesLeft() { return _livesLeft; }

The downside is that before we could do this:

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

Now, we need to do this:

Console.WriteLine(player1.GetName() + " has " + player1.GetPoints() + " total points.");

Accessing the fields directly was simpler and easier to read. We'll address that in the next tutorial.

But the main benefit is that our objects are now able to enforce the rules they have.

As a general rule, fields should always be private. There are counterexamples, but they should be rare. For the first 50 classes you make, stick with them being private. After that, you can make your own judgement calls about if you want them to be public instead.

Methods are often public, but if a method is a utility or helper method that you don't expect the outside world to use, it can also be made private.

Static Methods, Fields, and Classes

It is time to more formally define that static thing we've seen a bunch.

With most classes that you make, the expectation will be that you make instances of the class, and each instance has its own data and is independent of the other instances, aside from being used in the same ways.

However, some things can be shared by all instances of the class. When you want something to be owned collectively by the whole class, rather than having each instance have their own, you apply the static keyword to it.

Earlier, we added in code to reward the player with an extra life when they collected 1000 points. But suppose we wanted that in a variable. We could make another field for it:

private int _pointsNeededForExtraLife = 1000;

Then our AddPoints method could refer to that:

    public void AddPoints(int amount)
    {
        int previousGroup = _points / _pointsNeededForExtraLife;
        _points += amount;
        int newGroup = _points / _pointsNeededForExtraLife;
 
        if (newGroup > previousGroup)
        {
            _livesLeft++;
        }
    }

This change allows us to easily change our minds about how many points are required to earn an extra life. However, now every Player instance will drag around its own copy of that number.

It would make a lot of sense to have only one, shared across the class. That is easily done here by making _pointsNeededForExtraLife be static:

private static int _pointsNeededForExtraLife = 1000;

Along similar lines, a method can be made static. When you make a method static, it cannot use fields in the class (unless those fields are also static), and you reference it directly through the class name, rather than through individual instances.

All of Console, Convert, and Math's methods are static, which is why we do things like Console.ReadLine() instead of Console console = new Console(); and console.ReadLine(). If you want to mirror that style, you would simply apply the static keyword to the method.

In fact, if you want a class to only contain static things and to make it so you can't create instances of the class at all, you can apply the static keyword to the whole class:

static class StaticClass
{
    public static int GetNumber() { return 4; }
}

Once a class is made static, the compiler will stop you from accidentally adding any non-static ("instance") methods or fields. This is exactly how Console, Convert, and Math are all defined.

You'll make far more classes that are not static. But they do have their occasional use.

Variable Scope, Name Hiding, and this

Before we move on, there is another blob of topics we should touch on.

Look at this class:

class Rectangle
{
    private float width;
    private float height;
 
    public SetWidth(float width)
    {
        width = width;
    }
}

Do you see a problem looming there? The Rectangle class has a field called width, but the SetWidth method also has a parameter called width. So what happens on the line that says width = width;?

The intent was likely that the rectangle's width field should be updated with the value passed in for the width parameter. Alas, that's not what happens.

To understand what does happen, let's talk about a thing called variable scope. Variable scope refers to where a variable (or other identifier) is accessible. A field can be referenced from anywhere in the class. A parameter can be referenced from anywhere in the method.

Notably, that means that both the width field and the width parameter are both technically accessible inside of the SetWidth method. So what happens on the line width = width;? Which of the two variables does width refer to? Or is each used in different spots?

Alas, when a method has a parameter or local variable with the same name as something farther outward, like a field in the class, the more narrowly scoped thing wins. The fact that SetWidth has a width parameter essentially makes the width field inaccessible. This is referred to as name hiding.

There are a few ways to get around this. The first is to name the field and the parameter two different names. Indeed, this is at least part of the reason fields usually start with an underscore. If our fields started with an underscore in this case, the problem would solve itself:

class Rectangle
{
    private float _width;
    private float _height;
 
    public SetWidth(float width)
    {
        _width = width;
    }
}

There is another alternative, though. Any time we want, we can reach up to the instance we are in by using the this keyword and dig down from there. With this technique, we could leave off the underscores if we wanted:

class Rectangle
{
    private float width;
    private float height;
 
    public SetWidth(float width)
    {
        this.width = width;
    }
}

Now this.width refers to the field in the instance, because we got there via this, while plain old width still refers to the parameter.

Most people will use underscores for field names to avoid this (and some other gotchas). That is the most common convention in C#. But you could use this if you prefer.

Besides, this is a tool to get around a number of tricky situations, so it is useful to be aware of it.

What's Next?

This has been a huge tutorial. There's a ton here. But, importantly, at this point, we've covered the most essential C# elements. There's still plenty more to learn, but this is the top of the mountain. (If you're struggling still, you might want to try re-reading the tutorial, but also try tinkering with making a few objects of your own to get some hands-on experience. If you're still really struggling after that, I recommend either leaving a comment here asking questions on what you're confused on, or drop into the Discord server and chat for a bit. It will be worth the time to feel like you can make some basic classes before rushing off to the next step.

If you've made it this far, you've done well!

Our next topic will be properties, and then we'll start doing some stuff with reading and writing to files, and progress from there to some of the other more advanced C# features.