./index.html ../index.html

ObjectPascal Object Oriented Programming Vol.2
Metaclass and "virtual" "class" method 第1.01版

第1.01版前書き

…何ですか? 実は、Delphi使い以外に向けて書いた方がいいわけですか?

クラスメソッドのvirtual

クラスメソッドを書けないOOPLは…書けないほうが少ないでしょう。

メタクラスが存在するOOPLは…動的型言語ではほぼ100%でしょうか?

クラスメソッドをvirtualにできるOOPLは…まあ、動的型なら全部virtualですので、クラスを表すオブジェクトさえあれば…

一方、静的型言語では、クラスメソッドにstaticを用いていたりするものが多勢を占め…このキーワードをクラスメソッドに使うこと自体、発想として多態を拒んでいます。

むしろ、クラスメソッドをスコープ以外は通常の関数と同様に使いたい、という意思が感じられます。 privateにしてクラスメソッドをウィンドウプロシージャにする等…。

そもそもクラスメソッドはInstance.Methodでは無くClass.Methodのように使います。 Instanceは、常に変数の型のインスタンスを保持するわけではなく、 その派生クラスのインスタンスを保持している場合があるため、多態が成り立つわけです。 使う場所でクラス名を明記するクラスメソッドでは…無意味に思えます。

多くの言語にて、デストラクタ(或いはファイナライザ)は仮想関数にしなければいけないのに、 コンストラクタは仮想関数では無い理由もここにあります。 ObjectPascalは、コンストラクタにすらvirtualを付ける事を許します。

内側からクラスメソッドを呼べばどうなる?

クラスの外側からクラスメソッドを呼ぶ場合は、常にクラス名を併記します。

故に、クラスメソッドが多態しても無意味に思えます。

では…クラスの内側からクラスメソッドを呼べば…どうなるのでしょう?

ところで、「風来のシレン」モドキを開発していたとします。 (同じ芸で申し訳ない)

基底クラスのTShirenModokiObject(←Vol.1参照)には、画面に表示するための名前を取得するメソッドが必要です。 何でこんな基本的なメソッドも書かずに壷の処理を先に書いてたのか謎ですが、とにかく今それを書くことにします。

「シレン」では画面表示に種類の名前と個々の名前を使い分けています。 総称としての「カタナ」と「カタナ+1☆」の違いと言いましょうか。 クラスの名前とインスタンスの名前があるわけですね。 未識別アイテムに名前を付けるとクラスの名前を「書き換える」ことになりますし、たしか2でインスタンスの名前の後ろにメモを付ける機能も加わったと記憶しています。

すると「種類の名前(クラス毎に固定)」「未識別時の種類の名前(クラス毎だが可変)」「インスタンスの名前(インスタンス毎に可変)」の三種類があることになります。 「種類の名前」は、クラスに一対一対応しますから、どっちかといえばクラスメソッドが返した方が、インスタンスが無くてもカタナの名前をTKatanaから取って来れますので便利ですね。

type
  TShirenModokiObject = class(TObject, IInterface, ...)
    ...
  public
    class function KindName: string;
    class function KindDisplayName: string; {識別時はKindName, 未識別時は付けられた名前を返す}
    function InstanceName: string; {KindDisplayNameに"+1"や"☆"やメモを付加して返す}
  end;

TKatanaはこうなるでしょう。

type
  TKatana = class(TToolObject, ITool, IWeapon, ...)
    ...
  public
    class function KindName: string; {同名の関数を宣言して親のクラスメソッドを隠す}
  end;

...

class function TKatana.KindName: string;
begin
  Result := 'カタナ'
end;

これで、外からは、道具のクラス名.KindNameと書けば道具の「種類の名前」が得られます。

では…内側からは?

KindDisplayNameは、こんな感じになっている筈です。

class function TShirenModokiObject.KindDisplayName: string;
begin
  if Game.Distinguished[ClassInfo] then {ハッシュテーブルから識別フラグを得る}
    Result := KindName
  else
    Result := Game.AssumedNames[ClassInfo] {ハッシュテーブルから未識別状態の名前を得る}
end;

ClassInfoは、クラスのRTTIへのポインタを返します。 クラス名無しで書いていますので、現在のクラス…といってもTShirenModokiObjectではなくて、KindDisplayNameメソッドが、TKatana.KindDisplayNameの様に呼ばれた場合は、TKatanaクラスのRTTIへのポインタが返ります。

各ハッシュテーブルは、グローバル変数…が嫌ならTShirenModokiObjectのコンストラクタでゲーム全体を管理するオブジェクトの参照を貰っていると考えてください。 とにかく、クラス毎に固有な値であるRTTIへのポインタをキーにして、情報を管理しています。

InstanceNameは、こんな感じでしょう。 これは普通のインスタンスメソッドです。

function TShirenModokiObject.InstanceName: string;
  function Decoration;
  begin
    if Supports(Self, IWeapon) then
      Result := ...+20とか星とか髑髏とか...
    else
      ...壷なら[8]とか杖なら残り回数とか...
  end;
begin
  Result := KindDisplayName + Decoration
end;

Supportsで判断していくぐらいなら、多態させたほうがいいかもしれません。 では、Decorationはvirtualということで。

function TShirenModokiObject.InstanceName: string;
begin
  Result := KindDisplayName + Decoration
end;

さて、識別済みのカタナのInstanceNameは…ああ、もう、自分で書いてて白々しいTShirenModokiObject.KindDisplayNameから間接的に呼んでいる以上、TShirenModokiObject.KindNameが呼ばれて、多分空文字列なりAssertion Failureなりになります。

では、KindNameは、クラスメソッドにはせずに、クラスと名前の対応テーブルを作ってそこから引いてくるべきでしょうか?

そんな、それじゃ、仮想関数では無くcaseで分岐していた時代と変わらないじゃないか!
…実際のゲームではそれでもテーブルの方がメンテしやすいとか、そーいうつっこみは無しでお願いします。 元々あり得ない仮定の上の例なので。

ここまで来ると白々しさ大爆発ですが…

type
  TShirenModokiObject = class(TObject, IInterface, ...)
    ...
  public
    class function KindName: string; virtual;
    class function KindDisplayName: string;
    function InstanceName: string; virtual;
  end;

クラスメソッドを仮想関数にできないと、そのクラスメソッドを呼んでいるインスタンスメソッドを全て、各派生クラスにて全く同じ内容でオーバーライドしていく羽目になりかねません。

仕組み

ClassInfoの時点でネタばれしてるのですが、ObjectPascalでは、インスタンスメソッドのSelf(This)同様に、クラスメソッドにも暗黙のパラメータが渡って来ています。 インスタンスは存在しませんので、VMTへのポインタが来ています。

ですから、VMT経由で、多態したり、RTTIを使えるわけです。

Javaなんかでもリフレクションで同じことはできるかもしれませんが、ObjectPascalの場合、RTTIは使わず、通常の仮想メソッド呼び出しと同じコストで、クラスメソッドの多態を実現しています。 *1

クラスに関する処理

似たような例として、Windowsのウィンドウをラップしたクラスを作る時、RegisterClassExを行うメソッドを作るとしたら、それはクラスメソッドが自然ですし、そこから呼ばれる、RegisterClassExに渡すためのWNDCLASSEX構造体を生成するクラスメソッドは、サブクラスでカスタマイズできないといけませんから仮想関数になっている必要があります。

ほとんどのクラスライブラリでは、これをインスタンスメソッドで行ってしまっています…VCLも含めて。

クラスに属する処理で、インスタンスが不要なものは、クラスメソッドでやってしまうべきと思うのですが、いかがでしょうか?

メタクラス

VCLを用いると、プロジェクトソースに次の行が自動生成されます。

  Application.CreateForm(TForm1, Form1);

引数にTForm1とクラスそのものを渡しているのですが、これは(テンプレートでは無く)、クラスそのものへの参照です。

CreateFormの中では、こうして参照として渡されたクラスのインスタンスを作成して、もし初回ならメインウィンドウとして登録して…と色々やっているわけですが、ここでTFormのコンストラクタが仮想でなければどうなるでしょう?

実際にはTForm(のスーパークラスのTComponent)のコンストラクタは仮想なので、恐ろしい状態にならずにすむわけです。 コンストラクタもクラスメソッドの一種なので、ここでも、クラスメソッドを仮想関数にする機構が有効に働いています。

クラス参照型は、通常はメタクラスのように呼ばれているかもしれません。 でも、メタクラスの型は、大抵の言語では、一種類で、全てのクラスへの参照を格納できますが、ObjectPascalでは、ベースとなるクラスを指定して、そのつど型を作ります。

type
  TFormClass = class of TForm;

こうすることで、TFormのクラスメソッドが、TFormClassを介して、RTTIを使わず通常の仮想関数と同じコストで、多態して呼べるわけです。

従って、殆どの場合、Factoryのようなものをわざわざ作る必要はありません。

いくつか例を挙げさせていただきましたが、クラスメソッドを仮想にできない言語で同じことをしようとすると、(メタクラスの有無に関らず!)クラスを表すクラスを作ってそのインスタンスを用いないといけなくなります。 ひとつのクラスの情報のはずなのにふたつのクラスに分散してしまう上、 書くのも使うのも非常にまだるっこしいと思いませんか?

状態の無いインスタンス

応用…というよりは単にせこいだけの技になってしまうのですが、オブジェクト指向を徹底していると、インスタンスをひとつしか必要としない上、そのインスタンスには状態すらいらない場合があります。 Factory他、BridgeパターンとかState、Strategyパターン等が該当すると思われます。 要するに「多態」だけを使いたい場合ですね。

クラス参照とクラスメソッドでやってしまえば、メモリの節約になりますし、(ガベージコレクタの無いObjectPascalでは)インスタンスをいつ破棄するか考えなくてよくなります。

…そういうのは関数ポインタでやれ、という説もありますが、複数の関数の組が必要な場合は、クラスなら渡すのはメソッドをどれだけ沢山持っていても参照ひとつだけで済みますし、書くのも(IDEの補完が効くので)楽じゃないですか。

Javaと同等の機能しか使ってないのを見ると、まだるっこしいんですよね。 ObjectPascalのimplementsや仮想クラスメソッドをうまく使った方が、Java(1.5以前)程度の貧弱な記述力の範囲に収まっているよりも、メンテナンス性は遥かに上です。 ObjectPascalを使っているのですから、「ObjectPascalのオブジェクト指向機能」を全開にしませんか?
…と、ネタが尽きたのを自覚しているので、まとめの文も書いてみました。

他のネタ

「initialization/finalization」「message」「threadvar」なんかも便利なのですが…どんどんOOPと離れていきそうですので、保留。 Delphi for .NETで、OOPLとしては基本的な振りして微妙にいびつなDelphi Languageと、Delphi寄りとはいえ基本がJavaなC#との、折衷を行うための機能が大量に追加される筈ですので、面白いの見つけたらVol.3書きます。 今目を付けているのは「class helper」…DelphiでMixJuice!…できるかどうかわかりませんけど。
でもDの方が面白くなったらDelphi for .NET買わない可能性すら…


*1 Java詳しくないので間違ってたらごめんなさい。