Classes Part 1: Using Them

Classes Part 1: Using Them

The Crash Course

  • Object-oriented programming is a way of programming where you create chunks of code that match up with real-world objects.
  • Classes in C# define what an entire class/category of objects can do (what all players can do, or what any car can do)—what kind of data it stores, and what kinds of things can be done to it, as well as what it can do.
  • You can create a new object with something like the following: Random random = new Random(); This creates a new Random object, which is used to generate random numbers.
  • There's generally no need for destructors in C# because all objects that are no longer used are garbage collected, like in Java.

Introduction

C# is what people call an object-oriented programming language (Sometimes abbreviated OO, or OOP). This is a fancy word that means that you split your program into multiple objects, each responsible for a small part of the whole program. The objects work together to form the whole program. We'll start this discussion off with defining what an object is and how they are defined in C# (as a class), and then we'll talk about how to make and use objects in C#.

What is an Object?

As you write bigger programs, you're going to realize you need good tools to organize and structure your code. Methods, which we learned about in the previous tutorial, are a good start, but they are not enough when the programs get very large.

The basic gist of methods is still valid, though. Rather than throw everything together, we chop the program into small pieces we can build, think about, and work with mostly individually, then get the parts to play nicely together to solve the full problem.

A single component responsible for some small slice of the overall program is called an object, named such because these objects can often correspond to some real-world counterpart that fills a similar role.

In C#, types are a big deal. Everything fits into one type or another. When it comes to objects, they all have a type as well. Each object type--called a class--defines how things of that type behave. But we can have one, two, or a million of any given object type in our program.

As an example, if we were making the game Asteroids, we might make a class definition that describes how a single asteroid works—its position, its motion across the screen, perhaps also its ability to detect it has hit a bullet and the capability to break down into many smaller asteroids, each with their own positions and motion.

And maybe we also make a class to represent bullets and how they move across the screen, and make one object of that type whenever we shoot. A class for the player's ship, a class for the UFOs, and maybe a class for players themselves, with objects of each of those types that are made and removed as ships, UFOs, and players come and go in the game.

Objects and classes are a huge topic that takes some time to get used to. You'll probably need to make a handful of programs using objects before their merits (and limitations) click in your mind, and you can start "thinking in objects."

We'll take our first step here in this tutorial by using an existing class to make new objects that generate random numbers and using them in a program. This is far from the last step but should give us a decent base to build on.

Creating an Object

We'll begin our object-oriented journey by working with the Random class. This class is designed to generate random numbers, which is extremely useful in games but also has plenty of uses outside of games.

First thing's first: we can start a new program with this barebones code:

Random random;

This declares a variable that is capable of storing objects whose type is the class Random. This is not functionally different from a variable like int number;, other than we're using a more complex type—an object.

Objects of a particular type are often referred to as instances of that type, and we'll use that word frequently in these tutorials, usually in the form of "this is an instance of Random" or "this is a Random instance," both denoting that the object belongs to the type Random. A class like Random defines how to interact with objects of the type, so knowing that some object is an instance of Random tells us all we need to know about how to interact with the object.

The random part is just a variable name. It could have been anything else, though having variable names closely related to the type involved is not uncommon.

The next step is to make a new instance of the Random class to go into our random variable.

With most of the built-in types like int and string, there was a simple way to write out a specific value. For objects, we aren't so lucky. Instead, we need to instruct the computer to construct one. Constructing or making a new Random instance is shown below:

Random random = new Random();

The new keyword will always be used when we form a new object. The Random indicates the type. The parentheses make it look like a method call, and indeed, it basically is. Though in this case, it runs a special type of method called a constructor that gets instances of the class into a good starting state. Some constructors have parameters, which would require us to pass in arguments for it between the parentheses, but this one does not.

Once constructed, we get a reference to the newly-formed object as a result, which we can use later. In this case, we store that reference into the random variable, so we can ask the object to do things later on.

Getting the object to do stuff is our next step.

The Random class defines a pile of methods that any instance can perform. They all deal with generating random values. The simplest of these is the Next method, which returns a random positive int value. To use this method, we would write code like this:

int randomNumber = random.Next();

This should look somewhat similar to when we call Console.ReadLine() and things like that, but there's one major difference: with all of the methods on Console, Convert, and Math, the class name came first. Here, the class name is nowhere to be found. Instead, we're retrieving the Random instance out of our variable and asking the object, rather than the class to run the method. This is an important difference. Until now, we've been calling all our methods through the class itself. Going forward, this new style is going to be more common than not.

We can call this method a whole bunch of times, and our Random instance will keep giving us new int values:

Random random = new Random();
for (int time = 0; time < 100; time++)
{
    Console.WriteLine(random.Next());
}

This will generate and display 100 random integers. We do not need to make a new Random instance each time. We can keep going back to the object we already made.

Limiting the Range of Generated Numbers

Random.Next() is an interesting method, but picks any legitimate positive int value, which is usually a range far bigger than makes sense for most things. There is an overload of the Next method that allows you to pick a maximum value by supplying an argument of that maximum:

random.Next(6);

This will generate random values, but only ones that are 0 through 6, including the 0 but not including the 6. That is, you could get 0, 1, 2, 3, 4, or 5, but not 6.

That may not be what you want in all cases. You might, for example, want the numbers 1 through 6, inclusive, but not zero. One solution to that is to just add one to the result of random.Next(6):

int dieRoll = random.Next(6) + 1;

This is what most programmers do.

But there is a third overload of Next that allows you to specify a lower and upper bound. The lower bound is included while the upper bound is not, but it works like this:

int twentySomething = random.Next(20, 30);

This will generate a number between 20 and 30, but it will include 20 (as zero was included before) but not the 30.

Generating Random Floating-Point Values

Aside from Next, there are also methods for generating random float and double values:

float randomFloat = random.NextSingle();
double randomDouble = random.NextDouble();

These will always pick a value between 0.0 and 1.0 (0.0 is included but 1.0 is not), but you an multiply that by another number or add or subtract numbers to refine the range.

Specifying a Seed

One important thing to point out about random number generation on a computer is that most random number generation isn't truly random. It is done by some complicated algorithm that makes data appear random, but that is actually repeatable if you start the sequence the same way. The way these "pseudo-random" sequences start is with a seed. If you reuse the same seed, you can get the same sequence to generate over and over.

But that means you'd just make the same random-looking sequence over and over, which isn't very random.

To get around that, programmers will sometimes seed the sequence with time or some other bit of information that changes to ensure we see different sequences. The way we made our Random instance already picks an arbitrary seed value to start, but if you want to specify the seed yourself, you can do so by using one of Random's other constructors:

Random r1 = new Random(202);
Random r2 = new Random(202);
Random r3 = new Random(205);
 
Console.WriteLine(r1.Next());
Console.WriteLine(r2.Next());
Console.WriteLine(r3.Next());

Here, we've made three instances of Random, two with the same seed. When we call Next on the instance stored in r1 and r2, we get the exact same random-looking number. But we get a different one when we call Next on the instance stored in r3.

All of that gives us some insight into how classes and instances/objects work. The rest of this tutorial covers some slightly advanced topics related to objects. If you're in a rush, you could skip ahead now, but if you've got time, I recommend continuing.

The Stack vs. the Heap (Extra Information)

This next topic is rather complicated, so don't worry too much if you're getting a little lost. It takes some time and experience for it to really settle in most people's minds. Consider this a first pass at the material, but you'll want to keep thinking about it over time to truly understand it.

Your C# programs divide the memory it gets from the operating system into two main areas: a stack and a heap.

The stack is primarily for storing data related to method calls, but that includes local variables and parameters. As a method is called, space is reserved for the method and its data on the stack. Each additional method call reserves another chunk on the stack for its variables. When a method finishes, that chunk of memory that was reserved for its data can be safely cleaned up because the method is over.

However, not all data fits cleanly into the little boxes for methods on the stack. The main chunk of memory that an object needs, for example, cannot reasonably live on the stack, so it is placed in another place: the heap.

The heap is not as well organized as the stack. Memory is made for some object or another when the object comes into existence and is cleaned up later, when the object goes out of existence. But as methods are called and completed, the heap does not instantly get cleaned up. (More on that later.)

When you do something like int x = 3;, there is a 4-byte spot on the stack reserved for an int value, which is eventually assigned the value 3.

But when you do something like Random random = new Random(); a spot on the stack is reserved for random, but it is big enough only to hold a reference, not the entire object. The memory for the new Random instance is made in the heap, and only the reference is placed on the stack in random. (On 64-bit computers, these references are 64 bits big, or 8 bytes. On 32-bit computers, these references are 32 bits, or 4 bytes.)

This would normally just be a minor implementation detail that you could mostly ignore, except when it isn't. Consider this method:

static bool GenerateNumberAndCompare(Random generator, int limit)
{
    int value = generator.Next();
    bool isAboveLimit;
 
    if (value > limit) { isAboveLimit = true; }
    else { isAboveLimit = false; }
 
    return isAboveLimit;
}

Which can be called like this:

Random random = new Random();
int theLimit = 1000;
bool aboveLimit = GenerateNumberAndCompare(random, theLimit);
Console.WriteLine(aboveLimit);

Here we're passing both an object and an int to the GenerateNumberAndCompare method.

When we do this, in the int case, the contents of limit are copied and sent to the GenerateNumberAndCompare method. The GenerateNumberAndCompare method, however, has its own memory location for limit. Even if it modified limit, it would have no impact on theLimit back in the main method.

The same thing essentially happens with random, and the contents of random is copied and given to GenerateNumberAndCompare, but this is the reference to the object on the heap. Now both the main method and GenerateNumberAndCompare know about the same instance that actually lives on the heap!

There won't be any substantial consequences to this in this particular case, but there can be in other cases. For example, arrays work the same way as objects (an array is an object). We could pass a reference to an array to a method:

static void TweakNumbers(int[] numbers)
{
    for (int index = 0; index < numbers.Length; index++)
    {
        numbers[index]++;
    }
}

This can be called like so:

int[] numbers = new int[] { 1, 2, 3 };
TweakNumbers(numbers);

In this case, TweakNumbers gets a copy of the array reference, but it can use that reference to find the array object on the heap, and by writing new values into it, results in a change that anybody with that same reference is able to see, since they all have access to the same object.

All objects, including arrays and also the string type, work in this same way. They are known as reference types. Things that don't get made separately and have references are called value types, and most of the other types we've seen fit into that category, including int, char, bool, float, and double.

Garbage Collection: Where Objects Go to Die (Extra Information)

As mentioned in the previous section, things on the stack are automatically cleaned up when you return from the method that put them there. But what about things on the heap?

These are also cleaned up for you.

The runtime that your C# programs run within keeps track of which objects have been placed in the heap and knows who has a reference to them.

A component called the garbage collector periodically wakes up and inspects heap memory. If it finds objects that are no longer reachable by the "living" part of your program, then it marks it for cleanup and subsequently frees up the space on the heap for other objects.

This is a huge oversimplification of what happens, but it is enough to get started.

In general, you won't need to do anything to clean up dead objects aside from ditching your reference to the object. It mostly happens automatically, without you having to think about it.

What's Next?

We've now covered the basics of using objects—what they are and how to create them. And, if you read the Extra Information sections, you should have at least a basic understanding of what's going on behind the scenes with objects.

Our next big step is to create our own classes and objects. It is here that we really get to the core of what C# is.