構造体

この本ではこれまで何度か述べてきたように、基本型はより高レベルの概念を表現するには適していない。例えば、int型の値は1日の時間を表現するには適しているが、ある時点を表すには、2つのint変数(1つは時間用、もう1つは分用)を組み合わせたほうがより適している。

構造体は、既存の他の型を組み合わせて新しい型を定義できる機能だ。新しい型は、structキーワードで定義する。この定義により、構造体はユーザー定義型となる。この章の内容のほとんどは、クラスにも直接適用できる。特に、既存の型を組み合わせて新しい型を定義するという概念は、クラスでもまったく同じだ。

この章では、構造体の基本的な機能についてのみ説明する。構造体については、次の章で詳しく説明する。

構造体の有用性を理解するために、assertおよびenforceの章で以前に定義したaddDuration()関数を見てみよう。以下の定義は、その章の演習問題の解答から引用したものだ。

void addDuration(int startHour, int startMinute,
                 int durationHour, int durationMinute,
                 out int resultHour, out int resultMinute) {
    resultHour = startHour + durationHour;
    resultMinute = startMinute + durationMinute;
    resultHour += resultMinute / 60;

    resultMinute %= 60;
    resultHour %= 24;
}
D

注釈:この章では、コード例を簡潔にするため、inout、およびunittestブロックは無視する。

上記の関数は明らかに6つのパラメータを取っているが、3 組のパラメータを考慮すると、開始時間、継続時間、結果の3ビットの情報しか取っていないことになる。

定義

structキーワードは、何らかの関連がある変数を組み合わせて新しい型を定義する。

struct TimeOfDay {
    int hour;
    int minute;
}
D

上記のコードは、hourminuteという2つの変数で構成される、TimeOfDayという新しい型を定義している。この定義により、新しいTimeOfDay型は、他の型と同じようにプログラムで使用できるようになる。次のコードは、intの使用法と、その使用法がどれほど似ているかを示している。

int number;            // 変数
number = otherNumber;  // otherNumberの値を取る

TimeOfDay time;        // オブジェクト
time = otherTime;      // otherTimeの値を取る
D

structの定義の構文は次の通りだ:

struct TypeName {
    // ... メンバー変数および関数 ...
}
D

メンバー関数については、後の章で説明する。

構造体が結合する変数は、そのメンバーと呼ばれる。この定義によると、TimeOfDayには2つのメンバー、hourminuteがある。

structは変数ではなく型を定義する

ここで重要な違いがある。特に"名前空間と 有効期間 " および"基本操作"の章を読んだ後では、struct定義の波括弧が、構造体のメンバーがそのスコープ内で有効になり、そのスコープ内で無効になるという誤解を招くかもしれない。しかし、これは間違っている。

メンバー定義は変数定義ではない:

struct TimeOfDay {
    int hour;      // ← 変数ではない; プログラムで使用される
                   //   構造体変数の一部になる。

    int minute;    // ← 変数ではない; プログラムで使用される
                   //   構造体変数の一部になる。
}
D

structの定義は、そのstructのオブジェクトが持つメンバーの型と名前を決定する。これらのメンバー変数は、プログラムに参加するTimeOfDayオブジェクトの一部として構築される。

TimeOfDay bedTime;    // このオブジェクトは、独自の
                      // 時と分のメンバー変数を含む。

TimeOfDay wakeUpTime; // このオブジェクトは、独自の
                      // 時と分のメンバー変数も
                      // 含む。このオブジェクトの
                      // メンバー変数は、
                      // 前のオブジェクトの
                      // メンバー変数とは関連がない。
D

structおよびclass型の変数は、オブジェクトと呼ばれる

コーディングの利便性

時間と分という概念を新しい型として組み合わせることができるのは、非常に便利である。例えば、上記の関数は、既存の6つのintパラメータの代わりに3つのTimeOfDayパラメータを使用することで、より意味のある形に書き直すことができる。

void addDuration(TimeOfDay start,
                 TimeOfDay duration,
                 out TimeOfDay result) {
    // ...
}
D

注釈:2つの時点を表す2つの変数を加算するのは通常ではない。例えば、朝食の時間7:30に昼食の時間12:00を加算しても意味がない。別の型、Durationという適切な名前で定義し、その型のオブジェクトをTimeOfDayオブジェクトに加算するほうが理にかなっている。この設計上の欠陥はあるが、この章ではTimeOfDayオブジェクトのみを使用し、Durationは後の章で紹介する。

ご存じのとおり、関数は1つの値しか返さない。これが、先のaddDuration()の定義で2つのoutパラメータが必要だった理由だ。1つの値では、時と分の情報を返すことができなかったからだ。

構造体はこの制限も取り除く。複数の値を1つのstruct型として組み合わせることができるため、関数はそのようなstructのオブジェクトを返すことができ、事実上、複数の値を一度に返すことができる。addDuration()は、その結果を返す関数として定義できるようになった。

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

その結果、addDuration()は、副作用を持つ関数ではなく、値を生成する関数になった。関数の章で覚えているとおり、副作用を持つよりも結果を生成する方が望ましい。

構造体は他の構造体のメンバーになることができる。例えば、次のstructには2つのTimeOfDayメンバーがある。

struct Meeting {
    string    topic;
    size_t    attendanceCount;
    TimeOfDay start;
    TimeOfDay end;
}
D

Meetingは、別のstructのメンバーになることができる。Meal構造体も存在すると仮定すると、次のようになる。

struct DailyPlan {
    Meeting projectMeeting;
    Meal    lunch;
    Meeting budgetMeeting;
}
D
メンバーへのアクセス

構造体のメンバーは、他の変数と同じように使われる。唯一の違いは、メンバーの名前の前に、実際の構造体変数とドットを指定しなければならないことだ。

start.hour = 10;
D

上記の行は、startオブジェクトのhourメンバーに値10を割り当てる。

これまで見たことを基に、addDuration()関数を書き直しよう。

TimeOfDay addDuration(TimeOfDay start,
                      TimeOfDay duration) {
    TimeOfDay result;

    result.minute = start.minute + duration.minute;
    result.hour = start.hour + duration.hour;
    result.hour += result.minute / 60;

    result.minute %= 60;
    result.hour %= 24;

    return result;
}

このバージョンの関数では、変数の名前がはるかに短くなっていることに注意。startdurationresultである。さらに、startHourのような複雑な名前を使用する代わりに、start.hourのように、それぞれの構造体変数を通じて構造体メンバーにアクセスすることができる。

新しいaddDuration()関数を使用したコードを以下に示す。開始時刻と継続時間から、学校の授業が終了する時刻を計算するコードである。

void main() {
    TimeOfDay periodStart;
    periodStart.hour = 8;
    periodStart.minute = 30;

    TimeOfDay periodDuration;
    periodDuration.hour = 1;
    periodDuration.minute = 15;

    immutable periodEnd = addDuration(periodStart,
                                      periodDuration);

    writefln("Period end: %s:%s",
              periodEnd.hour, periodEnd.minute);
}

出力:

終了時刻9:45

上記のmain()は、これまで説明してきた内容だけで記述されている。このコードは、まもなくさらに短く、よりすっきりとしたものにする。

構造

main()の最初の3行は、periodStartオブジェクトの構築について、次の3行は、periodDurationオブジェクトの構築について記述している。3行のコードでは、まずオブジェクトが定義され、その後にそのオブジェクトのhourとminuteの値が設定されている。

変数を安全に使用するには、その変数をまず一貫した状態で構築する必要がある。構築はごく一般的な操作であるため、構造体オブジェクトには特別な構築構文がある。

TimeOfDay periodStart = TimeOfDay(8, 30);
TimeOfDay periodDuration = TimeOfDay(1, 15);
D

値は、指定された順序でメンバーに自動的に割り当てられる。hourstructで最初に定義されているため、値8はperiodStart.hourに割り当てられ、30はperiodStart.minuteに割り当てられる。

型変換の章で見たように、構築構文は他の型にも使用できる。

auto u = ubyte(42);    // uはubyte
auto i = int(u);       // iはint
D
オブジェクトを構築するimmutable

オブジェクトのメンバの値を一度に指定してオブジェクトを構築できることで、オブジェクトをimmutableとして定義することができる。

immutable periodStart = TimeOfDay(8, 30);
immutable periodDuration = TimeOfDay(1, 15);
D

そうでなければ、オブジェクトを最初にimmutableとしてマークし、その後メンバーを修正することはできない:

immutable TimeOfDay periodStart;
periodStart.hour = 8;      // ← コンパイルエラー
periodStart.minute = 30;   // ← コンパイルエラー
D
末尾のメンバーは指定する必要はない

指定する値の数がメンバーの数より少ない場合がある。その場合、残りのメンバーは、それぞれの型の.init値で初期化される。

次のプログラムは、コンストラクタのパラメータが1つ少ないTestオブジェクトを毎回作成する。assertのチェックは、指定されていないメンバーが、その.init値によって自動的に初期化されることを示している。(isNaN()を呼び出す必要がある理由は、プログラムの後に説明する):

import std.math;

struct Test {
    char   c;
    int    i;
    double d;
}

void main() {
    // すべてのメンバーの初期値が指定されている
    auto t1 = Test('a', 1, 2.3);
    assert(t1.c == 'a');
    assert(t1.i == 1);
    assert(t1.d == 2.3);

    // 最後の値が欠落している
    auto t2 = Test('a', 1);
    assert(t2.c == 'a');
    assert(t2.i == 1);
    assert(isNaN(t2.d));

    // 最後の2つが欠落している
    auto t3 = Test('a');
    assert(t3.c == 'a');
    assert(t3.i == int.init);
    assert(isNaN(t3.d));

    // 初期値が指定されていない
    auto t4 = Test();
    assert(t4.c == char.init);
    assert(t4.i == int.init);
    assert(isNaN(t4.d));

    // 上記と同じ
    Test t5;
    assert(t5.c == char.init);
    assert(t5.i == int.init);
    assert(isNaN(t5.d));
}

浮動小数点型の章で覚えているように、doubleの初期値はdouble.nanである。.nan値は順序がないため、等価比較に使用しても意味がない。そのため、値が.nanと等しいかどうかを判断するには、std.math.isNaNを呼び出すのが正しい方法だ。

メンバー変数のデフォルト値の指定

メンバー変数は、既知の初期値で自動的に初期化されることが重要だ。これにより、不確定な値でプログラムが継続することを防ぐことができる。ただし、それぞれの型の.init値は、すべての型に適しているとは限らない。例えば、char.initは有効な値ではない。

構造体のメンバーの初期値は、構造体を定義するときに指定することができる。これは、例えば、ほとんど使用できない.nanの代わりに、0.0で浮動小数点メンバーを初期化する場合に便利だ。

デフォルト値は、メンバーが定義されるときに、代入構文で指定する。

struct Test {
    char   c = 'A';
    int    i = 11;
    double d = 0.25;
}
D

上記の構文は、実際には代入ではないことに注意。上記のコードは、プログラムの後半でその構造体のオブジェクトが実際に構築される際に使用されるデフォルト値を決定するだけだ。

例えば、次のTestオブジェクトは、特定の値を指定せずに構築されている。

Test t;  // メンバーには値が指定されていない
writefln("%s,%s,%s", t.c, t.i, t.d);
D

すべてのメンバーは、デフォルト値で初期化される。

t.ct.it.d
A110.25
{}構文による構築

構造体オブジェクトは、次の構文でも構築できる。

TimeOfDay periodStart = { 8, 30 };
D

前の構文と同様に、指定された値は、指定された順序でメンバーに割り当てられる。最後のメンバーにはデフォルト値が割り当てられる。

この構文は C プログラミング言語から継承されている:

auto periodStart = TimeOfDay(8, 30);    // ← 通常
TimeOfDay periodEnd = { 9, 30 };        // ← C言語のスタイル
D

この構文では、指定初期化子を使用できる。指定初期化子は、初期化値が関連付けられているメンバーを指定するためのものだ。structで定義されている順序とは異なる順序でメンバーを初期化することも可能だ。

TimeOfDay t = { minute: 42, hour: 7 };
D
コピーと代入

構造体は値型である。値型と参照型の章で説明したように、これは、すべてのstructオブジェクトが独自の値を持つことを意味する。オブジェクトは、構築時に独自の値を取得し、新しい値が割り当てられると、その値が変更される。

auto yourLunchTime = TimeOfDay(12, 0);
auto myLunchTime = yourLunchTime;

// 私の昼食時間は12:05になるだけだ:
myLunchTime.minute += 5;

// ... あなたの昼食時間は変わらない:
assert(yourLunchTime.minute == 0);
D

コピー時には、ソースオブジェクトのすべてのメンバーが自動的にデスティネーションオブジェクトの対応するメンバーにコピーされる。同様に、代入では、ソースの各メンバーがデスティネーションの対応するメンバーに代入される。

参照型である構造体メンバーには、特別な注意が必要だ。

参照型であるメンバーには注意。

ご存じのとおり、参照型の変数をコピーまたは代入しても、値は変更されず、参照されているオブジェクトが変更されるだけだ。その結果、コピーまたは代入によって、右側のオブジェクトへの参照が1つ追加される。これが構造体メンバーに関連するのは、2つの別々の構造体オブジェクトのメンバーが、同じ値へのアクセスを提供し始めるからだ。

この例を見るために、メンバーの一方が参照型である構造体を見てみよう。この構造体は、学生の学生番号と成績を保持するために使用される。

struct Student {
    int number;
    int[] grades;
}
D

次のコードは、既存のオブジェクトをコピーして2つ目のStudentオブジェクトを構築する:

// 最初のオブジェクトを構築する:
auto student1 = Student(1, [ 70, 90, 85 ]);

// 2番目の学生を1番目の学生のコピーとして構築し、
// その番号を変更する:
auto student2 = student1;
student2.number = 2;

// 警告: 2つのオブジェクトで成績が共有されるようになった!

// 1番目の学生の成績を変更すると...
student1.grades[0] += 5;

// ... 2番目の学生にも影響する:
writeln(student2.grades[0]);
D

student2が構築されると、そのメンバーはstudent1のメンバーの値を取得する。intは値型であるため、2番目のオブジェクトは独自のnumber値を取得する。

2つのStudentオブジェクトも、それぞれ個別のgradesメンバーを持っている。ただし、スライスは参照型であるため、2つのスライスが共有する実際の要素は同じだ。その結果、一方のスライスで加えた変更は、もう一方のスライスにも反映される。

コードの出力結果から、2番目の生徒の成績も増加していることがわかる:

75

そのため、より良いアプローチは、最初のオブジェクトの成績のコピーを使用して2つ目のオブジェクトを構築することだ。

// 2人目の学生は、
// 1人目の成績のコピーによって作成されている:
auto student2 = Student(2, student1.grades.dup);

// 1人目の成績を変更しても ...
student1.grades[0] += 5;

// ... 2人目の成績には影響しない:
writeln(student2.grades[0]);
D

.dupで成績がコピーされているため、今回は2番目の生徒の成績は影響を受けない:

70

注釈: 参照メンバーも自動的にコピーすることは可能だ。その方法については、後で構造体メンバー関数について説明する。

構造体リテラル

式で変数を定義しなくても10などの整数リテラル値を使用できるのと同様に、構造体オブジェクトもリテラルとして使用できる。

構造体リテラルは、オブジェクトの構築構文によって構築される。

TimeOfDay(8, 30) // ← 構造体リテラル値
D

まず、上記のmain()関数を、これまでの学習内容に基づいて書き直しよう。変数は、構築構文によって構築され、今回はimmutableとなっている。

void main() {
    immutable periodStart = TimeOfDay(8, 30);
    immutable periodDuration = TimeOfDay(1, 15);

    immutable periodEnd = addDuration(periodStart,
                                      periodDuration);

    writefln("Period end: %s:%s",
              periodEnd.hour, periodEnd.minute);
}

periodStartおよびperiodDurationは、上記のコードでは名前付き変数として定義する必要がないことに注意。これらは、実際にはこの単純なプログラムでは一時変数であり、periodEnd変数の計算にのみ使用される。これらは、代わりにリテラル値としてaddDuration()に渡すこともできる。

void main() {
    immutable periodEnd = addDuration(TimeOfDay(8, 30),
                                      TimeOfDay(1, 15));

    writefln("Period end: %s:%s",
              periodEnd.hour, periodEnd.minute);
}
staticメンバー

オブジェクトは、ほとんどの場合、構造体のメンバーの個別のコピーを必要とするが、特定の構造体型のオブジェクトがいくつかの変数を共有することが合理的な場合もある。これは、その構造体型に関する一般的な情報を維持するために必要な場合がある。

例として、その型で構築されるすべてのオブジェクトに個別の識別子を割り当てる型を考えてみよう。

struct Point {
    // 各オブジェクトの識別子
    size_t id;

    int line;
    int column;
}
D

各オブジェクトに異なるIDを割り当てるためには、次に使用可能なIDを保持するための個別の変数が必要になる。この変数は、新しいオブジェクトが作成されるたびに加算される。nextIdが別の場所で定義され、次の関数で使用可能であると仮定しよう。

Point makePoint(int line, int column) {
    size_t id = nextId;
    ++nextId;

    return Point(id, line, column);
}
D

共通のnextId変数をどこに定義するかを決定する必要がある。このような場合、staticメンバーが役立つ。

このような共通情報は、構造体のstaticメンバーとして定義する。通常のメンバーとは異なり、staticメンバーは、スレッドごとに1つずつしか存在しない。(ほとんどのプログラムは、main()関数の実行を開始する単一のスレッドで構成されていることに注意。) この1つの変数は、そのスレッド内のその構造体のすべてのオブジェクトで共有される。

import std.stdio;

struct Point {
    // 各オブジェクトの識別子
    size_t id;

    int line;
    int column;

    // 次に構築するオブジェクトのID
    static size_t nextId;
}

Point makePoint(int line, int column) {
    size_t id = Point.nextId;
    ++Point.nextId;

    return Point(id, line, column);
}

void main() {
    auto top = makePoint(7, 0);
    auto middle = makePoint(8, 0);
    auto bottom =  makePoint(9, 0);

    writeln(top.id);
    writeln(middle.id);
    writeln(bottom.id);
}

nextIdは各オブジェクトの構築時に加算されるため、各オブジェクトは一意のIDを取得する:

0
1
2

staticメンバーは型全体によって所有されているため、それらにアクセスするためのオブジェクトは必要ない。上で見たように、このようなオブジェクトには、型の名前だけでなく、その構造体のオブジェクトの名前でもアクセスできる。

++Point.nextId;
++bottom.nextId;    // 上記と同じになる
D

変数がスレッドごとに1つではなくプログラムごとに1つ必要になる場合、それらの変数はshared staticとして定義する必要がある。sharedキーワードについては後で説明する。

初期化にはstatic this()、最終化にはstatic ~this()

上記のnextIdに初期値を明示的に割り当てる代わりに、そのデフォルトの初期値である0を使用した。他の値を使用することも可能だが、

static size_t nextId = 1000;
D

ただし、このような初期化は、初期値がコンパイル時にわかっている場合にのみ可能だ。さらに、構造体をスレッドで使用する前に、特別なコードを実行しなければならない場合もある。このようなコードは、static this()スコープで記述する。

例えば、次のコードは、ファイルが存在する場合、そのファイルから初期値を読み込む。

import std.file;

struct Point {
// ...

    enum nextIdFile = "Point_next_id_file";

    static this() {
        if (exists(nextIdFile)) {
            auto file = File(nextIdFile, "r");
            file.readf(" %s", &nextId);
        }
    }
}
D

static this()ブロックの内容は、そのスレッドでstruct型が使用される前に、スレッドごとに1回だけ自動的に実行される。プログラム全体で1回だけ実行すべきコード(sharedおよびimmutable変数の初期化など)は、shared static this()およびshared static ~this()ブロックで定義する必要がある。これについては、データ共有の並行性の章で説明する。

同様に、static ~this()はスレッドの最終操作用、shared static ~this()はプログラム全体の最終操作用だ。

次の例は、nextIdの値を同じファイルに書き込むことで、前のstatic this()を補完し、プログラムの連続した実行にわたってオブジェクトIDを効果的に保持している。

struct Point {
// ...

    static ~this() {
        auto file = File(nextIdFile, "w");
        file.writeln(nextId);
    }
}
D

これで、プログラムは中断した場所からnextIdを初期化する。例えば、プログラムを2回実行した場合、出力は次のようになる。

3
4
5
演習
  1. トランプを表すCardという構造体を設計しよう。

    この構造体には、スーツと値の2つのメンバーを含めることができる。スーツを表すにはenumを使用するのが妥当だが、単に♠、♡、♢、♣という文字を使用しても構わない。

    カードの値には、intまたはdcharを使用できる。intを使用する場合、値1、11、12、13は、数字のないカード(エース、ジャック、クイーン、キング)を表すことができる。

    他にも設計上の選択がある。例えば、カードの値はenum型でも表現できる。

    この構造体のオブジェクトの構築方法は、そのメンバーの型の選択によって異なる。例えば、両方のメンバーがdcharの場合、Cardオブジェクトは次のように構築できる。

    auto card = Card('♣', '2');
    D
  2. Cardオブジェクトをパラメータとして受け取り、それを単に表示するprintCard()という関数を定義する。
    struct Card {
        // ... 構造体を定義してください ...
    }
    
    void printCard(Card card) {
        // ... 関数本体を定義してください ...
    }
    
    void main() {
        auto card = Card(/* ... */);
        printCard(card);
    }

    例えば、この関数は、クラブの2を次のように出力する。

    ♣2

    この関数の実装は、メンバーの型の選択によって異なる。

  3. newDeck()という関数を定義し、デッキの 52 枚のカードをCardスライスとして返すようにする。
    Card[] newDeck()
    out (result) {
        assert(result.length == 52);
    
    } do {
        // ... 関数本体を定義しよう ...
    }
    D

    newDeck()は、以下のコードのように呼び出すことができる必要がある:

    Card[] deck = newDeck();
    
    foreach (card; deck) {
        printCard(card);
        write(' ');
    }
    
    writeln();
    D

    出力は、52 枚の異なるカードを含む以下の例と類似している必要がある:

    ♠2♠3♠4♠5♠6♠7♠8♠9♠0♠J♠Q♠K♠A♡2♡3♡4
    ♡5♡6♡7♡8♡9♡0♡J♡Q♡K♡A♢2♢3♢4♢5♢6♢7
    ♢8♢9♢0♢J♢Q♢K♢A♣2♣3♣4♣5♣6♣7♣8♣9♣0
    ♣J♣Q♣K♣A
  4. デッキをシャッフルする関数を作成しよう。1つの方法は、std.random.uniformを使用して2枚のカードをランダムに選び、その 2 枚のカードを交換し、この処理を十分な回数繰り返すことだ。この関数は、繰り返しの回数をパラメータとして受け取る必要がある。
    void shuffle(Card[] deck, int repetition) {
        // ... 関数本体を定義しよう ...
    }
    D

    以下のように使用する:

    Card[] deck = newDeck();
    shuffle(deck, 1);
    
    foreach (card; deck) {
        printCard(card);
        write(' ');
    }
    
    writeln();
    D

    この関数は、repetition回、カードを交換する。例えば、1で呼び出した場合、出力は次のようになる。

    ♠2♠3♠4♠5♠6♠7♠8♠9♠0♠J♠Q♠K♠A♡2♡3♡4
    ♡5♡6♡7♡8♣4♡0♡J♡Q♡K♡A♢2♢3♢4♢5♢6♢7
    ♢8♢9♢0♢J♢Q♢K♢A♣2♣3♡9♣5♣6♣7♣8♣9♣0
    ♣J♣Q♣K♣A

    repetitionの値が大きいほど、デッキはよりよくシャッフルされる。

    shuffled(deck, 100);
    D

    出力:

    ♠4♣7♢9♢6♡2♠6♣6♢A♣5♢8♢3♡Q♢J♣K♣8♣4
    ♡J♣Q♠Q♠9♢0♡A♠A♡9♠7♡3♢K♢2♡0♠J♢7♡7
    ♠8♡4♣J♢4♣0♡6♢5♡5♡K♠3♢Q♠2♠5♣2♡8♣A
    ♠K♣9♠0♣3

    注釈:カードデッキをシャッフルするよりよい方法が、解答で説明されている。