D Programming Language Tutorial
Ali Çehreli



İngilizce Kaynaklar

Diğer



Object

Classes that do not explicitly inherit any class, automatically inherit the Object class.

By that definition, the topmost class in any class hierarchy inherits Object:

// ": Object" is not written; it is automatic
class MusicalInstrument  : Object 
{
    // ...
}

// Inherits Object indirectly
class StringInstrument : MusicalInstrument
{
    // ...
}

Since the topmost class inherits Object, every class indirectly inherits Object as well. In that sense, every class "is an" Object.

Every class inherits the following member functions of Object:

The last three of these functions emphasize the values of objects. They also make a class eligible for being the key type of associative arrays.

Because these functions are inherited, their redefinitions for the subclasses require the override keyword.

Note: Object defines other members as well. This chapter will include only these four functions.

toString

Same with structs, toString enables using objects as strings:

    auto clock = new Clock(20, 30, 0);
    writeln(clock);         // Calls clock.toString()

The inherited toString() is usually not useful; it produces just the name of the type:

deneme.Clock

The part before the name of the type is the name of the module. The output above indicates that Clock has been defined in the deneme module.

As we have seen in the previous chapter, this function is almost always overridden to produce a more meaningful string representation:

import std.string;

class Clock
{
    override string toString() const
    {
        return format("%02s:%02s:%02s", hour, minute, second);
    }

    // ...
}

class AlarmClock : Clock
{
    override string toString() const
    {
        return format("%s ♫%02s:%02s", super.toString(),
                      alarmHour, alarmMinute);
    }

    // ...
}

// ...

    auto bedSideClock = new AlarmClock(20, 30, 0, 7, 0);
    writeln(bedSideClock);

The output:

20:30:00 ♫07:00
opEquals

As we have seen in the Operator Overloading chapter, this member function is about the behavior of the == operator (and the != operator indirectly). The return value of the operator must be true if the objects are considered to be equal and false otherwise.

Warning: The definition of this function must be consistent with opCmp(); for two objects that opEquals() returns true, opCmp() must return zero.

Contrary to structs, the compiler does not call a.opEquals(b) right away when it sees the expression a == b. When two class objects are compared by the == operator, a four-step algorithm is executed:

bool opEquals(Object a, Object b) {
    if (a is b) return true;                          // (1)
    if (a is null || b is null) return false;         // (2)
    if (typeid(a) == typeid(b)) return a.opEquals(b); // (3)
    return a.opEquals(b) && b.opEquals(a);            // (4)
}
  1. If the two variables provide access to the same object (or they are both null), then they are equal.
  2. Following from the previous check, if only one is null then they are not equal.
  3. If both of the objects are of the same type, then a.opEquals(b) is called to determine the equality.
  4. Otherwise, for the two objects to be considered equal, opEquals must have been defined for both of their types and a.opEquals(b) and b.opEquals(a) must agree that the objects are equal.

Accordingly, if opEquals() is not provided for a class type, then the values of the objects are not considered; rather, equality is determined by checking whether the two class variables provide access to the same object:

    auto variable0 = new Clock(6, 7, 8);
    auto variable1 = new Clock(6, 7, 8);

    assert(variable0 != variable1); // They are not equal
                                    // because the objects are
                                    // different

Even though the two objects are constructed by the same arguments above, the variables are not equal because they are not associated with the same object.

On the other hand, because the following two variables provide access to the same object, they are equal:

    auto partner0 = new Clock(9, 10, 11);
    auto partner1 = partner0;

    assert(partner0 == partner1);   // They are equal because
                                    // the object is the same

Sometimes it makes more sense to compare objects by their values instead of their identities. For example, it is conceivable that variable0 and variable1 above compare equal because their values are the same.

Different from structs, the type of the parameter of opEquals for classes is Object:

class Clock
{
    override bool opEquals(Object o) const
    {
        // ...
    }

    // ...
}

As you will see below, the parameter is almost never used directly. For that reason, it should be acceptable to name it simply as o. Most of the time the first thing to do with that parameter is to use it in a type conversion.

The parameter of opEquals is the object that appears on the right-hand side of the == operator:

    variable0 == variable1;    // o represents variable1

Since the purpose of opEquals() is to compare two objects of this class type, the first thing to do is to convert o to a variable of the same type of this class. Since it would not be appropriate to modify the right-hand side object in an equality comparison, it is also proper to convert the type as const:

    override bool opEquals(Object o) const
    {
        auto rhs = cast(const Clock)o;

        // ...
    }

As you would remember, rhs is a common abbreviation for right-hand side. Also, std.conv.to can be used for the conversion as well:

import std.conv;
// ...
        auto rhs = to!(const Clock)(o);

If the original object on the right-hand side can be converted to Clock, then rhs becomes a non-null class variable. Otherwise, rhs is set to null, indicating that the objects are not of the same type.

According to the design of a program, it may make sense to compare objects of two incompatible types. I will assume here that for the comparison to be valid, rhs must not be null; so, the first logical expression in the following return statement checks that it is not null. Otherwise, it would be an error to try to access the members of rhs:

class Clock
{
    int hour;
    int minute;
    int second;

    override bool opEquals(Object o) const
    {
        auto rhs = cast(const Clock)o;

        return (rhs &&
                (hour == rhs.hour) &&
                (minute == rhs.minute) &&
                (second == rhs.second));
    }

    // ...
}

With that definition, Clock objects can now be compared by their values:

    auto variable0 = new Clock(6, 7, 8);
    auto variable1 = new Clock(6, 7, 8);

    assert(variable0 == variable1); // Now they are equal
                                    // because their values
                                    // are equal

When defining opEquals it is important to remember the members of the superclass. For example, when comparing objects of AlarmClock it would make sense to also consider the inherited members:

class AlarmClock : Clock
{
    int alarmHour;
    int alarmMinute;

    override bool opEquals(Object o) const
    {
        auto rhs = cast(const AlarmClock)o;

        return (rhs &&
                (alarmHour == rhs.alarmHour) &&
                (alarmMinute == rhs.alarmMinute) &&
                super.opEquals(o));
    }

    // ...
}

That expression could be written as super == o as well. However, that would initiate the four-step algorithm again and as a result, the code might be a little slower.

opCmp

This operator is used when sorting class objects. opCmp is the function that gets called behind the scenes for the <, <=, >, and >=.

This operator must return a negative value when the left-hand object is before, a positive value when the left-hand object is after, and zero when both objects have the same sorting order.

Warning: The definition of this function must be consistent with opEquals(); for two objects that opEquals() returns true, opCmp() must return zero.

Unlike toString and opEquals, there is no default implementation of this function in Object. If the implementation is not available, comparing objects for sort order causes an exception to be thrown:

    auto variable0 = new Clock(6, 7, 8);
    auto variable1 = new Clock(6, 7, 8);

    assert(variable0 <= variable1);    // ← Causes exception
object.Exception: need opCmp for class deneme.Clock

It is up to the design of the program what happens when the left-hand and right-hand objects are of different types. One way is to take advantage of the order of types that is maintained by the compiler automatically. This is achieved by calling the opCmp function on the typeid values of the two types:

class Clock
{
    int hour;
    int minute;
    int second;

    override int opCmp(Object o) const
    {
        /* Taking advantage of the automatically-maintained
         * order of the types. */
        if (typeid(this) != typeid(o)) {
            return typeid(this).opCmp(typeid(o));
        }

        auto rhs = cast(const Clock)o;
        /* No need to check whether rhs is null, because it is
         * known at this line that it has the same type as o. */

        if (hour != rhs.hour) {
            return hour - rhs.hour;

        } else if (minute != rhs.minute) {
            return minute - rhs.minute;

        } else {
            return second - rhs.second;
        }
    }

    // ...
}

The definition above first checks whether the types of the two objects are the same. If not, it uses the ordering of the types themselves. Otherwise, it compares the objects by the values of their hour, minute, and second members.

A chain of ternary operators may also be used:

    override int opCmp(Object o) const
    {
        if (typeid(this) != typeid(o)) {
            return typeid(this).opCmp(typeid(o));
        }

        auto rhs = cast(const Clock)o;

        return (hour != rhs.hour
                ? hour - rhs.hour
                : (minute != rhs.minute
                   ? minute - rhs.minute
                   : second - rhs.second));
    }

If important, the comparison of the members of the superclass must also be considered. The following AlarmClock.opCmp is calling Clock.opCmp first:

class AlarmClock : Clock
{
    override int opCmp(Object o) const
    {
        auto rhs = cast(const AlarmClock)o;

        const int superResult = super.opCmp(o);

        if (superResult != 0) {
            return superResult;

        } else if (alarmHour != rhs.alarmHour) {
            return alarmHour - rhs.alarmHour;

        } else {
            return alarmMinute - rhs.alarmMinute;
        }
    }

    // ...
}

Above, if the superclass comparison returns a non-zero value then that result is used because the sort order of the objects is already determined by that value.

AlarmClock objects can now be compared for their sort orders:

    auto ac0 = new AlarmClock(8, 0, 0, 6, 30);
    auto ac1 = new AlarmClock(8, 0, 0, 6, 31);

    assert(ac0 < ac1);

opCmp is used by other language features and libraries as well. For example, the .sort property of arrays takes advantage of opCmp when sorting the elements.

opCmp for string members

When some of the members are strings, they can be compared explicitly to return a negative, positive, or zero value:

import std.exception;

class Student
{
    string name;

    override int opCmp(Object o) const
    {
        auto rhs = cast(Student)o;
        enforce(rhs);

        if (name < rhs.name) {
            return -1;

        } else if (name > rhs.name) {
            return 1;

        } else {
            return 0;
        }
    }

    // ...
}

Instead, the existing std.algorithm.cmp function can be used, which happens to be faster as well:

import std.algorithm;

class Student
{
    string name;

    override int opCmp(Object o) const
    {
        auto rhs = cast(Student)o;
        enforce(rhs);

        return cmp(name, rhs.name);
    }

    // ...
}

Also note that Student does not support comparing incompatible types by enforcing that the conversion from Object to Student is possible.

toHash

This function allows objects of a class type to be used as associative array keys. It does not affect the cases where the type is used as associative array values.

Warning: Defining only this function is not sufficient. In order for the class type to be used as associative array keys, consistent definitions of opEquals and opCmp must also be defined.

Hash table indexes

Associative arrays are a hash table implementation. Hash table is a very fast data structure when it comes to searching elements in the table. (Note: Like most other things in software, this speed comes at a cost: Hash tables must keep elements in an unordered way, and they may be taking up space that is more than exactly necessary.)

The high speed of hash tables comes from the fact that they first produce integer values for keys. These integers are called hash values. The hash values are then used for indexing into an internal array that is maintained by the table.

A benefit of this method is the fact that any type that can be converted to an integer value can be used as the key type of associative arrays. toHash is the function that returns the hash table index value for an object.

Although perhaps not very meaningful, even Clock objects can be used as associative array key values:

    string[Clock] timeTags;
    timeTags[new Clock(12, 0, 0)] = "Noon";

The default definition of toHash that is inherited from Clock produces different hash values for different objects. This is similar to how the default behavior of opEquals considers different objects as being not equal.

The code above compiles and runs even when there is no special definition of toHash for Clock. However, its default behavior is almost never what is desired. To see that default behavior, let's try to access an element by an object that is different from the one that has been used when inserting the element. Although the new Clock object below has the same value as the Clock object that has been used when inserting into the associative array above, the value cannot be found:

    if (new Clock(12, 0, 0) in timeTags) {
        writeln("Exists");

    } else {
        writeln("Missing");
    }

According to the in operator, there is no element in the table that corresponds to the value Clock(12, 0, 0):

Missing

The reason for this surprising behavior is that the key object that has been used when inserting the element is not the same as the key object that has been used when accessing the element.

Selecting members for toHash

Although the hash value is calculated from the members of an object, not every member is suitable for this task.

The candidates are the members that distinguish objects from each other. For example, the members name and lastName of a Student class would be suitable if those members can be used for identifying objects of that type.

On the other hand, a grades array of a Student class would not be suitable both because many objects may have the same array and also it is likely that the grades array may change over time.

Calculating hash values

The choice of hash values has a direct effect on the performance of associative arrays. Furthermore, a hash calculation that is effective on one type of data may not be as effective on another type of data. As hash algorithms are beyond the scope of this book, I will give just one guideline here: In general, it is better to produce different hash values for objects that are considered to have different values. However, it is not an error if two objects with different values produce the same index value; it is merely undesirable for performance reasons.

It is conceivable that all of the members of Clock are significant to distinguish its objects from each other. For that reason, the hash values can be calculated from the values of its three members. The number of seconds since midnight would be effective hash values for objects that represent different points in time:

class Clock
{
    int hour;
    int minute;
    int second;

    override size_t toHash() const
    {
        /* Because there are 3600 seconds in an hour and 60
         * seconds in a minute: */
        return (3600 * hour) + (60 * minute) + second;
    }

    // ...
}

Whenever Clock is used as the key type of associative arrays, that special definition of toHash would be used. As a result, even though the two key objects of Clock(12, 0, 0) above are distinct, they would now produce the same hash value.

The new output:

Exists

Similar to the other member functions, the superclass may need to be considered as well. For example, AlarmClock.toHash can take advantage of Clock.toHash during its index calculation:

class AlarmClock : Clock
{
    int alarmHour;
    int alarmMinute;

    override size_t toHash() const
    {
        return super.toHash() + alarmHour + alarmMinute;
    }

    // ...
}

Note: Take the calculation above just as an example. In general, adding integer values is not an effective way of generating hash values.

There are existing efficient algorithms for calculating hash values for variables of floating point, array, and struct types. These algorithms are available to the programmer as well.

What needs to be done is to call getHash() on the typeid of each member. The syntax of this method is the same for floating point, array, and struct types.

For example, hash values of a Student type can be calculated from its name member as in the following code:

class Student
{
    string name;

    override size_t toHash() const
    {
        return typeid(name).getHash(&name);
    }

    // ...
}
Hash values for structs

Since structs are value types, hash values for their objects are calculated automatically by an efficient algorithm. That algorithm takes all of the members of the object into consideration.

When there is a specific reason like needing to exclude certain members from the hash calculation, toHash() can be overridden for structs as well.

Exercises
  1. Start with the following class, which represents colored points:
  2. enum Color { blue, green, red }
    
    class Point
    {
        int x;
        int y;
        Color color;
    
        this(int x, int y, Color color)
        {
            this.x = x;
            this.y = y;
            this.color = color;
        }
    }
    

    Implement opEquals for this class in a way that ignores colors. When implemented in that way, the following assert check should pass:

        // Different colors
        auto bluePoint = new Point(1, 2, Color.blue);
        auto greenPoint = new Point(1, 2, Color.green);
    
        // They are still equal
        assert(bluePoint == greenPoint);
    
  3. Implement opCmp by considering first x then y. The following assert checks should pass:
  4.     auto redPoint1 = new Point(-1, 10, Color.red);
        auto redPoint2 = new Point(-2, 10, Color.red);
        auto redPoint3 = new Point(-2,  7, Color.red);
    
        assert(redPoint1 < bluePoint);
        assert(redPoint3 < redPoint2);
    
        /* Even though blue is before green in enum Color,
         * because color is being ignored, bluePoint must not be
         * before greenPoint. */
        assert(!(bluePoint < greenPoint));
    

    Like the Student class above, you can implement opCmp by excluding incompatible types by the help of enforce.

  5. Consider the following class that combines three Point objects in an array:
  6. class TriangularArea
    {
        Point[3] points;
    
        this(Point one, Point two, Point three)
        {
            points = [ one, two, three ];
        }
    }
    

    Implement toHash for that class. Again, the following assert checks should pass:

        /* area1 and area2 are constructed by distinct points that
         * happen to have the same values. (Remember that
         * bluePoint and greenPoint should be considered equal.) */
        auto area1 = new TriangularArea(bluePoint, greenPoint, redPoint1);
        auto area2 = new TriangularArea(greenPoint, bluePoint, redPoint1);
    
        // The areas should be equal
        assert(area1 == area2);
    
        // An associative array
        double[TriangularArea] areas;
    
        // A value is being entered by area1
        areas[area1] = 1.25;
    
        // The value is being accessed by area2
        assert(area2 in areas);
        assert(areas[area2] == 1.25);
    

    Remember that opEquals and opCmp must also be defined when toHash is defined.

... the solutions