構造体
この本ではこれまで何度か述べてきたように、基本型はより高レベルの概念を表現するには適していない。例えば、int
型の値は1日の時間を表現するには適しているが、ある時点を表すには、2つのint
変数(1つは時間用、もう1つは分用)を組み合わせたほうがより適している。
構造体は、既存の他の型を組み合わせて新しい型を定義できる機能だ。新しい型は、struct
キーワードで定義する。この定義により、構造体はユーザー定義型となる。この章の内容のほとんどは、クラスにも直接適用できる。特に、既存の型を組み合わせて新しい型を定義するという概念は、クラスでもまったく同じだ。
この章では、構造体の基本的な機能についてのみ説明する。構造体については、次の章で詳しく説明する。
- メンバー関数
- const refパラメータと constメンバー関数
- コンストラクタおよびその他の特殊関数
- 演算子オーバーロード
- カプセル化と保護属性
- プロパティ
- 構造体およびクラスの契約プログラミング
- 構造体およびクラスでの foreach
構造体の有用性を理解するために、assert
およびenforce
の章で以前に定義したaddDuration()
関数を見てみよう。以下の定義は、その章の演習問題の解答から引用したものだ。
注釈:この章では、コード例を簡潔にするため、in
、out
、およびunittest
ブロックは無視する。
上記の関数は明らかに6つのパラメータを取っているが、3 組のパラメータを考慮すると、開始時間、継続時間、結果の3ビットの情報しか取っていないことになる。
定義
struct
キーワードは、何らかの関連がある変数を組み合わせて新しい型を定義する。
上記のコードは、hour
とminute
という2つの変数で構成される、TimeOfDay
という新しい型を定義している。この定義により、新しいTimeOfDay
型は、他の型と同じようにプログラムで使用できるようになる。次のコードは、int
の使用法と、その使用法がどれほど似ているかを示している。
struct
の定義の構文は次の通りだ:
メンバー関数については、後の章で説明する。
構造体が結合する変数は、そのメンバーと呼ばれる。この定義によると、TimeOfDay
には2つのメンバー、hour
とminute
がある。
struct
は変数ではなく型を定義する
ここで重要な違いがある。特に"名前空間と 有効期間 " および"基本操作"の章を読んだ後では、struct
定義の波括弧が、構造体のメンバーがそのスコープ内で有効になり、そのスコープ内で無効になるという誤解を招くかもしれない。しかし、これは間違っている。
メンバー定義は変数定義ではない:
struct
の定義は、そのstruct
のオブジェクトが持つメンバーの型と名前を決定する。これらのメンバー変数は、プログラムに参加するTimeOfDay
オブジェクトの一部として構築される。
struct
およびclass
型の変数は、オブジェクトと呼ばれる。
コーディングの利便性
時間と分という概念を新しい型として組み合わせることができるのは、非常に便利である。例えば、上記の関数は、既存の6つのint
パラメータの代わりに3つのTimeOfDay
パラメータを使用することで、より意味のある形に書き直すことができる。
注釈:2つの時点を表す2つの変数を加算するのは通常ではない。例えば、朝食の時間7:30に昼食の時間12:00を加算しても意味がない。別の型、Duration
という適切な名前で定義し、その型のオブジェクトをTimeOfDay
オブジェクトに加算するほうが理にかなっている。この設計上の欠陥はあるが、この章ではTimeOfDay
オブジェクトのみを使用し、Duration
は後の章で紹介する。
ご存じのとおり、関数は1つの値しか返さない。これが、先のaddDuration()
の定義で2つのout
パラメータが必要だった理由だ。1つの値では、時と分の情報を返すことができなかったからだ。
構造体はこの制限も取り除く。複数の値を1つのstruct
型として組み合わせることができるため、関数はそのようなstruct
のオブジェクトを返すことができ、事実上、複数の値を一度に返すことができる。addDuration()
は、その結果を返す関数として定義できるようになった。
その結果、addDuration()
は、副作用を持つ関数ではなく、値を生成する関数になった。関数の章で覚えているとおり、副作用を持つよりも結果を生成する方が望ましい。
構造体は他の構造体のメンバーになることができる。例えば、次のstruct
には2つのTimeOfDay
メンバーがある。
Meeting
は、別のstruct
のメンバーになることができる。Meal
構造体も存在すると仮定すると、次のようになる。
メンバーへのアクセス
構造体のメンバーは、他の変数と同じように使われる。唯一の違いは、メンバーの名前の前に、実際の構造体変数とドットを指定しなければならないことだ。
上記の行は、start
オブジェクトのhour
メンバーに値10を割り当てる。
これまで見たことを基に、addDuration()
関数を書き直しよう。
このバージョンの関数では、変数の名前がはるかに短くなっていることに注意。start
、duration
、result
である。さらに、startHour
のような複雑な名前を使用する代わりに、start.hour
のように、それぞれの構造体変数を通じて構造体メンバーにアクセスすることができる。
新しいaddDuration()
関数を使用したコードを以下に示す。開始時刻と継続時間から、学校の授業が終了する時刻を計算するコードである。
出力:
終了時刻 | 9:45 |
---|
上記のmain()
は、これまで説明してきた内容だけで記述されている。このコードは、まもなくさらに短く、よりすっきりとしたものにする。
構造
main()
の最初の3行は、periodStart
オブジェクトの構築について、次の3行は、periodDuration
オブジェクトの構築について記述している。3行のコードでは、まずオブジェクトが定義され、その後にそのオブジェクトのhourとminuteの値が設定されている。
変数を安全に使用するには、その変数をまず一貫した状態で構築する必要がある。構築はごく一般的な操作であるため、構造体オブジェクトには特別な構築構文がある。
値は、指定された順序でメンバーに自動的に割り当てられる。hour
はstruct
で最初に定義されているため、値8はperiodStart.hour
に割り当てられ、30はperiodStart.minute
に割り当てられる。
型変換の章で見たように、構築構文は他の型にも使用できる。
オブジェクトを構築するimmutable
オブジェクトのメンバの値を一度に指定してオブジェクトを構築できることで、オブジェクトをimmutable
として定義することができる。
そうでなければ、オブジェクトを最初にimmutable
としてマークし、その後メンバーを修正することはできない:
末尾のメンバーは指定する必要はない
指定する値の数がメンバーの数より少ない場合がある。その場合、残りのメンバーは、それぞれの型の.init
値で初期化される。
次のプログラムは、コンストラクタのパラメータが1つ少ないTest
オブジェクトを毎回作成する。assert
のチェックは、指定されていないメンバーが、その.init
値によって自動的に初期化されることを示している。(isNaN()
を呼び出す必要がある理由は、プログラムの後に説明する):
浮動小数点型の章で覚えているように、double
の初期値はdouble.nan
である。.nan
の値は順序がないため、等価比較に使用しても意味がない。そのため、値が.nan
と等しいかどうかを判断するには、std.math.isNaN
を呼び出すのが正しい方法だ。
メンバー変数のデフォルト値の指定
メンバー変数は、既知の初期値で自動的に初期化されることが重要だ。これにより、不確定な値でプログラムが継続することを防ぐことができる。ただし、それぞれの型の.init
値は、すべての型に適しているとは限らない。例えば、char.init
は有効な値ではない。
構造体のメンバーの初期値は、構造体を定義するときに指定することができる。これは、例えば、ほとんど使用できない.nan
の代わりに、0.0
で浮動小数点メンバーを初期化する場合に便利だ。
デフォルト値は、メンバーが定義されるときに、代入構文で指定する。
上記の構文は、実際には代入ではないことに注意。上記のコードは、プログラムの後半でその構造体のオブジェクトが実際に構築される際に使用されるデフォルト値を決定するだけだ。
例えば、次のTest
オブジェクトは、特定の値を指定せずに構築されている。
すべてのメンバーは、デフォルト値で初期化される。
t.c | t.i | t.d |
---|---|---|
A | 11 | 0.25 |
{}
構文による構築
構造体オブジェクトは、次の構文でも構築できる。
前の構文と同様に、指定された値は、指定された順序でメンバーに割り当てられる。最後のメンバーにはデフォルト値が割り当てられる。
この構文は C プログラミング言語から継承されている:
この構文では、指定初期化子を使用できる。指定初期化子は、初期化値が関連付けられているメンバーを指定するためのものだ。struct
で定義されている順序とは異なる順序でメンバーを初期化することも可能だ。
コピーと代入
構造体は値型である。値型と参照型の章で説明したように、これは、すべてのstruct
オブジェクトが独自の値を持つことを意味する。オブジェクトは、構築時に独自の値を取得し、新しい値が割り当てられると、その値が変更される。
コピー時には、ソースオブジェクトのすべてのメンバーが自動的にデスティネーションオブジェクトの対応するメンバーにコピーされる。同様に、代入では、ソースの各メンバーがデスティネーションの対応するメンバーに代入される。
参照型である構造体メンバーには、特別な注意が必要だ。
参照型であるメンバーには注意。
ご存じのとおり、参照型の変数をコピーまたは代入しても、値は変更されず、参照されているオブジェクトが変更されるだけだ。その結果、コピーまたは代入によって、右側のオブジェクトへの参照が1つ追加される。これが構造体メンバーに関連するのは、2つの別々の構造体オブジェクトのメンバーが、同じ値へのアクセスを提供し始めるからだ。
この例を見るために、メンバーの一方が参照型である構造体を見てみよう。この構造体は、学生の学生番号と成績を保持するために使用される。
次のコードは、既存のオブジェクトをコピーして2つ目のStudent
オブジェクトを構築する:
student2
が構築されると、そのメンバーはstudent1
のメンバーの値を取得する。int
は値型であるため、2番目のオブジェクトは独自のnumber
値を取得する。
2つのStudent
オブジェクトも、それぞれ個別のgrades
メンバーを持っている。ただし、スライスは参照型であるため、2つのスライスが共有する実際の要素は同じだ。その結果、一方のスライスで加えた変更は、もう一方のスライスにも反映される。
コードの出力結果から、2番目の生徒の成績も増加していることがわかる:
75
そのため、より良いアプローチは、最初のオブジェクトの成績のコピーを使用して2つ目のオブジェクトを構築することだ。
.dup
で成績がコピーされているため、今回は2番目の生徒の成績は影響を受けない:
70
注釈: 参照メンバーも自動的にコピーすることは可能だ。その方法については、後で構造体メンバー関数について説明する。
構造体リテラル
式で変数を定義しなくても10などの整数リテラル値を使用できるのと同様に、構造体オブジェクトもリテラルとして使用できる。
構造体リテラルは、オブジェクトの構築構文によって構築される。
まず、上記のmain()
関数を、これまでの学習内容に基づいて書き直しよう。変数は、構築構文によって構築され、今回はimmutable
となっている。
periodStart
およびperiodDuration
は、上記のコードでは名前付き変数として定義する必要がないことに注意。これらは、実際にはこの単純なプログラムでは一時変数であり、periodEnd
変数の計算にのみ使用される。これらは、代わりにリテラル値としてaddDuration()
に渡すこともできる。
static
メンバー
オブジェクトは、ほとんどの場合、構造体のメンバーの個別のコピーを必要とするが、特定の構造体型のオブジェクトがいくつかの変数を共有することが合理的な場合もある。これは、その構造体型に関する一般的な情報を維持するために必要な場合がある。
例として、その型で構築されるすべてのオブジェクトに個別の識別子を割り当てる型を考えてみよう。
各オブジェクトに異なるIDを割り当てるためには、次に使用可能なIDを保持するための個別の変数が必要になる。この変数は、新しいオブジェクトが作成されるたびに加算される。nextId
が別の場所で定義され、次の関数で使用可能であると仮定しよう。
共通のnextId
変数をどこに定義するかを決定する必要がある。このような場合、static
メンバーが役立つ。
このような共通情報は、構造体のstatic
メンバーとして定義する。通常のメンバーとは異なり、static
メンバーは、スレッドごとに1つずつしか存在しない。(ほとんどのプログラムは、main()
関数の実行を開始する単一のスレッドで構成されていることに注意。) この1つの変数は、そのスレッド内のその構造体のすべてのオブジェクトで共有される。
nextId
は各オブジェクトの構築時に加算されるため、各オブジェクトは一意のIDを取得する:
0
1
2
static
メンバーは型全体によって所有されているため、それらにアクセスするためのオブジェクトは必要ない。上で見たように、このようなオブジェクトには、型の名前だけでなく、その構造体のオブジェクトの名前でもアクセスできる。
変数がスレッドごとに1つではなくプログラムごとに1つ必要になる場合、それらの変数はshared static
として定義する必要がある。shared
キーワードについては後で説明する。
初期化にはstatic this()
、最終化にはstatic ~this()
上記のnextId
に初期値を明示的に割り当てる代わりに、そのデフォルトの初期値である0を使用した。他の値を使用することも可能だが、
ただし、このような初期化は、初期値がコンパイル時にわかっている場合にのみ可能だ。さらに、構造体をスレッドで使用する前に、特別なコードを実行しなければならない場合もある。このようなコードは、static this()
スコープで記述する。
例えば、次のコードは、ファイルが存在する場合、そのファイルから初期値を読み込む。
static this()
ブロックの内容は、そのスレッドでstruct
型が使用される前に、スレッドごとに1回だけ自動的に実行される。プログラム全体で1回だけ実行すべきコード(shared
およびimmutable
変数の初期化など)は、shared static this()
およびshared static ~this()
ブロックで定義する必要がある。これについては、データ共有の並行性の章で説明する。
同様に、static ~this()
はスレッドの最終操作用、shared static ~this()
はプログラム全体の最終操作用だ。
次の例は、nextId
の値を同じファイルに書き込むことで、前のstatic this()
を補完し、プログラムの連続した実行にわたってオブジェクトIDを効果的に保持している。
これで、プログラムは中断した場所からnextId
を初期化する。例えば、プログラムを2回実行した場合、出力は次のようになる。
3
4
5
演習
- トランプを表す
Card
という構造体を設計しよう。この構造体には、スーツと値の2つのメンバーを含めることができる。スーツを表すには
enum
を使用するのが妥当だが、単に♠、♡、♢、♣という文字を使用しても構わない。カードの値には、
int
またはdchar
を使用できる。int
を使用する場合、値1、11、12、13は、数字のないカード(エース、ジャック、クイーン、キング)を表すことができる。他にも設計上の選択がある。例えば、カードの値は
enum
型でも表現できる。この構造体のオブジェクトの構築方法は、そのメンバーの型の選択によって異なる。例えば、両方のメンバーが
dchar
の場合、Card
オブジェクトは次のように構築できる。 Card
オブジェクトをパラメータとして受け取り、それを単に表示するprintCard()
という関数を定義する。例えば、この関数は、クラブの2を次のように出力する。
♣2
この関数の実装は、メンバーの型の選択によって異なる。
newDeck()
という関数を定義し、デッキの 52 枚のカードをCard
スライスとして返すようにする。newDeck()
は、以下のコードのように呼び出すことができる必要がある:出力は、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 - デッキをシャッフルする関数を作成しよう。1つの方法は、
std.random.uniform
を使用して2枚のカードをランダムに選び、その 2 枚のカードを交換し、この処理を十分な回数繰り返すことだ。この関数は、繰り返しの回数をパラメータとして受け取る必要がある。以下のように使用する:
この関数は、
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
の値が大きいほど、デッキはよりよくシャッフルされる。出力:
♠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 注釈:カードデッキをシャッフルするよりよい方法が、解答で説明されている。