Return to home

Introduction

We have learned about structs as custom types. But there is another kind of custom type that we will be learning about today -- classes. A class is very similar to a struct, but has several important differences. Both classes and structs provide the mechanism in D to implement Object-oriented programming. As the name suggests, OOP is centered on "objects", which are collections of related data and functions. However, classes have additional features which we will examine in future lessons.

Declaring and creating a class

Declaring a class is very similar to declaring a struct:

class Animal {
    string name;
}

An Animal is a simple class with one string field member, name. A major difference between classes and structs is that you must explicitly create a variable of a class type using a new expression. A new expression reads like this:

Animal anml = new Animal;

This creates a new object with variable name anml, which has the type Animal. An object is defined as one instance of a class type. When we create an object, we call this instantiating/. Note that we did not set the name field yet, and unlike structs, we can't specify the value of fields when instantiating them without some extra effort (we'll learn that soon). We access fields of a class in the same manner that we access fields of a struct, by using the . operator:

anml.name = "Fido";

Classes are reference types

The biggest difference between classes and structs in D are that classes are reference types. A reference type means that when we declare a variable of that type, it's really a reference to the actual thing, not the actual thing itself. When you copy a reference, you are not copying the whole object, just the reference.

Animal fido = new Animal;
fido.name = "Fido";
Animal rex = fido; // notice we do not use `new` here
rex.name = "Rex";
writeln(fido.name); // Fido's name changed to "Rex"!

You can see by testing it out that variable assignment for an object does not create a copy of the object, instead we have two references to the same object. Changing the name of the common instance can be seen through both references.

A good rule of thumb with class instances is that you will have as many objects as you have new expressions.

A variable of a class type has a default value of null. This is a special reference to nothing. Using a reference to null will result in a crash of your program. These crashes can be very uninformative. It will not tell you where the crash happened, it will just usually pop up an error box saying the program died. For that reason, it is important to always assign variables of class type to a new object or to reference an existing object.

Animal crashMe; // = null
crashMe.name = "Bad"; // program crashes here!

Member functions

Like structs, classes can have functions defined within them. Class member functions must be called from an instance, and they will have access to the data from that instance.

Let's add a function to our class:

import std.stdio;

class Animal {
    string name;

    void greet() {
        writeln("hello! My name is ", name);
    }
}

void main() {
    Animal fido = new Animal;
    fido.name = "Fido";
    fido.greet();
}

Notice how you don't have to specify an object to use the name field, it is already known that the field comes from the object the function is called on.

Member functions can take parameters as well, and have return values.

Constructors

A constructor is a special type of function that is called when an object is created. When you call new Animal it calls the constructor of the Animal type. By default, there is a constructor which does nothing. But we can create a constructor which accepts a name, and assigns it to the name field. The constructor's identifier is always the word this:

import std.stdio;

class Animal {
    string name;

    void greet() {
        writeln("hello! My name is ", name);
    }

    this(string nm) {
        name = nm;
    }
}

void main() {
    Animal anml = new Animal("Fido");
    anml.greet(); // "hello! My name is Fido
}

An object is created with a constructor by adding parentheses to the new expression, and putting the parameter inside those parentheses.

In this case, we create a new Animal, and the constructor is called with the string "Fido" as the nm parameter. Inside the constructor, we assign name to whatever value is in nm, and the object is now ready!

Using objects in arrays

Classes can also be the element type of an array. Just remember to use the new expression to add to the array:

Animal[] animals = [new Animal("Fido"), new Animal("Rex")];
animals ~= new Animal("Spot");
foreach(a; animals) {
    a.greet();
}

Exercise

For the exercise, think of some other member variables that should belong to an animal that would help describe it. Maybe a string species? Maybe an int legs? Think up as many as you want!

Add these fields to the Animal type, and update the greet member function to include more about the animal. We will be using this in further exercises, so be sure to save this for later!

Return to home

©2023-2024 Steven Schveighoffer