オブジェクト
明示的にクラスを継承していないクラスは、自動的に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
を定義する際に必ず定義する必要があることに注意。