オブジェクト
明示的にクラスを継承していないクラスは、自動的にObjectクラスを継承する。
この定義により、クラス階層の最上位のクラスはObjectを継承する。
最上位のクラスがObjectを継承するため、すべてのクラスは間接的にObjectも継承する。その意味で、すべてのクラスは"Objectである"と言える。
すべてのクラスは、Objectの以下のメンバー関数を継承する。
toString: オブジェクトのstring表現。opEquals: 他のオブジェクトとの等価性比較。opCmp: 別のオブジェクトとのソート順比較。toHash: 連想配列のハッシュ値。
このうち最後の3つの関数は、オブジェクトの値を強調する。また、これらの関数により、クラスは連想配列のキー型として使用可能になる。
これらの関数は継承されるため、サブクラスで再定義するにはoverrideキーワードが必要だ。
注釈: Objectは他のメンバーも定義する。この章では、その4つのメンバー関数についてのみ説明する。
typeidとTypeInfo
Objectは、objectモジュール(stdパッケージの一部ではない)で定義されている。objectモジュールは、型に関する情報を提供するクラスであるTypeInfoも定義している。すべての型には、固有のTypeInfoオブジェクトがある。typeid式は、特定の型に関連付けられたTypeInfoオブジェクトへのアクセスを提供する。後で説明するように、TypeInfoクラスは、2つの型が同じであるかどうかを判断したり、型の特殊関数 (toHash、postblitなど)にアクセスしたりするために使用できる。
TypeInfoは、常に実際の実行時の型に関するものだ。例えば、以下のViolinとGuitarは、どちらもStringInstrumentを直接、MusicalInstrumentを間接的に継承しているが、ViolinとGuitarのTypeInfoインスタンスは異なる。これらは、それぞれViolin型とGuitar型に正確に対応している。
上記のtypeid式は、Violin自体のような型で使用されている。typeidも式を受け取ることができ、その場合はその式のランタイム型に対応するTypeInfoオブジェクトを返す。例えば、次の関数は、異なるが関連した2つの型をパラメータとして受け取る。
foo()の2つの引数は、その特定の呼び出しに対して2つのViolinオブジェクトであるため、foo()は、それらの型が同じであると判断する。
引数の型は同じだ。
.sizeofおよびtypeofは式を実行することはないが、typeidは、受け取った式を常に実行する。
出力は、typeidの式だけが実行されたことを示している。
'typeid'中に呼び出される。
この違いは、式の実行型は、その式が実行されるまでわからないためだ。例えば、次の関数の正確な戻り値の型は、関数引数iの値に応じて、ViolinまたはGuitarのいずれかになる。
TypeInfoには、配列、構造体、クラスなど、さまざまな型に対応するさまざまなサブクラスがある。このうち、TypeInfo_Classは特に便利だ。例えば、オブジェクトの実行時型の名前は、そのTypeInfo_Class.nameプロパティからstringとして取得できる。オブジェクトのTypeInfo_Classインスタンスには、その.classinfoプロパティからアクセスできる。
toString
構造体と同様に、toStringを使用すると、クラスオブジェクトを文字列として使用することができる。
継承されたtoString()は通常、あまり有用ではない。これは、型の名前のみを生成するからだ。
deneme.Clock
型名の前の部分はモジュールの名前だ。上記の出力は、Clockがdenemeモジュールで定義されていることを示している。
前の章で見たように、この関数はほとんどの場合、より意味のあるstring表現を生成するためにオーバーライドされる。
出力:
| 20:30:00 | ♫07:00 |
opEquals
演算子オーバーロードの章で見たように、このメンバー関数は、==演算子(および間接的に!=演算子)の動作に関するものだ。オブジェクトが等しいとみなされる場合、演算子の戻り値はtrueでなければならず、そうでない場合はfalseでなければならない。
警告:この関数の定義は、opCmp()と一致している必要がある。opEquals()がtrueを返す2つのオブジェクトの場合、opCmp()は0を返さなければならない。
構造体とは異なり、コンパイラは、式a == bを見つけたときに、すぐにa.opEquals(b)を呼び出さない。2つのクラスオブジェクトが==演算子で比較される場合、4 段階のアルゴリズムが実行される。
- 2つの変数が同じオブジェクトへのアクセスを提供する(またはどちらも
nullである)場合、それらは等しい。 - 前のチェックの結果、一方だけが
nullの場合、それらは等しくない。 - 両方のオブジェクトが同じ型の場合、
a.opEquals(b)が呼び出されて等価性が判断される。 - それ以外の場合、2つのオブジェクトが等しいとみなされるためには、
opEqualsが両方の型に対して定義されており、a.opEquals(b)およびb.opEquals(a)が、オブジェクトが等しいことを一致して示している必要がある。
したがって、クラス型に対してopEquals()が定義されていない場合、オブジェクトの値は考慮されず、2つのクラス変数が同じオブジェクトへのアクセスを提供するかどうかによって等価性が判断される。
上記の例では、2つのオブジェクトは同じ引数で構築されているが、変数は同じオブジェクトに関連付けられていないため、等価ではない。
一方、以下の2つの変数は同じオブジェクトへのアクセスを提供するため、等しい:
オブジェクトの同一性ではなく、その値によって比較するほうが理にかなっている場合もある。例えば、上記のvariable0とvariable1は、その値が同じであるため、等価であるとみなすこともできる。
構造体とは異なり、クラスに対するopEqualsのパラメータの型はObjectである。
後で説明するように、このパラメータが直接使用されることはほとんどない。そのため、oという単純な名前でよいだろう。ほとんどの場合、このパラメータは、型変換で使用するのが最初の用途となる。
opEqualsのパラメーターは、==演算子の右側に現れるオブジェクトだ:
opEquals()の目的は、このクラス型の2つのオブジェクトを比較することなので、まず、oをこのクラスと同じ型の変数に変換する必要がある。等価比較では右辺のオブジェクトを変更することは適切ではないため、型もconst
ご存知の通り、rhsは右辺の一般的な略語だ。また、std.conv.toも変換に使用できる:
右側の元のオブジェクトがClockに変換できる場合、rhsはnullではないクラス変数になる。そうでない場合、rhsはnullに設定され、オブジェクトが同じ型ではないことを示する。
プログラムの設計によっては、互換性のない2つの型のオブジェクトを比較することが意味のある場合もある。ここでは、比較が有効であるためには、rhsがnullであってはならないと仮定する。したがって、次のreturn文の最初の論理式は、それがnullでないことをチェックする。そうでないと、rhsのメンバーにアクセスしようとするとエラーになる。
この定義により、Clockオブジェクトは、その値によって比較できるようになった。
opEqualsを定義する際には、スーパークラスのメンバーを覚えておくことが重要だ。例えば、AlarmClockのオブジェクトを比較する場合、継承されたメンバーも考慮するとよいだろう。
この式は、super == oと書くこともできる。ただし、その場合は 4 ステップのアルゴリズムが再び開始され、その結果、コードの速度が少し遅くなる可能性がある。
opCmp
この演算子は、クラスオブジェクトをソートするときに使われる。opCmpは、<、<=、>、および>=の背後で呼び出される関数だ。
この演算子は、左側のオブジェクトが前にある場合は負の値を、左側のオブジェクトが後にある場合は正の値を、両方のオブジェクトのソート順が同じ場合は0を返す必要がある。
警告:この関数の定義は、opEquals()と一致している必要がある。opEquals()がtrueを返す2つのオブジェクトの場合、opCmp()は0を返す必要がある。
toStringおよびopEqualsとは異なり、Objectにはこの関数のデフォルトの実装はない。実装がない場合、オブジェクトのソート順を比較すると例外がスローされる。
左側と右側のオブジェクトの型が異なる場合、どうするかはプログラムの設計次第だ。1つの方法は、コンパイラが自動的に維持する型の順序を利用することである。これは、2つの型のtypeid値に対してopCmp関数を呼び出すことで実現できる。
上記の定義は、まず2つのオブジェクトの型が同じかどうかを調べる。同じでない場合は、型自体の順序を使用する。それ以外の場合、hour、minute、およびsecondメンバーの値によってオブジェクトを比較する。
三項演算子の連鎖も使用できる:
重要な場合は、スーパークラスのメンバーの比較も考慮する必要がある。次のAlarmClock.opCmpは、Clock.opCmpを最初に呼び出している。
上記の場合、スーパークラスの比較が0以外の値を返すと、その値によってオブジェクトのソート順がすでに決定されているため、その結果が使用される。
AlarmClockオブジェクトのソート順を比較できるようになった:
opCmpは、他の言語機能やライブラリでも使われている。例えば、sort()関数は、要素をソートするときにopCmpを利用している。
opCmp文字列メンバーの場合
一部のメンバーが文字列である場合、それらを明示的に比較して、負、正、または0の値を返すことができる。
その代わりに、既存のstd.algorithm.cmp関数を使用することができる。この関数は、より高速でもある。
また、Studentは、ObjectからStudentへの変換が可能であることを強制することで、互換性のない型の比較をサポートしていないことにも注意。
toHash
この関数は、クラス型のオブジェクトを連想配列のキーとして使用できるようにする。型が連想配列の値として使用される場合は影響はない。この関数を定義する場合は、opEqualsも定義する必要がある。
ハッシュテーブルのインデックス
連想配列は、ハッシュテーブルの実装だ。ハッシュテーブルは、テーブル内の要素を検索する場合に非常に高速なデータ構造である。(注釈: ソフトウェアの他のほとんどの機能と同様に、この高速性には代償がある。ハッシュテーブルは要素を順序付けずに保持するため、必要以上にスペースを消費する場合がある。
ハッシュテーブルの高速性は、まずキーに対して整数値を生成することから生まれている。これらの整数はハッシュ値と呼ばれる。ハッシュ値は、テーブルによって管理されている内部配列へのインデックス作成に使用される。
この方法の利点は、オブジェクトに対して一意の整数値を生成できる型なら、連想配列のキー型として使用できることだ。toHashは、オブジェクトのハッシュ値を返す関数だ。
Clockオブジェクトも連想配列のキー値として使用できる。
Clockから継承されるtoHashのデフォルトの定義では、値に関係なく、オブジェクトごとに異なるハッシュ値が生成される。これは、opEqualsのデフォルトの動作が、異なるオブジェクトを等しくないものとみなすのと同じだ。
上記のコードは、ClockにtoHashの特別な定義がない場合でも、コンパイルおよび実行できる。ただし、そのデフォルトの動作は、ほとんどの場合、望ましいものとは異なる。そのデフォルトの動作を確認するために、要素の挿入に使用したものとは異なるオブジェクトによって要素にアクセスしよう。以下の新しいClockオブジェクトは、上記の連想配列への挿入に使用されたClockオブジェクトと同じ値を持っているが、その値は見つからない。
in演算子によると、値Clock(12, 0, 0)に対応する要素はテーブル内に存在しない。
見つからない
この意外な動作の原因は、要素を挿入する際に使用されたキーオブジェクトが、要素にアクセスする際に使用されたキーオブジェクトと異なるためだ。
メンバーの選択toHash
ハッシュ値はオブジェクトのメンバーから計算されるが、すべてのメンバーがこのタスクに適しているわけではない。
候補となるのは、オブジェクトを互いに区別するメンバーだ。例えば、StudentクラスのメンバーnameおよびlastNameは、その型のオブジェクトを識別するために使用できる場合、このタスクに適している。
一方、Studentクラスのgrades配列は、多くのオブジェクトが同じ配列を持つ可能性があるため、またgrades配列が時間経過で変更される可能性があるため、適していない。
ハッシュ値の計算
ハッシュ値の選択は、連想配列のパフォーマンスに直接影響する。さらに、ある型に対して効果的なハッシュ計算は、別の型に対してはそれほど効果的ではない場合もある。ハッシュアルゴリズムについてはこの本の範囲外なので、ここでは1つのガイドラインだけを紹介する。一般的には、値が異なると思われるオブジェクトには、異なるハッシュ値を生成したほうがいい。ただし、値が異なる2つのオブジェクトが同じインデックス値を生成しても、それはエラーではない。パフォーマンス上の理由から望ましくないだけだ。
Clockのすべてのメンバーは、そのオブジェクトを互いに区別するために重要であると考えることができる。そのため、3つのメンバーの値からハッシュ値を計算することができる。真夜中から経過した秒数は、異なる時点を表すオブジェクトの有効なハッシュ値となる。
Clockが連想配列のキー型として使用される場合は、toHashの特別な定義が使用される。その結果、上記のClock(12, 0, 0)の2つのキーオブジェクトは異なるものの、同じハッシュ値が生成されることになる。
新しい出力:
存在する
他のメンバー関数と同様に、スーパークラスも考慮する必要がある場合がある。例えば、AlarmClock.toHashは、インデックスの計算時にClock.toHashを活用することができる。
注釈:上記の計算はあくまで例である。通常、整数値の加算はハッシュ値を生成する効果的な方法ではない。
浮動小数点、配列、および構造体型の変数のハッシュ値を計算するための効率的なアルゴリズムがすでに存在する。これらのアルゴリズムは、プログラマーも利用できる。
必要なことは、各メンバーのtypeidでgetHash()を呼び出すことだけだ。このメソッドの構文は、浮動小数点、配列、構造体型で同じだ。
例えば、Student型のハッシュ値は、次のコードのように、そのnameメンバーから計算できる。
構造体のハッシュ値
構造体は値型であるため、そのオブジェクトのハッシュ値は、効率的なアルゴリズムによって自動的に計算される。このアルゴリズムは、オブジェクトのすべてのメンバーを考慮する。
特定のメンバーをハッシュの計算から除外する必要があるなど、特別な理由がある場合は、構造体でもtoHash()をオーバーライドすることができる。
演習
- 次のクラスから始める。これは色付きの点を表すクラスだ:
このクラスに対して、色を無視する形で
opEqualsを実装しよう。そのように実装した場合、次のassertチェックが通ることを確認しよう。 - まず
x、次にyを考慮してopCmpを実装しよう。次のassertのチェックがパスする必要がある:上記の
Studentクラスと同様に、enforceを使用して、互換性のない型を除外することでopCmpを実装することができる。 - 3つの
Pointオブジェクトを配列に結合する次のクラスを考えてみよう。そのクラスに対して
toHashを実装しよう。再び、以下のassertのチェックがパスする必要がある:opEqualsは、toHashを定義する際に必ず定義する必要があることに注意。