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.
54.1 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 refParameters andconstMember 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.
类变量必须始终通过
is和is运算符进行比较。
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.
Since no object gets copied, the postblit function this(this) is not available for classes.
由于类是引用类型,将一个新类变量定义为另一个类变量的副本,就产生了两个提供对同一对象访问的变量。实际的对象不会被复制。由于没有对象被复制,postblit函数
this(this)对类不可用。
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.
不能更改类的赋值行为。换句话说,不能为它们重载
opAssign。
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.
与结构一样,构造函数的名称是
this。与结构不同,类对象不能用{}语法构造。
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.
除了
opAssign不能为类重载之外,操作符重载与结构相同。对于类,opAssign的含义总是将类变量与类对象相关联。
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:
由于可重写的成员函数在运行时有一定的性能代价,在此不做赘述,我建议你用
final关键字来定义所有不需要重写的类函数。除非出现编译错误,否则你可以盲目地应用这一准则。
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.