[ クラス | フォーム | 配列、動的配列 | 文字列 | Delphi の格言 | IDE の小技 | エッセンス ]
クラス(クラス型)とは,フィールド,メソッド,およびプロパティから構成される構造を定義したものです。クラス型のインスタンスをオブジェクトといいます。クラスのフィールド,メソッド,およびプロパティは,クラスのコンポーネントまたはメンバーと呼ばれます。
オブジェクトは動的に割り当てられたメモリブロックで,その構造はクラス型により決まります。各オブジェクトはクラスで定義されている全フィールドのコピーをそれぞれ持っていますが,クラスのすべてのインスタンスで同じメソッドが共有されています。オブジェクトは,コンストラクタおよびデストラクタと呼ばれる特別なメソッドにより作成および破棄されます。
クラス型の変数は,実際にはオブジェクトを参照するポインタです。したがって,同じオブジェクトを複数の変数で参照することもできます。ほかのポインタと同じように,クラス型の変数には nil 値を格納できます。しかし,ポインタが指し示すオブジェクトにアクセスするのに,クラス型の変数を明示的に逆参照する必要はありません。たとえば,
SomeObject.Size := 100
という式で,SomeObject により参照されるオブジェクトの Size プロパティに値 100 が代入されます。これを
SomeObject^.Size := 100
と記述する必要はありません。(ヘルプ「クラスとオブジェクト」より)
詳しくは Object Pascal 言語リファレンス を見てください。[delphi-ml:1994]
なぜTCustomFrom.Create -> CreateNewで2つメモリがアロックされないか?
1.クラス参照からの呼び出し ( 例: TClass.Create )
2.コンストラクタから上位または自クラスのコンストラクタの呼び出し ( 例: inherited Create や Self.Create )
3.オブジェクト参照からの呼び出し( 例: Object.Create )
メモリを確保して初期化するのは、「1.クラス参照からの呼び出し」だけです。 TCustomForm.Createコンストラクタから自クラスの CreateNew コンストラクタを呼び出すということは、「2.コンストラクタから上位または自クラスのコンストラクタの呼び出し」に該当しますので、新たにメモリが確保されることはありません。[delphi-ml:35481]
VMT(仮想メソッドテーブル)には、特定メソッドのポインタや情報テーブルのポインタなどクラス固有の情報が格納されています。VMTはコンパイラによりクラス毎に作成されます。 インスタンスのサイズも、コンパイル時にVMTの負のオフセット部分(vmtInstanceSize)に格納されます。
コンストラクタを呼び出すときに、コンパイラはVMTへのポインタを暗黙のうちに渡していますので、このVMTからインスタンスのサイズを知ることが出来ます。 よって、インスタンスサイズはVMTが知っていますので、コンストラクタの静的か仮想かは関係ありません。
※ちなみにクラス参照型の変数には、VMTへのポインタが格納されています。[delphi-ml:35481]
上位のクラスに記述されたコンストラクタのコードが実行されないだけです。メモリの確保および初期化は、クラス参照から呼び出されたコンストラクタで行いますので、
上位クラスのコンストラクタを呼び出さなくても、メモリの確保および初期化については問題ありません。[delphi-ml:35481]
#上位クラスのコンストラクタを呼び出さないこと自体、致命的な結果をもたらすでしょうが。。。
上位クラスの Create を呼び出すことです。
Create を virtual にする意味は、コンストラクタに多態性を持たせることで、いろいろ便利な使い方が出来るからです。クラス参照型の変数を使わないと生きてきません。
例えばどのクラスをインスタンスとして生成するかを、パラメータとして受け渡すときにとっても便利です。VCL 内でもあちこちで使われています。classes.pas やgraphics.pas をご覧ください。[delphi-ml:18580]
上位オブジェクトの constructor ( virtual 宣言) + 派生クラスのconstructor ( override 指令)の組み合わせで派生クラス中でinherited を使う場合を(1)とします。
上位オブジェクトの constructor ( virtual 無し) + 派生クラスのconstructor ( override 無し)の組み合わせで派生クラス中でinherited を使う場合を(2)とします。
(1) と(2)の違いは何でしょうか?今の私には全て(1)で良いように思えます。そうそう「仮想コンストラクタ」というのも実行効率の問題なのでしょうか?「実行時解決」とか「コンパイル時未解決」とか何とか!?
違いは、仮想コンストラクタでは、クラス参照型の変数を介してコンストラクタを呼ぶ時、コンストラクタに多態性を持たせる事ができるという事です。具体例は、Delphi-ML 3563 で私が載せたソースコードを見てください。連想配列の配列要素を仮想コンストラクタで生成する例が載っています。
「コンパイル時に解決」というのは、特定のコードを呼び出すようにコンパイルする事です。
「コンパイル時未解決」「実行時解決」というのは、特定のコードではなく、オブジェクト内に埋めこまれたVMT(仮想テーブル)のポインタをたどり、VMT内のジャンプテーブルを使って間接ジャンプするようにコンパイルすることです(実際の処理はもう少し複雑です)。こうすると、オブジェクトに埋めこまれた VMT ポインタが違えば、実行時に呼び出されるメソッドのコードが変わるので、「virtual/dynamic」なメソッドが実現できます。VMT はクラス毎に用意され、そのクラスのオブジェクトにそのVMT へのポインタが埋めこまれます。[delphi-ml:3590]
オブジェクトの本来の型のオブジェクト参照で Destroy を呼び出しても、オブジェクト参照をその上位クラスのオブジェクト参照に変換してから destroy を呼び出しても、Destroy が virtual なら正しい Destroy が呼び出されてオブジェクトが正常に廃棄されるということです。例えば
var a: TMyClass; a := TMyClass.Create; a.Destroy;
としても
var a: TMyClass; b: TObject; b := TMyClass.Create; b.Destroy;
としても同じだという事です。
TList Items Property や StringList の Objects Property に オブジェクトを代入しておいて、後でオブジェクトを破棄する時、いちいち元の型は何だったか覚えておかなければならないとしたら大変ですが、正しく Destroy を実装していればそんなことは気にしないで単に Destroy を呼び出して破棄できます。[delphi-ml:3590]
※実際にオブジェクトを破棄する場合には、Destroy を呼ばすに Free メソッドを呼ぶようにしてください。Free メソッドは、以下のように実装されています。( Free は静的メソッドです。)
procedure TObject.Free; begin if Self <> nil then Destroy; end;
作成済みのオブジェクトに対してコンストラクタを呼び出すとメモリは確保されず、確保済みのメモリの中身がコンストラクタにより初期化されます。
こんなかんじです。
type TMyClass = class . . puclic constructor CreateA; constructor CreateB; end; var a: TMayClass; a := TMyClass.CreateA; {オブジェクトの作成} a.CreateB; {オブジェクトの再初期化}
コンストラクタ内から上位クラスのコンストラクタを呼び出すのも後者の形式です。[delphi-ml:3590]
※クラス参照ではなく、オブジェクト参照によりコンストラクタが呼び出された場合には、オブジェクトは生成されません。この場合に行われることは指定されたオブジェクトの操作であり、コンストラクタの実装部分で指定された文だけが実行され、そのオブジェクトへの参照が返されます。(ヘルプより)
TObject のコンストラクタとデストラクタなのですが、デストラクタの方に virtual が付いている意味がよくわかりません。System.pas にある TObject を見てみたのですが、TObject の Create と Destroy の中身がカラ(begin と end の中が何もない)でした。このことは、つまり何を意味しているのでしょうか?(TObject の Create と Destroy は何もしていないということなのでしょうか? )
1) クラス型の変数はポインタである。
Object Pascal ではオブジェクトは動的に確保されたメモリ上に作られます。Create でオブジェクトが作られるとき、メモリが確保されコンストラクタで初期化されてその先頭アドレスが返ります。クラス型の変数に代入されるのはオブジェクト参照と呼ばれるものですが、これはオブジェクトのために確保されたメモリの先頭アドレスです。
2) クラス型の変数は下位型のオブジェクトのオブジェクト参照を保持できる。
例えば、全てのコントロールのオブジェクト参照は TControl 型の変数に代入できます。しつこいようですがオブジェクト参照は「ポインタ」です。全てのフォームは TForm 型や TCustomForm型へ代入できます。全てのオブジェクト参照は TObject 型の変数に代入できます。このように、下位型のクラスのオブジェクトは上位型のクラス型の変数に代入できます。
言い方を変えると、オブジェクトはより上位のクラスのオブジェクトとみなして扱えるということになります。ややこしくいうと、オブジェクトをより上位の概念(クラス)で抽象的に扱えるということです。例えばコントロールの位置を調整するなら、TControl の Left, Top プロパティを変更できれば良いので、コントロールが具体的に TEdit なのかを知る必要は無く、TControl 型の変数だけでコントロールの位置の変更を処理できます。
別の見方をすると、クラス型の変数に代入されているオブジェクト(繰り返しますが、変数にはポインタしか代入されません)は変数の型のオブジェクトであるとは限らず、その下位型のクラスのオブジェクトが代入されている場合もあるわけです。
このように「変数の型と、代入されているオブジェクトの型が異なることがある」という点をよく頭に入れておいてください。
3) 多義性、多態性
オブジェクトのメソッドを <変数名>.<メソッド名>(パラメータ) という形式で呼び出す際、呼び出し方には大きく2種類あります。非仮想メソッドの呼び出しと仮想メソッドの呼び出しです(動的メソッドは方式は異なりますが意味は仮想メソッドと同じです)。
非仮想メソッドが呼び出されるときは、どこで定義されたメソッドを呼び出すかは<変数名>のクラス型によって決定されます。例えば TControl.Show メソッドは非仮想メソッドで、TEdit型は TControl から Showメソッドを継承しています。ですから TControl 型の変数や TEdit 型の変数を使って Show を呼ぶとShowメソッドは TControl で定義されたものが呼び出されます。
非仮想メソッドには困った問題があります。例えば TEdit から TEditEx というコンポーネントを継承で作って、TEditEx で Showメソッドを再定義したとしましょう。すると TEditEx型の変数を使って Showメソッドを呼び出すと、TEditEx の Show が呼び出されますが、TEditEx型のオブジェクトを TEdit型や TControl 型の変数に代入してから Show を呼び出すと、TControl のShow が呼ばれてしまいます。多分不具合が起きるでしょう。つまり、このようなことをするとコントロールを高い抽象度で扱えなくなってしまうわけです。
#「格言」に書いたように、非仮想メソッドを上記のように再定義することは避けなければなりません。
仮想メソッドが呼び出されるときは、どこで定義されたメソッドを呼び出すかは変数の型ではなく、* オブジェクトの型 によって決まります。例えば、TControlで定義されている SetBounds メソッドはコントロールの大きさと位置を変更するメソッドですが、コントロールによってはその処理内容が異なります。個々のオブジェクトの型の適した処理がなされないと困るので、SetBounds がオブジェクトが代入された変数の型によってどれが呼び出されるか決まっては困ります。つまり SetBounds は仮想メソッドでないと困るわけです。
このように、オブジェクトのメソッドの呼び出しが変数の型ではなくオブジェクトの型によって決まることをレイトバインディングなどといったりますが、より抽象度の高い、上位のクラス型の変数から、下位クラスのオブジェクトの下位のクラスで再定義された仮想メソッドを呼び出してオブジェクトを操作すること、できることを、多義性があるとか、オブジェクトを多態的に使うとかいいます。
#学問の常で、単に上で説明した動作にレッテルを貼ったに過ぎません。
4) デストラクタを仮想メソッドにする理由。
3) から判るように、オブジェクトが上位の型の変数で保持されていた場合でも、オブジェクトに適したデストラクタがオブジェクトの型に応じて選択され実行されるようにするためです。
一般に、デストラクタは各オブジェクトの型特有のものですからオブジェクトの内容に応じた処理が必要です。つまり再定義が必須です。非仮想メソッドにすると、3) で示した非仮想メソッドの再定義の問題点が表に出てきてとんでもない目にあいます。
めちゃめちゃになるので、必ず override で再定義してください。
5) デストラクタで inherited を呼ぶ理由。
オブジェクトの構築や破棄は、上位クラスのコンストラクタ、デストラクタと、それから継承するクラスのコンストラクタとデストラクタの共同作業です。inherited は上位クラスから継承したメソッドの呼び出しです。
つまり、上位クラスのコンストラクタ、デストラクタは、上位クラスで定義したフィールドなどのセットアップや終了処理を担当し、下位クラスのコンストラクタや、デストラクタは下位クラスで追加したフィールド等のセットアップや終了処理を行います。もちろん、下位クラスが上位クラスのフィールドやプロパティ等を書き換えることもありますが、下位クラスが全部を行えるわけも無く、役割分担が必要です。そのために inherited で上位のものを呼び出し、残りの処理を下位クラスで行うわけです。
下位クラスのコンストラクタ、デストラクタが、上位のものを呼び出さないことはよっぽどのことが無い限りないので、上位クラスのコンストラクタやデストラクタが空でも、下位クラスのコンストラクタ、デストラクタに、習慣というか脊髄反射的に inherited を書いてしまうのが普通です(^^; これは後で、上位クラスのコンストラクタや、デストラクタの処理が空でなくなった場合でも、呼び出し忘れを防ぐための習慣でもあります。[delphi-ml:68548]
オリジナルの基本クラス(TMyBase)を定義し、それから派生したクラスが複数(TMyClassA、TMyClassB、TMyClassC…)あったとします。
それぞれ簡単に複製できるように、TMyBaseにvirtualでAssignメソッドを定義し、派生クラスでそれをOverrideしています。
そこで、この派生したクラス(TMyClassA、TMyClassB、TMyClassC…)それぞれをランダムにTListに保存してあるのですが、それらをすべて複製しようとしてつまづいてしまいました。
下にそのダメな例を書きます。
procedure ClassListCopy(lstSource, lstDest: TList); var i: Integer; NewClass: TMyBase; begin for i := 0 to lstSource.Count - 1 do begin NewClass := TMyBase.Create; // ここがマズイ!! NewClass.Assign(TMyBase(lstSourceCount[i])); lstDest.Add(NewClass); end; end;
それぞれをis演算子で比較しなければいけないのでしょうか?
こういう場合は仮想コンストラクタを使います。TMyBase のコンストラクタを virtual を付けて宣言してください。[delphi-ml:40584]
type TMyBaseClass = class of TMyBase; var ClsType: TMyBaseClass; NewInstance: TMyBase; : : // オブジェクトのクラス参照を取り出す。 ClsType := TMyBaseClass(TMyBase(lstSourceCount[i]).ClassType); // クラス参照の指すクラスのコンストラクタを呼び出す。 NewInstance := ClsType.Create; NewInstance.Assign(TMyBase(lstSourceCount[i])); : : 以下に修正したソースを示します。 TMyBaseClass = class of TMyBase; TMyBase = class : : : constructor Create; virtural; function Assign(Source: TMyBase); virtural; end; : : : procedure ClassListCopy(lstSource, lstDest: TList); var i: Integer; NewClass: TMyBase; begin for i := 0 to lstSource.Count - 1 do begin // NewClass := TMyBase.Create; // ここがマズイ!! NewClass := TMyBaseClass(TMyBase(lstSourceCount[i]).ClassType).Create; NewClass.Assign(TMyBase(lstSourceCount[i])); lstDest.Add(NewClass); end; end;
デストラクタを呼び出すと、
このため、デストラクタを複数回呼び出すと、既に破棄されている「インスタンスに割り当てられたメモリ」にアクセスすることになり、アクセスバイオレーションなどの不具合が発生します。
このことは、デストラクタに実装したコードとは全く関係ありません。デストラクタにコードを実装していなくても同じ現象になります。 このことから、デストラクタに実装するコードは、インスタンスが不完全な状態で呼び出されることを考慮する必要はあるが、複数回呼び出されることを考慮する必要はないということになります。
よって、
ということになります。[delphi-ml:37518]
クラス参照.クラスメソッド;
でクラスメソッドを呼び出した場合には、Self は クラス参照となりますが、
オブジェクト参照.クラスメソッド;
の形式での呼び出しでは、Self は呼び出したオブジェクトのインスタンスに対してのクラス参照になります。
この結果、インスタンス経由のクラスメソッド呼び出しは実体が存在していなければなりません。[delphi-ml:56526]
type TA = class class procedure WhoAmI; end; TB = class(TA) end; class procedure TA.WhoAmI; begin WriteLn( Self.ClassName ); end; var a: TA; begin a := TA.Create; a.WhoAmI; a.Free; a := TB.Create; a.WhoAmI; a.Free; TA.WhoAmI; TB.WhoAmI; end.
クラス参照の実体はポインタで、仮想メソッドテーブル(VMT)というクラス毎に静的に確保されたエリア指しています。ここには、
が入っています。かなり大きな情報です。
クラスのインスタンスの先頭にはクラス参照が入っています。Object Pascal のインスタンスはこの情報を元に、作成され、活動し、破棄されます。
Object Pascal ではこの情報へのポインタ(クラス参照)をデータとして変数に格納したり、パラメータとして他に渡せるので、これをうまく活用すると、プログラムに大きな柔軟性を与えることができます。[delphi-ml:61450]
クラスメソッドはインスタンスを作成しなくても、クラス参照だけで実行できます。
Label1.Caption := TButton.ClassName;
というふうに。コンストラクタも同様にしてクラス参照だけで実行できないとインスタンスを作成できませんね。これはコンストラクタはクラスメソッドである(class キーワードがありませんが)、と理解してよろしいですか。それとも、ヘルプにあるように「特殊なメソッド」であると。
コンストラクタはクラスメソッドでもあり、普通のメソッドでも有ります。クラス参照で呼び出せばクラスメソッドですが、オブジェクト参照から呼び出せば普通のメソッドです。
特殊なのは、クラスメソッドとしての場合、Self がクラス参照ではなくてオブジェクト参照になることでしょう。コンストラクタがインスタンスを作るからです。
--
コンストラクタは、クラス参照を通してメンバ変数にアクセスできるメソッド という形で普通のクラスメソッドにはない特殊なメソッドといえます。ヘルプを読めば、コンストラクタの実装についても書かれています。
イメージ的には
constructor TFooBar.Create(...); ↓ class function TFooBar.Create(Flag: Boolean; ...): TFooBar; begin if Flag then Result := TFooBar( Self.NewInstance ); Result.Create(...); end; procedure Create(...); beggin ; { constructor TFooBar.Create の中身 } end;
のような2つの機能に分離されていることがわかります。[delphi-ml:61463]
# ヘルプには実装での EAX レジスタや DL レジスタの役割に # ついても言及されています。
Inherited は Virtual とは無関係に使えます。また virtual だからといって Inherited を使用しなくてはならないわけでは有りません。Inherited は単にクラスに上位クラスから継承されたメソッドと同名のメソッドが有る場合、継承されたメソッドを指定するためのものです。
Object Pascal は C++ のように、コンストラクタやデストラクタが上位クラスのコンストラクタやデストラクタを自動的に呼び出してはくれないので明示的に記述する必要が有ります。上位クラスが初期化を必要としない場合をのぞいて(滅多に無い)コンストラクタは上位クラスのメンバを初期化する「義務」があります。従って、上位クラスのメンバを初期化する何らかのルーチンを呼び出さなければなりません。必ずしも上位のコンストラクタを呼ぶ必要は有りませんが、通常は呼び出して初期化します。
上位のコンストラクタを呼び出す時、上位のコンストラクタの名前がコンストラクタと同名の場合は Inherited を付けて呼び出さなければなりません。[delphi-ml:3560]
通常オブジェクトを廃棄するには Free メソッドを呼び出し、nil値のチェック後 Destroy デストラクタが呼び出されます。しかし、破棄されたオブジェクトにはnil値が代入されません。
そこで、デストラクタの最後で Self := nil を実行するようにしたいと思いますが、この方法で何らかの問題が発生するというようなことはありますでしょうか?
これは出来ません。Self はオブジェクト内でメソッド等が参照する オブジェクト自身のアドレスが入っているもので、値を変更すると 後でアクセスバイオレーションが起こります。
クラス型の変数が保持しているのはオブジェクトのオブジェクト参照(ポインタ)で、オブジェクトはどの変数が自分を指しているか知りません。複数の変数がひとつのオブジェクトを指すことだって可能です。ですから「オブジェクトの側から」変数をクリアするのは不可能なんです。
蛇足:Delphi 5 なら FreeAndNil を使うと便利ですよ。[delphi-ml:44194]
オブジェクト参照はオブジェクトを指しているポインタの一種に過ぎません。オブジェクトが破棄されてもオブジェクト参照を保持している変数やリスト内の値が自動的に変化することはありえません。
Object Pascal ではオブジェクト参照を保持する変数をオブジェクト本体で有るかのように記述が出来るのでそれはそれで便利ですが、参照と実体の区別を把握しておくことが大切です。[delphi-ml:53212]
オブジェクト破棄される際、そのことを自動的に他に知らせる機構はデフォルトでは有りません。オブジェクトをオブジェクトの外から明示的にFree で破棄する場合は。それのオブジェクト参照を保持していた変数の値を Nil にしたり、リストから削除したりすることでオブジェクトが破棄されたことを残しておくことが出来ますが、オブジェクトが自殺するケースでは、オブジェクトから破棄を通知するような仕掛けをオブジェクト側に設ける必要が有ります。[delphi-ml:53212]
手続き(メソッド)型でない変数の場合: その変数を指すポインタを返します。ポインタの型は {$T-} の状態では Pointer 型に、{$T+} の状態では ^[変数の型] になります。 手続き型(メソッド型)の変数の場合: 変数が手続きの呼び出しと解釈されることを抑止し、手続き(メソッド)への ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ポインタが入っている変数として扱うことを強制します。従って変数の ポインタを 取得したい場合は @@A という具合に @ を2個書きます。 手続き名、関数名、メソッド名の場合: 手続き、関数、メソッドもエントリポイントを指す ポインタ(メソッドポインタ)が返ります。
詳しくは @ 演算子のヘルプを見てください。[delphi-ml:32700]
[ クラス | フォーム | 配列、動的配列 | 文字列 | Delphi の格言 | IDE の小技 | エッセンス ]
更新日:2005-02-12