Defining Classes in C#, as easy as Riding a Bike (h4-12)
The first few hundred lines of a C# program are written. You know your way around variables, loops, methods and other things. You have learned to walk. But if you want more, walking is no longer enough. In the real world, a bike would extend your radius. In C# you reach completely new horizons when you define your own classes.
Introduction to the basics of C# classes: We build a bike
Before I use classes for the HexaFour game, I will first explain how to define a C# class using a simpler example.
Define a Bike class
Suppose we want to implement a class for a bike. Let’s start with the most important feature of a bike, its color. A simple C# class for a bike could then look like this:
/// <summary>
/// An instance of this class represents a bike.
/// </summary>
public class Bike
{
public string Color { get; set; } = "red";
}
At the beginning of the class, I often write a comment in this form: “An instance of this class represents …”. This is quite trivial in the example above, but in many real cases it helps to be clearer about the responsibility of the class.
The class Bike
has a property with the name Color
. Initially, a bike here has the color “red”
(specified as a string). By specifying get
and set
, it is defined that the property may be read and changed. By specifying public
at the beginning of the property, it is specified that this property may be read and changed from code, that is using this class.
Using the class
Now we can use the Bike class in our program. For example, we can create a new console app and change the content of the Program.cs
file as following:
/// <summary>
/// An instance of this class represents a bike.
/// </summary>
public class Bike
{
public string Color { get; set; } = "red";
}
internal class Program
{
static void Main()
{
var bike1 = new Bike();
var bike2 = new Bike();
bike2.Color = "blue";
string colorOfBike1 = bike1.Color;
string colorOfBike2 = bike2.Color;
Console.WriteLine($"Color of bike 1 is {colorOfBike1}");
Console.WriteLine($"Color of bike 2 is {colorOfBike2}");
}
}
This example creates two bikes, changes the color of the second bike and then reads the colors of both bikes.
One file for each class
It is better to create a file for each class.
One way to do this is as follows: We place the cursor on the name of the class (i.e. on Bike). Then we select “Quick Actions and Refactorings” via the right mouse button or the shortcut Ctrl+.
. And then we select the action “Move type to Bike.cs”. This creates a “Bike.cs” file and the code for the Bike class is moved to this file. The file can then be found in the Solution Explorer.
You can also add a class in different way: Call up the item Add in the context menu of your project and then the sub-item Class. In the following window select the Class entry from the list and enter the name of our class with the file extension .cs
. Visual Studio then creates the file and fills it with an empty class.
Navigate to a class
The code is now spread across several files: One part is in Program.cs and another part in Bike.cs. Let’s assume we are currently in Program.cs and want to look at the code of Bike. Then there is a simple shortcut for this: Place the cursor on the name of the class, i.e. on Bike, and select the “Go to Definition” item in the context menu, or simply press the F12 key. Visual Studio then shows the contents of the Bike.cs file. You can use the “Navigate Backward” menu item or press Ctrl+- to return to the original position.
Constructors
Another property of a bike is the number of wheels. We can extend the class in this way:
public class Bike
{
public Bike()
{
WheelCount = 2;
}
public Bike(int wheelCount)
{
WheelCount = wheelCount;
}
public int WheelCount { get; }
public string Color { get; set; } = "red";
}
The class has been given a new property called WheelCount
, and two constructors . A constructor is a special method that has the same name as the class. There can be several constructors with different parameters. In this example, there are two: The first constructor is used to create a normal bike with two wheels. The second constructor can be used to specify how many wheels the bike should have. Both constructors set the WheelCount property to the appropriate value. Each method of a class can access the properties of the class and read and change them. The property WheelCount has a special feature, it only has a get
but no set
. This means that this property can only be set in the constructor and cannot be changed afterwards.
The class can be used in this way:
static void Main()
{
var bike = new Bike();
var unicycle = new Bike(1);
int wheelCountOfBike = bike.WheelCount;
int wheelCountOfUnicycle = unicycle.WheelCount;
// Not possible: unicycle.WheelCount = 2;
}
Fields
Finally, we need to inflate the wheels and want to check whether all the wheels are sufficiently inflated. To do this, we expand the class like this:
public class Bike
{
private readonly double[] _wheelPressure;
public Bike()
{
WheelCount = 2;
_wheelPressure = new double[2];
InitialInflate();
}
public Bike(int wheelCount)
{
WheelCount = wheelCount;
_wheelPressure = new double[wheelCount];
InitialInflate();
}
public int WheelCount { get; }
public void InflateWheel(int wheelNumber, double pressure)
{
_wheelPressure[wheelNumber] = pressure;
}
public bool WheelPressureIsOk
{
get
{
foreach (double pressure in _wheelPressure)
{
if (pressure < 3.0)
return false;
}
return true;
}
}
private void InitialInflate()
{
for (int i = 0; i < WheelCount; i++)
{
_wheelPressure[i] = 2.0;
}
}
}
The class has been given a field with the name _wheelPressure
. A field is something like a variable that we can use anywhere within our class. On the one hand, this is very practical, but on the other hand it can also make the code confusing. We should therefore use such fields sparingly. Under no circumstances should such fields be used from the outside, which is why this field is marked as private
. The data type of the _wheelPressure
field is double[]
. This is a so-called array. An array is a list with a fixed number of elements. Because we only know how many elements this array must have in the constructor, the array is created in the constructor. The statement new double[2]
creates an array with two elements of the type double
.
Private and public methods
The method InitialInflate
fills all tires with an initial pressure. This method should only be used within the class, which is why it is defined as private
.
The method InflateWheel
can be used to inflate the tires of the wheels (note that the count starts at 0
). This method should also be able to be used from “outside”, which is why it is defined as public
.
get or set of properties can contain code
And last but not least, there is a new property called WheelPressureIsOk
. This has no set
and only a get
. In contrast to the previous examples, there is program code behind the get
. This code checks all tire pressures and only returns true
if all tires have sufficient pressure.
Using the bike class
Finally, here is an example of how to use the new methods and properties:
public static void Main()
{
var bike1 = new Bike();
bool pressureIsOk = bike1.WheelPressureIsOk; // returns false
bike1.InflateWheel(0, 5.0);
bike1.InflateWheel(1, 5.6);
pressureIsOk = bike1.WheelPressureIsOk; // returns true
}
With the above examples I have only explained a few aspects of C# classes. There is much more, more information can be found for example in Microsoft Learn, see there for example the explanations in Properties in C#
Back to HexaFour
Now we return to our HexaFour program and start implementing classes in it. Our program currently consists of a Program.cs
file, which contains the entire program code (see here on GitHub). It contains approx. 200 program lines, which are distributed across several methods. The program is far from finished. If we carry on like this, the program will become confusing. We need to structure the program better. That’s why we split the code into C# classes.
The GameState class is responsible for checking for four in a row
What goes in one class and what in another? The Single Responsibility Principle (SRP) helps us here. This principle states that a class should only have one responsibility.
What are the responsibilities of our HexaFour game? Firstly, the game board has to be drawn on the screen. We’ve been working on that so far and are almost finished. On the other hand, we also need game logic. For example, we need to determine whether a player already has four tokens in a row and is therefore the winner of the game. We will now deal with this game logic. We want to make the classes for the game logic so “small” that they only have one responsibility.
To determine how many tokens of a color are in a row, the information summarized in this image is sufficient:
We have found a responsibility: We need to know for a given position whether there is a token there and which player it belongs to. Then we can check whether there is already a row with four tokens of one color. To do this, we create a class and call it GameState
.
Code for the GameState class
There are many ways to implement this class. I’ll just do it this way for now:
/// <summary>
/// An instance of this class represents the current state of the board (where are which tokens)
/// and can check whether a player has reached the necessary conditions for victory.
/// </summary>
internal class GameState
{
private readonly int[,] _tokens;
public GameState(int maxRow, int maxColumn)
{
_tokens = new int[maxRow + 1, maxColumn + 1];
MaxRow = maxRow;
MaxColumn = maxColumn;
}
public int MaxRow { get; }
public int MaxColumn { get; }
public void SetToken(int playerNumber, int row, int column)
{
_tokens[row, column] = playerNumber;
}
public bool PositionIsOccupied(int row, int column)
{
return _tokens[row, column] != 0;
}
public int PlayerOnPosition(int row, int column)
{
return _tokens[row, column];
}
}
The essential part of this class is the field _tokens
, which is used to store for each row and column the player number to which the token belongs at this position. If there is no token at a position, then _tokens has the value 0
at this position. In the constructor, _tokens
is initialized according to the size of the playing field passed as parameter maxRow
and maxColumn
.
The field _tokens
is a two-dimensional array, i.e. something like a table. If you create this array with new
, you must first specify the type of the elements (here int
) and then within square brackets how many rows and columns the array should contain. In our case maxRow + 1
many rows and maxColumn + 1
many columns.
The SetToken
method can be used to store the fact that a player’s token is located at a position on the board. The PositionIsOccupied
method checks whether there is a token at the specified position. The PlayerOnPosition
method checks which player the token at a specified position belongs to.
Testing whether the GameState class is doing the right thing
We still have to integrate the class correctly into the HexaFour program. But to check first whether the GameState class works at all, we write a test method FirstTest
and call it from the main method in Program.cs
:
internal class Program
{
public static void WoopecMain()
{
FirstTest();
}
private static void FirstTest()
{
var gameState = new GameState(6, 10);
bool occupied = gameState.PositionIsOccupied(0, 0);
if (occupied)
{
Console.WriteLine("Unexptected return value");
}
}
}
We can start the program as usual in the debugger and check by debugging whether the class is doing the right thing.
The bottom line
We can now divide the program code into manageable chunks. We create a separate class for each responsibility, and each class is in its own file. This makes it much easier to keep track of even large programs.
The GameState class is not yet ready. For example, a method is missing to check whether a player has four tokens in a row. The check could then be called from the calling location like this:
// ...
var gameState = new GameState(6, 10);
// ... Players set their tokens
// ...
// ... Check if player 1 or player 2 has 4 in a row:
int neededToWin = 4;
bool player1HasARow = gameState.PlayerHasConsecutiveTokens(1, neededToWin);
bool player2HasARow = gameState.PlayerHasConsecutiveTokens(2, neededToWin);
You might try to implement the method PlayerHasConsecutiveTokens
yourself. Or you can take a look at the example in HexaFourV02 on GitHub.
TL;DR
This post is part of a series. You can find the previous post here and an overview here.
C#
- Essential elements of a class are properties, fields and constructors.
- Properties have a
get
and/or aset
, these may also contain individual code. - Properties and methods can be public (in which case they can be used externally) or private (in which case they can only be used within the class).
- One-dimensional arrays are something like lists with a fixed length. Arrays can also be multidimensional.
Visual Studio:
- Use Quick Actions (Ctrl+.) to create a class file.
- Jump to the class with F12 and jump back with Ctrl+-.
Clean Code
- Split the code so that each class has only one responsibility (single responsibility principle).
Comment on this post ❤️
I am very interested in what readers think of this post and what ideas or questions they have. The easiest way to do this is to respond to my anonymous survey.