オブジェクト

明示的にクラスを継承していないクラスは、自動的にObjectクラスを継承する。

この定義により、クラス階層の最上位のクラスはObjectを継承する。

// ": Object"は記述されていない; 自動的に追加される
class MusicalInstrument : Object  {
    // ...
}

// Objectを間接的に継承している
class StringInstrument : MusicalInstrument {
    // ...
}
D

最上位のクラスがObjectを継承するため、すべてのクラスは間接的にObjectも継承する。その意味で、すべてのクラスは"Objectである"と言える。

すべてのクラスは、Objectの以下のメンバー関数を継承する。

このうち最後の3つの関数は、オブジェクトの値を強調する。また、これらの関数により、クラスは連想配列のキー型として使用可能になる。

これらの関数は継承されるため、サブクラスで再定義するにはoverrideキーワードが必要だ。

注釈: Objectは他のメンバーも定義する。この章では、その4つのメンバー関数についてのみ説明する。

typeidTypeInfo

Objectは、objectモジュール(stdパッケージの一部ではない)で定義されている。objectモジュールは、型に関する情報を提供するクラスであるTypeInfoも定義している。すべての型には、固有のTypeInfoオブジェクトがある。typeid式は、特定の型に関連付けられたTypeInfoオブジェクトへのアクセスを提供する。後で説明するように、TypeInfoクラスは、2つの型が同じであるかどうかを判断したり、型の特殊関数 (toHashpostblitなど)にアクセスしたりするために使用できる。

TypeInfoは、常に実際の実行時の型に関するものだ。例えば、以下のViolinGuitarは、どちらもStringInstrumentを直接、MusicalInstrumentを間接的に継承しているが、ViolinGuitarTypeInfoインスタンスは異なる。これらは、それぞれViolin型とGuitar型に正確に対応している。

class MusicalInstrument {
}

class StringInstrument : MusicalInstrument {
}

class Violin : StringInstrument {
}

class Guitar : StringInstrument {
}

void main() {
    TypeInfo v = typeid(Violin);
    TypeInfo g = typeid(Guitar);
    assert(v != g);    // ← 2つの型は同じではない
}

上記のtypeid式は、Violin自体のような型で使用されている。typeidも式を受け取ることができ、その場合はその式のランタイム型に対応するTypeInfoオブジェクトを返す。例えば、次の関数は、異なるが関連した2つの型をパラメータとして受け取る。

import std.stdio;

// ...

void foo(MusicalInstrument m, StringInstrument s) {
    const isSame = (typeid(m) == typeid(s));

    writefln("The types of the arguments are %s.",
             isSame ? "the same" : "different");
}

// ...

    auto a = new Violin();
    auto b = new Violin();
    foo(a, b);
D

foo()の2つの引数は、その特定の呼び出しに対して2つのViolinオブジェクトであるため、foo()は、それらの型が同じであると判断する。

引数の型は同じだ。

.sizeofおよびtypeofは式を実行することはないが、typeidは、受け取った式を常に実行する。

import std.stdio;

int foo(string when) {
    writefln("Called during '%s'.", when);
    return 0;
}

void main() {
    const s = foo("sizeof").sizeof;     // foo()は呼び出されない
    alias T = typeof(foo("typeof"));    // foo()は呼び出されない
    auto ti = typeid(foo("typeid"));    // foo()が呼び出される
}

出力は、typeidの式だけが実行されたことを示している。

'typeid'中に呼び出される。

この違いは、式の実行型は、その式が実行されるまでわからないためだ。例えば、次の関数の正確な戻り値の型は、関数引数iの値に応じて、ViolinまたはGuitarのいずれかになる。

MusicalInstrument foo(int i) {
    return (i % 2) ? new Violin() : new Guitar();
}
D

TypeInfoには、配列、構造体、クラスなど、さまざまな型に対応するさまざまなサブクラスがある。このうち、TypeInfo_Classは特に便利だ。例えば、オブジェクトの実行時型の名前は、そのTypeInfo_Class.nameプロパティからstringとして取得できる。オブジェクトのTypeInfo_Classインスタンスには、その.classinfoプロパティからアクセスできる。

TypeInfo_Class info = a.classinfo;
string runtimeTypeName = info.name;
D
toString

構造体と同様に、toStringを使用すると、クラスオブジェクトを文字列として使用することができる。

auto clock = new Clock(20, 30, 0);
writeln(clock);         // clock.toString()を呼び出す
D

継承されたtoString()は通常、あまり有用ではない。これは、型の名前のみを生成するからだ。

deneme.Clock

型名の前の部分はモジュールの名前だ。上記の出力は、Clockdenemeモジュールで定義されていることを示している。

前の章で見たように、この関数はほとんどの場合、より意味のあるstring表現を生成するためにオーバーライドされる。

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);
D

出力:

20:30:00♫07:00
opEquals

演算子オーバーロードの章で見たように、このメンバー関数は、==演算子(および間接的に!=演算子)の動作に関するものだ。オブジェクトが等しいとみなされる場合、演算子の戻り値はtrueでなければならず、そうでない場合はfalseでなければならない。

警告:この関数の定義は、opCmp()と一致している必要がある。opEquals()trueを返す2つのオブジェクトの場合、opCmp()は0を返さなければならない。

構造体とは異なり、コンパイラは、式a == bを見つけたときに、すぐにa.opEquals(b)を呼び出さない。2つのクラスオブジェクトが==演算子で比較される場合、4 段階のアルゴリズムが実行される。

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)
}
D
  1. 2つの変数が同じオブジェクトへのアクセスを提供する(またはどちらもnullである)場合、それらは等しい。
  2. 前のチェックの結果、一方だけがnullの場合、それらは等しくない。
  3. 両方のオブジェクトが同じ型の場合、a.opEquals(b)が呼び出されて等価性が判断される。
  4. それ以外の場合、2つのオブジェクトが等しいとみなされるためには、opEqualsが両方の型に対して定義されており、a.opEquals(b)およびb.opEquals(a)が、オブジェクトが等しいことを一致して示している必要がある。

したがって、クラス型に対してopEquals()が定義されていない場合、オブジェクトの値は考慮されず、2つのクラス変数が同じオブジェクトへのアクセスを提供するかどうかによって等価性が判断される。

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

assert(variable0 != variable1); // それらは等しくない
                                // なぜなら、オブジェクトが
                                // 異なるから
D

上記の例では、2つのオブジェクトは同じ引数で構築されているが、変数は同じオブジェクトに関連付けられていないため、等価ではない。

一方、以下の2つの変数は同じオブジェクトへのアクセスを提供するため、等しい:

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

assert(partner0 == partner1);   // オブジェクトが同じであるため、
                                // これらは等しい
D

オブジェクトの同一性ではなく、その値によって比較するほうが理にかなっている場合もある。例えば、上記のvariable0variable1は、その値が同じであるため、等価であるとみなすこともできる。

構造体とは異なり、クラスに対するopEqualsのパラメータの型はObjectである。

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

    // ...
}
D

後で説明するように、このパラメータが直接使用されることはほとんどない。そのため、oという単純な名前でよいだろう。ほとんどの場合、このパラメータは、型変換で使用するのが最初の用途となる。

opEqualsのパラメーターは、==演算子の右側に現れるオブジェクトだ:

variable0 == variable1;    // oは変数1を表す
D

opEquals()の目的は、このクラス型の2つのオブジェクトを比較することなので、まず、oをこのクラスと同じ型の変数に変換する必要がある。等価比較では右辺のオブジェクトを変更することは適切ではないため、型もconst

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

    // ...
}
D

ご存知の通り、rhsは右辺の一般的な略語だ。また、std.conv.toも変換に使用できる:

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

右側の元のオブジェクトがClockに変換できる場合、rhsnullではないクラス変数になる。そうでない場合、rhsnullに設定され、オブジェクトが同じ型ではないことを示する。

プログラムの設計によっては、互換性のない2つの型のオブジェクトを比較することが意味のある場合もある。ここでは、比較が有効であるためには、rhsnullであってはならないと仮定する。したがって、次のreturn文の最初の論理式は、それがnullでないことをチェックする。そうでないと、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));
    }

    // ...
}
D

この定義により、Clockオブジェクトは、その値によって比較できるようになった。

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

assert(variable0 == variable1); // これらは等しい
                                // なぜならこれらの値が
                                // 等しいからだ
D

opEqualsを定義する際には、スーパークラスのメンバーを覚えておくことが重要だ。例えば、AlarmClockのオブジェクトを比較する場合、継承されたメンバーも考慮するとよいだろう。

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));
    }

    // ...
}
D

この式は、super == oと書くこともできる。ただし、その場合は 4 ステップのアルゴリズムが再び開始され、その結果、コードの速度が少し遅くなる可能性がある。

opCmp

この演算子は、クラスオブジェクトをソートするときに使われる。opCmpは、<<=>、および>=の背後で呼び出される関数だ。

この演算子は、左側のオブジェクトが前にある場合は負の値を、左側のオブジェクトが後にある場合は正の値を、両方のオブジェクトのソート順が同じ場合は0を返す必要がある。

警告:この関数の定義は、opEquals()と一致している必要がある。opEquals()trueを返す2つのオブジェクトの場合、opCmp()は0を返す必要がある。

toStringおよびopEqualsとは異なり、Objectにはこの関数のデフォルトの実装はない。実装がない場合、オブジェクトのソート順を比較すると例外がスローされる。

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

assert(variable0 <= variable1);    // ← 例外を発生させる
D
object.Exception: クラスdeneme.ClockにopCmpが必要
Undefined

左側と右側のオブジェクトの型が異なる場合、どうするかはプログラムの設計次第だ。1つの方法は、コンパイラが自動的に維持する型の順序を利用することである。これは、2つの型のtypeid値に対してopCmp関数を呼び出すことで実現できる。

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

    override int opCmp(Object o) const {
        /* 自動的に維持される型の順序を
         * 利用している。 */
        if (typeid(this) != typeid(o)) {
            return typeid(this).opCmp(typeid(o));
        }

        auto rhs = cast(const Clock)o;
        /* rhsがnullであるかどうかを調べる必要はない。この行では、
         * rhsがoと同じ型であることがわかっているからだ。 */

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

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

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

    // ...
}
D

上記の定義は、まず2つのオブジェクトの型が同じかどうかを調べる。同じでない場合は、型自体の順序を使用する。それ以外の場合、hourminute、およびsecondメンバーの値によってオブジェクトを比較する。

三項演算子の連鎖も使用できる:

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));
}
D

重要な場合は、スーパークラスのメンバーの比較も考慮する必要がある。次のAlarmClock.opCmpは、Clock.opCmpを最初に呼び出している。

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;
        }
    }

    // ...
}
D

上記の場合、スーパークラスの比較が0以外の値を返すと、その値によってオブジェクトのソート順がすでに決定されているため、その結果が使用される。

AlarmClockオブジェクトのソート順を比較できるようになった:

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

assert(ac0 < ac1);
D

opCmpは、他の言語機能やライブラリでも使われている。例えば、sort()関数は、要素をソートするときにopCmpを利用している。

opCmp文字列メンバーの場合

一部のメンバーが文字列である場合、それらを明示的に比較して、負、正、または0の値を返すことができる。

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;
        }
    }

    // ...
}
D

その代わりに、既存のstd.algorithm.cmp関数を使用することができる。この関数は、より高速でもある。

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);
    }

    // ...
}
D

また、Studentは、ObjectからStudentへの変換が可能であることを強制することで、互換性のない型の比較をサポートしていないことにも注意。

toHash

この関数は、クラス型のオブジェクトを連想配列のキーとして使用できるようにする。型が連想配列のとして使用される場合は影響はない。この関数を定義する場合は、opEqualsも定義する必要がある。

ハッシュテーブルのインデックス

連想配列は、ハッシュテーブルの実装だ。ハッシュテーブルは、テーブル内の要素を検索する場合に非常に高速なデータ構造である。(注釈: ソフトウェアの他のほとんどの機能と同様に、この高速性には代償がある。ハッシュテーブルは要素を順序付けずに保持するため、必要以上にスペースを消費する場合がある。

ハッシュテーブルの高速性は、まずキーに対して整数値を生成することから生まれている。これらの整数はハッシュ値と呼ばれる。ハッシュ値は、テーブルによって管理されている内部配列へのインデックス作成に使用される。

この方法の利点は、オブジェクトに対して一意の整数値を生成できる型なら、連想配列のキー型として使用できることだ。toHashは、オブジェクトのハッシュ値を返す関数だ。

Clockオブジェクトも連想配列のキー値として使用できる。

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

Clockから継承されるtoHashのデフォルトの定義では、値に関係なく、オブジェクトごとに異なるハッシュ値が生成される。これは、opEqualsのデフォルトの動作が、異なるオブジェクトを等しくないものとみなすのと同じだ。

上記のコードは、ClocktoHashの特別な定義がない場合でも、コンパイルおよび実行できる。ただし、そのデフォルトの動作は、ほとんどの場合、望ましいものとは異なる。そのデフォルトの動作を確認するために、要素の挿入に使用したものとは異なるオブジェクトによって要素にアクセスしよう。以下の新しいClockオブジェクトは、上記の連想配列への挿入に使用されたClockオブジェクトと同じ値を持っているが、その値は見つからない。

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

} else {
    writeln("Missing");
}
D

in演算子によると、値Clock(12, 0, 0)に対応する要素はテーブル内に存在しない。

見つからない

この意外な動作の原因は、要素を挿入する際に使用されたキーオブジェクトが、要素にアクセスする際に使用されたキーオブジェクトと異なるためだ。

メンバーの選択toHash

ハッシュ値はオブジェクトのメンバーから計算されるが、すべてのメンバーがこのタスクに適しているわけではない。

候補となるのは、オブジェクトを互いに区別するメンバーだ。例えば、StudentクラスのメンバーnameおよびlastNameは、その型のオブジェクトを識別するために使用できる場合、このタスクに適している。

一方、Studentクラスのgrades配列は、多くのオブジェクトが同じ配列を持つ可能性があるため、またgrades配列が時間経過で変更される可能性があるため、適していない。

ハッシュ値の計算

ハッシュ値の選択は、連想配列のパフォーマンスに直接影響する。さらに、ある型に対して効果的なハッシュ計算は、別の型に対してはそれほど効果的ではない場合もある。ハッシュアルゴリズムについてはこの本の範囲外なので、ここでは1つのガイドラインだけを紹介する。一般的には、値が異なると思われるオブジェクトには、異なるハッシュ値を生成したほうがいい。ただし、値が異なる2つのオブジェクトが同じインデックス値を生成しても、それはエラーではない。パフォーマンス上の理由から望ましくないだけだ。

Clockのすべてのメンバーは、そのオブジェクトを互いに区別するために重要であると考えることができる。そのため、3つのメンバーの値からハッシュ値を計算することができる。真夜中から経過した秒数は、異なる時点を表すオブジェクトの有効なハッシュ値となる。

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

    override size_t toHash() const {
        /* 1時間に3600秒、1分に60秒
         * あるため: */
        return (3600 * hour) + (60 * minute) + second;
    }

    // ...
}
D

Clockが連想配列のキー型として使用される場合は、toHashの特別な定義が使用される。その結果、上記のClock(12, 0, 0)の2つのキーオブジェクトは異なるものの、同じハッシュ値が生成されることになる。

新しい出力:

存在する

他のメンバー関数と同様に、スーパークラスも考慮する必要がある場合がある。例えば、AlarmClock.toHashは、インデックスの計算時にClock.toHashを活用することができる。

class AlarmClock : Clock {
    int alarmHour;
    int alarmMinute;

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

    // ...
}
D

注釈:上記の計算はあくまで例である。通常、整数値の加算はハッシュ値を生成する効果的な方法ではない。

浮動小数点、配列、および構造体型の変数のハッシュ値を計算するための効率的なアルゴリズムがすでに存在する。これらのアルゴリズムは、プログラマーも利用できる。

必要なことは、各メンバーのtypeidgetHash()を呼び出すことだけだ。このメソッドの構文は、浮動小数点、配列、構造体型で同じだ。

例えば、Student型のハッシュ値は、次のコードのように、そのnameメンバーから計算できる。

class Student {
    string name;

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

    // ...
}
D
構造体のハッシュ値

構造体は値型であるため、そのオブジェクトのハッシュ値は、効率的なアルゴリズムによって自動的に計算される。このアルゴリズムは、オブジェクトのすべてのメンバーを考慮する。

特定のメンバーをハッシュの計算から除外する必要があるなど、特別な理由がある場合は、構造体でもtoHash()をオーバーライドすることができる。

演習
  1. 次のクラスから始める。これは色付きの点を表すクラスだ:
    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;
        }
    }
    D

    このクラスに対して、色を無視する形でopEqualsを実装しよう。そのように実装した場合、次のassertチェックが通ることを確認しよう。

    // 色は異なる
    auto bluePoint = new Point(1, 2, Color.blue);
    auto greenPoint = new Point(1, 2, Color.green);
    
    // それらは依然として等しい
    assert(bluePoint == greenPoint);
    D
  2. まずx、次にyを考慮してopCmpを実装しよう。次のassertのチェックがパスする必要がある:
    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);
    
    /* enum Colorでは青は緑よりも前にあるが、
     * 色は無視されるため、bluePointはgreenPointの前に
     * あってはならない。 */
    assert(!(bluePoint < greenPoint));
    D

    上記のStudentクラスと同様に、enforceを使用して、互換性のない型を除外することでopCmpを実装することができる。

  3. 3つのPointオブジェクトを配列に結合する次のクラスを考えてみよう。
    class TriangularArea {
        Point[3] points;
    
        this(Point one, Point two, Point three) {
            points = [ one, two, three ];
        }
    }
    D

    そのクラスに対してtoHashを実装しよう。再び、以下のassertのチェックがパスする必要がある:

    /* area1とarea2は、偶然同じ値を持つ
     * 異なる点によって構成されている。(bluePointとgreenPointは
     * 等しいとみなすべきであることを忘れてはいけない。) */
    auto area1 = new TriangularArea(bluePoint, greenPoint, redPoint1);
    auto area2 = new TriangularArea(greenPoint, bluePoint, redPoint1);
    
    // 面積は等しくなるはず
    assert(area1 == area2);
    
    // 連想配列
    double[TriangularArea] areas;
    
    // 値はarea1によって入力されている
    areas[area1] = 1.25;
    
    // 値はarea2によってアクセスされている
    assert(area2 in areas);
    assert(areas[area2] == 1.25);
    D

    opEqualsは、toHashを定義する際に必ず定義する必要があることに注意。