メンバー関数

この章では構造体のみに焦点を当てているが、この章のほとんどの情報はクラスにも適用できる。

この章では、構造体のメンバー関数について説明し、string形式でオブジェクトを表現するために使用される特別なメンバー関数toString()を定義する。

構造体やクラスが定義されると、通常、それとともにいくつかの関数も定義される。このような関数の例は、前の章で見たことがある。addDuration()info()のオーバーロードは、TimeOfDay型で使用するために特別に記述されている。ある意味で、この2つの関数はTimeOfDayのインターフェースを定義している。

addDuration()info()の両方の最初のパラメータは、各関数が操作するTimeOfDayオブジェクトだ。さらに、これまで見てきた他のすべての関数と同様に、この2つの関数は、他のスコープの外側のモジュールレベルで定義されている。

構造体のインターフェースを決定する一連の関数の概念は、非常に一般的だ。そのため、型と密接に関連する関数は、その型の本体内で定義することができる。

メンバー関数の定義

structの波括弧で囲まれた部分で定義された関数は、メンバー関数と呼ばれる。

struct SomeStruct {
    void member_function(/* 関数のパラメータ */) {
        // ... 関数の定義 ...
    }

    // ... 構造体の他のメンバー ...
}
D

メンバー関数は、メンバー変数と同じ方法で、オブジェクトの名前からドットで区切ってアクセスする。

object.member_function(arguments);
D

入力および出力操作でstdinおよびstdoutを明示的に指定する際に、メンバー関数を以前使用したことがある。

stdin.readf(" %s", &number);
stdout.writeln(number);
D

上記のreadf()writeln()は、それぞれオブジェクトstdinstdoutに対して動作するメンバー関数の呼び出しだ。

info()をメンバー関数として定義しよう。その以前の定義は次の通りだった。

void info(TimeOfDay time) {
    writef("%02s:%02s", time.hour, time.minute);
}
D

info()をメンバー関数にするには、その定義を構造体内に移動するだけで済むわけではない。この関数は2つの点で変更する必要がある。

struct TimeOfDay {
    int hour;
    int minute;

    void info() {    // (1)
        writef("%02s:%02s", hour, minute);    // (2)
    }
}
D
  1. メンバー関数は、オブジェクトを明示的にパラメータとして受け取らない。
  2. そのため、メンバー変数は単にhourminuteとして参照される。

これは、メンバー関数は常に既存のオブジェクトに対して呼び出されるためだ。オブジェクトは、メンバー関数から暗黙的に利用可能だ。

auto time = TimeOfDay(10, 30);
time.info();
D

上記のinfo()メンバー関数は、timeオブジェクトに対して呼び出されている。関数定義内で参照されているメンバーhourおよびminuteは、timeオブジェクトのメンバー、具体的にはtime.hourおよびtime.minuteに対応している。

上記のメンバー関数の呼び出しは、以下の通常の関数呼び出しとほぼ同じである。

time.info();    // メンバー関数
info(time);     // 通常の関数(前の定義)
D

オブジェクトでメンバー関数が呼び出されると、そのオブジェクトのメンバーは関数から暗黙的にアクセス可能になる。

auto morning = TimeOfDay(10, 0);
auto evening = TimeOfDay(22, 0);

morning.info();
write('-');
evening.info();
writeln();
D

morningで呼び出されると、メンバー関数内で使用されるhourおよびminuteは、morning.hourおよびmorning.minuteを参照する。同様に、eveningで呼び出されると、これらはevening.hourおよびevening.minuteを参照する。

10:00-22:00
toString() stringの表現の場合

前章で、info()関数の制限について説明した。この関数には、少なくとももう1つ不便な点がある。それは、人間が読める形式で時刻を出力するものの、'-'文字の出力と行の終了は、依然としてプログラマが明示的に行う必要があることだ。

しかし、TimeOfDayオブジェクトを、次のコードのように基本的な型と同じように簡単に使用できれば、より便利だろう。

writefln("%s-%s", morning, evening);
D

コードを4行から1行に削減できるだけでなく、オブジェクトを任意のストリームに出力することも可能になる:

auto file = File("time_information", "w");
file.writefln("%s-%s", morning, evening);
D

ユーザー定義型のtoString()メンバー関数は特別に扱われる。これは、オブジェクトのstring表現を生成するために自動的に呼び出される。toString()は、オブジェクトのstring表現を返さなければならない。

詳細には立ち入らず、まずtoString()関数の定義を見てみよう。

import std.stdio;

struct TimeOfDay {
    int hour;
    int minute;

    string toString() {
        return "todo";
    }
}

void main() {
    auto morning = TimeOfDay(10, 0);
    auto evening = TimeOfDay(22, 0);

    writefln("%s-%s", morning, evening);
}
D
member_functions.1

toString()まだ意味のある出力を生成していないが、出力から、writefln()によって2つのオブジェクトに対して2回呼び出されたことがわかる:

todo-todo

また、info()はもう必要ないことにも注意。toString()がその機能を置き換えているからだ。

toString()の最も単純な実装は、std.stringモジュールにあるformat()を呼び出すことだ。format()は、writef()のようなフォーマット出力関数と同じように動作する。唯一の違いは、変数を表示する代わりに、string形式でフォーマットされた結果を返すことだ。

toString() format()の結果を直接返すこともできる:

import std.string;
// ...
struct TimeOfDay {
// ...
    string toString() {
        return format("%02s:%02s", hour, minute);
    }
}
D

toString()は、このオブジェクトのみの表現を返すことに注意。残りの出力は、writefln()によって処理される。これは、2つのオブジェクトに対して個別にメンバー関数toString()を呼び出し、その間に'-'文字を出力し、最後にその行を終了する。

10:00-22:00

上記で説明したtoString()の定義は、引数を受け取らず、単にstringを作成してそれを返すだけだ。toString()の別の定義では、delegate引数を受け取る。この定義については、関数ポインタ、デリゲート、およびラムダ式の章で後で説明する。

例:increment()メンバー関数

TimeOfDayオブジェクトに期間を追加するメンバー関数を定義しよう。

その前に、これまで見過ごしていた設計上の欠陥を修正しておこう。構造体の章で、addDuration()で2つのTimeOfDayオブジェクトを足すことは意味のない操作であることを学んだ。

TimeOfDay addDuration(TimeOfDay start,
                      TimeOfDay duration) {  // 無意味
    // ...
}
D

ある時点に追加するのが当然なのは、その時点からの経過時間だ。例えば、出発時間に移動時間を追加すると、到着時間が算出される。

一方、2つの時刻を引く操作は自然な操作であり、その場合は結果が期間になる

次のプログラムは、分単位の精度を持つ構造体Durationと、それを使用する関数addDuration()を定義している。

struct Duration {
    int minute;
}

TimeOfDay addDuration(TimeOfDay start,
                      Duration duration) {
    // startのコピーから始める
    TimeOfDay result = start;

    // それに継続時間を追加する
    result.minute += duration.minute;

    // オーバーフローに注意する
    result.hour += result.minute / 60;
    result.minute %= 60;
    result.hour %= 24;

    return result;
}

unittest {
    // 簡単なテスト
    assert(addDuration(TimeOfDay(10, 30), Duration(10))
           == TimeOfDay(10, 40));

    // 深夜0時の時刻
    assert(addDuration(TimeOfDay(23, 9), Duration(51))
           == TimeOfDay(0, 0));

    // 翌日の時刻
    assert(addDuration(TimeOfDay(17, 45), Duration(8 * 60))
           == TimeOfDay(1, 45));
}
D

今回は、同様の関数をメンバー関数として再定義しよう。addDuration()は、その結果として新しいオブジェクトを生成していた。代わりに、このオブジェクトを直接変更するメンバー関数increment()を定義しよう。

struct Duration {
    int minute;
}

struct TimeOfDay {
    int hour;
    int minute;

    string toString() {
        return format("%02s:%02s", hour, minute);
    }

    void increment(Duration duration) {
        minute += duration.minute;

        hour += minute / 60;
        minute %= 60;
        hour %= 24;
    }

    unittest {
        auto time = TimeOfDay(10, 30);

        // 簡単なテスト
        time.increment(Duration(10));
        assert(time == TimeOfDay(10, 40));

        // 15時間後には次の日になっているはず
        time.increment(Duration(15 * 60));
        assert(time == TimeOfDay(1, 40));

        // 22時間20分後には真夜中になっているはず
        time.increment(Duration(22 * 60 + 20));
        assert(time == TimeOfDay(0, 0));
    }
}
D

increment()オブジェクトの値を指定した期間だけ増加させる。後の章では、Dの演算子オーバーロード機能によって+=演算子構文で期間を追加する方法について説明する。

time += Duration(10);    // 後の章で説明される
D

また、unittestブロックは、主にメンバー関数のテストのために、struct定義内にも記述できることに注意。このようなunittestブロックを構造体の本体外に移動することも可能だ。

struct TimeOfDay {
    // ... 構造体の定義 ...
}

unittest {
    // ... 構造体のテスト ...
}
D
演習
  1. TimeOfDayに、指定した時間だけ時間を短縮するメンバー関数decrement()を追加しよう。increment()と同様に、その日に十分な時間がない場合は、前の日にオーバーフローするようにして。例えば、00:05から10 分を差し引くと、結果は 23:55になるはずだ。

    つまり、decrement()を実装して、以下のユニットテストに合格するようにしよう。

    struct TimeOfDay {
        // ...
    
        void decrement(Duration duration) {
            // ... この関数を実装しよう ...
        }
    
        unittest {
            auto time = TimeOfDay(10, 30);
    
            // 簡単なテスト
            time.decrement(Duration(12));
            assert(time == TimeOfDay(10, 18));
    
            // 3日11時間前
            time.decrement(Duration(3 * 24 * 60 + 11 * 60));
            assert(time == TimeOfDay(23, 18));
    
            // 23時間18分前の深夜0時であるはず
            time.decrement(Duration(23 * 60 + 18));
            assert(time == TimeOfDay(0, 0));
    
            // 1分前
            time.decrement(Duration(1));
            assert(time == TimeOfDay(23, 59));
        }
    }
    D
  2. info()のオーバーロードであるMeetingMealDailyPlanも、toString()のメンバー関数に変換しよう。info()のオーバーロードについては、関数オーバーロードの章の演習の解答を参照しよう。

    それぞれの構造体がより便利になったことに加えて、toString()メンバー関数の実装がすべて1行で構成されるようになったことに注目しよう。