Classes
Similar to structs, class
is a feature for defining new types. By this definition, classes are user defined types. Different from structs, classes provide the object oriented programming (OOP) paradigm in D. The major aspects of OOP are the following:
- Encapsulation: Controlling access to members (Encapsulation is available for structs as well but it has not been mentioned until this chapter.)
- Inheritance: Acquiring members of another type
- Polymorphism: Being able to use a more special type in place of a more general type
Encapsulation is achieved by protection attributes, which we will see in a later chapter. Inheritance is for acquiring implementations of other types. Polymorphism is for abstracting parts of programs from each other and is achieved by class interfaces.
This chapter will introduce classes at a high level, underlining the fact that they are reference types. Classes will be explained in more detail in later chapters.
Comparing with structs
In general, classes are very similar to structs. Most of the features that we have seen for structs in the following chapters apply to classes as well:
- Structs
- Member Functions
const ref
Parameters andconst
Member Functions- Constructor and Other Special Functions
- Operator Overloading
However, there are important differences between classes and structs.
Classes are reference types
The biggest difference from structs is that structs are value types and classes are reference types. The other differences outlined below are mostly due to this fact.
Class variables may be null
As it has been mentioned briefly in The null
Value and the is
Operator chapter, class variables can be null
. In other words, class variables may not be providing access to any object. Class variables do not have values themselves; the actual class objects must be constructed by the new
keyword.
As you would also remember, comparing a reference to null
by the ==
or the !=
operator is an error. Instead, the comparison must be done by the is
or the !is
operator, accordingly:
MyClass referencesAnObject = new MyClass; assert(referencesAnObject !is null); MyClass variable; // does not reference an object assert(variable is null);
The reason is that, the ==
operator may need to consult the values of the members of the objects and that attempting to access the members through a potentially null
variable would cause a memory access error. For that reason, class variables must always be compared by the is
and !is
operators.
Class variables versus class objects
Class variable and class object are separate concepts.
Class objects are constructed by the new
keyword; they do not have names. The actual concept that a class type represents in a program is provided by a class object. For example, assuming that a Student
class represents students by their names and grades, such information would be stored by the members of Student
objects. Partly because they are anonymous, it is not possible to access class objects directly.
A class variable on the other hand is a language feature for accessing class objects. Although it may seem syntactically that operations are being performed on a class variable, the operations are actually dispatched to a class object.
Let's consider the following code that we saw previously in the Value Types and Reference Types chapter:
auto variable1 = new MyClass; auto variable2 = variable1;
The new
keyword constructs an anonymous class object. variable1
and variable2
above merely provide access to that anonymous object:
(anonymous MyClass object) variable1 variable2 ───┬───────────────────┬─── ───┬───┬─── ───┬───┬─── │ ... │ │ o │ │ o │ ───┴───────────────────┴─── ───┴─│─┴─── ───┴─│─┴─── ▲ │ │ │ │ │ └────────────────────┴────────────┘
Copying
Copying affects only the variables, not the object.
Because classes are reference types, defining a new class variable as a copy of another makes two variables that provide access to the same object. The actual object is not copied.
The postblit function this(this)
is not available for classes.
auto variable2 = variable1;
In the code above, variable2
is being initialized by variable1
. The two variables start providing access to the same object.
When the actual object needs to be copied, the class must have a member function for that purpose. To be compatible with arrays, this function may be named dup()
. This function must create and return a new class object. Let's see this on a class that has various types of members:
class Foo { S o; // assume S is a struct type char[] s; int i; // ... this(S o, const char[] s, int i) { this.o = o; this.s = s.dup; this.i = i; } Foo dup() const { return new Foo(o, s, i); } }
The dup()
member function makes a new object by taking advantage of the constructor of Foo
and returns the new object. Note that the constructor copies the s
member explicitly by the .dup
property of arrays. Being value types, o
and i
are copied automatically.
The following code makes use of dup()
to create a new object:
auto var1 = new Foo(S(1.5), "hello", 42); auto var2 = var1.dup();
As a result, the objects that are associated with var1
and var2
are different.
Similarly, an immutable
copy of an object can be provided by a member function appropriately named idup()
. In this case, the constructor must be defined as pure
as well. We will cover the pure
keyword in a later chapter.
class Foo { // ... this(S o, const char[] s, int i) pure { // ... } immutable(Foo) idup() const { return new immutable(Foo)(o, s, i); } } // ... immutable(Foo) imm = var1.idup();
Assignment
Just like copying, assignment affects only the variables.
Assigning to a class variable disassociates that variable from its current object and associates it with a new object.
If there is no other class variable that still provides access to the object that has been disassociated from, then that object is going to be destroyed some time in the future by the garbage collector.
auto variable1 = new MyClass(); auto variable2 = new MyClass(); variable1 = variable2;
The assignment above makes variable1
leave its object and start providing access to variable2
's object. Since there is no other variable for variable1
's original object, that object will be destroyed by the garbage collector.
The behavior of assignment cannot be changed for classes. In other words, opAssign
cannot be overloaded for them.
Definition
Classes are defined by the class
keyword instead of the struct
keyword:
class ChessPiece { // ... }
Construction
As with structs, the name of the constructor is this
. Unlike structs, class objects cannot be constructed by the { }
syntax.
class ChessPiece { dchar shape; this(dchar shape) { this.shape = shape; } }
Unlike structs, there is no automatic object construction where the constructor parameters are assigned to members sequentially:
class ChessPiece { dchar shape; size_t value; } void main() { auto king = new ChessPiece('♔', 100); // ← compilation ERROR }
Error: no constructor for ChessPiece
For that syntax to work, a constructor must be defined explicitly by the programmer.
Destruction
As with structs, the name of the destructor is ~this
:
~this() { // ... }
However, different from structs, class destructors are not executed at the time when the lifetime of a class object ends. As we have seen above, the destructor is executed some time in the future during a garbage collection cycle. (By this distinction, class destructors should have more accurately been called finalizers).
As we will see later in the Memory Management chapter, class destructors must observe the following rules:
- A class destructor must not access a member that is managed by the garbage collector. This is because garbage collectors are not required to guarantee that the object and its members are finalized in any specific order. All members may have already been finalized when the destructor is executing.
- A class destructor must not allocate new memory that is managed by the garbage collector. This is because garbage collectors are not required to guarantee that they can allocate new objects during a garbage collection cycle.
Violating these rules is undefined behavior. It is easy to see an example of such a problem simply by trying to allocate an object in a class destructor:
class C { ~this() { auto c = new C(); // ← WRONG: Allocates explicitly // in a class destructor } } void main() { auto c = new C(); }
The program is terminated with an exception:
core.exception.InvalidMemoryOperationError@(0)
It is equally wrong to allocate new memory indirectly from the garbage collector in a destructor. For example, memory used for the elements of a dynamic array is allocated by the garbage collector as well. Using an array in a way that would require allocating a new memory block for the elements is undefined behavior as well:
~this() { auto arr = [ 1 ]; // ← WRONG: Allocates indirectly // in a class destructor }
core.exception.InvalidMemoryOperationError@(0)
Member access
Same as structs, the members are accessed by the dot operator:
auto king = new ChessPiece('♔'); writeln(king.shape);
Although the syntax makes it look as if a member of the variable is being accessed, it is actually the member of the object. Class variables do not have members, the class objects do. The king
variable does not have a shape
member, the anonymous object does.
Note: It is usually not proper to access members directly as in the code above. When that exact syntax is desired, properties should be preferred, which will be explained in a later chapter.
Operator overloading
Other than the fact that opAssign
cannot be overloaded for classes, operator overloading is the same as structs. For classes, the meaning of opAssign
is always associating a class variable with a class object.
Member functions
Although member functions are defined and used the same way as structs, there is an important difference: Class member functions can be and by-default are overridable. We will see this concept later in the Inheritance chapter.
As overridable member functions have a runtime performance cost, without going into more detail, I recommend that you define all class
functions that do not need to be overridden with the final
keyword. You can apply this guideline blindly unless there are compilation errors:
class C { final int func() { // ← Recommended // ... } }
Another difference from structs is that some member functions are automatically inherited from the Object
class. We will see in the next chapter how the definition of toString
can be changed by the override
keyword.
The is
and !is
operators
These operators operate on class variables.
is
specifies whether two class variables provide access to the same class object. It returns true
if the object is the same and false
otherwise. !is
is the opposite of is
.
auto myKing = new ChessPiece('♔'); auto yourKing = new ChessPiece('♔'); assert(myKing !is yourKing);
Since the objects of myKing
and yourKing
variables are different, the !is
operator returns true
. Even though the two objects are constructed by the same character '♔'
, they are still two separate objects.
When the variables provide access to the same object, is
returns true
:
auto myKing2 = myKing; assert(myKing2 is myKing);
Both of the variables above provide access to the same object.
Summary
- Classes and structs share common features but have big differences.
- Classes are reference types. The
new
keyword constructs an anonymous class object and returns a class variable. - Class variables that are not associated with any object are
null
. Checking againstnull
must be done byis
or!is
, not by==
or!=
. - The act of copying associates an additional variable with an object. In order to copy class objects, the type must have a special function likely named
dup()
. - Assignment associates a variable with an object. This behavior cannot be changed.