#Delphi Starter Edition チュートリアルシリーズ シーズン2 第7回 「オブジェクト指向」その2
Delphi Starter Edition チュートリアルシリーズ シーズン2 第7回 「オブジェクト指向」その1 の続きです。
その1ではオブジェクト指向の全体の説明と隠蔽&公開の話を説明してきました。 その2ではいよいよオブジェクト指向の「継承」の話に入ります。
その1でも記述しましたが、記述しているコード例のいくつかはGithubに上げているサンプルコードで確認することができます。
https://github.com/kazaiso/Starter_Tutorial_season2_20170306
では続きです…
###クラスの継承
- 既にあるクラスのフィールドやメソッドを受け継いで新たなクラス定義が可能
- 既存のクラスのフィールドやメソッドを受け継ぎつつ新たなフィールドやメソッドを追加したり、上書きすることが可能
- いずれ実装される子クラスに、メソッドの実装をゆだねておく、といった定義も可能
クラスを継承してあらたなクラスを定義する場合の構文は以下のようになります
type
子となるクラス定義識別子 = class(継承元となるクラス定義識別子)
public
//フィールド、メソッド…
継承してあらたなクラスを定義するコード例は下記のようになります。
type //継承クラスの定義例
TDate = class //親となるTDateクラス(ベースクラス)
private
FDate: TDateTime; //日付を保持する TDateTime型 FDateフィールド
public
//省略
function GetText: string; //日付情報を文字化して渡すメソッド
//省略
end;
type
TNewDate = class(TDate) //TDateから派生(TDateを継承)する 子クラス(サブクラス)
public
function GetText: string; //親の持つ GetTextメソッドをTNewDateクラス版に書き換えるための宣言
end;
このコードのポイント
- 最初の
TDate
クラスは親となるクラスの定義です。次のTNewDate
クラスがTDate
クラスを継承して作っている新たな子のクラスです。(前週のチュートリアルや、その1で使っているTDate
クラスです) - 親のクラスで
GetText
メソッドを持っています。子のクラスで同名のGetText
メソッドを定義して、その処理を変更する定義を行っています。
###継承先での新しいメソッドへの書き換え実装例
上のセクションで TDate
クラスを継承してTNewDate
クラスを定義しました。そしてTDate
クラスが持つ、GetText
メソッドを上書きする同名のGetText
メソッドをTNewDate
クラスにも定義してありました。
ここでは継承先で同名のメソッド新しいメソッドへと書き換えるコード例を見てみます。
implementation
{ TDate }
function TDate.GetText: string; //参考に掲載。親のGetTextメソッド実装部
begin
Result := DateToStr(FDate); //親の元実装 : ’2017/03/06’の文字列が返る
end;
{ TNewDate }
function TNewDate.GetText: string; //TNewDateクラスで親と同名のGetTextメソッドを新たに記述
begin
Result := FormatDateTime('ggE年m月d日 (dddd)', FDate); //子の実装 ’平成29年3月6日 (月曜日)’の文字列が返る
end;
このコードのポイント
- 最初に実装してある
TDate.GetText
は親クラスが持っているもともとのGetText
メソッドです。 -
TNewDate.GetText
が今回新たに実装した継承クラス(子クラス)のGetText
です。 -
TNewDate.GetText
で特に小難しいテクニックを使わず、普通に子クラス定義識別子.メソッド識別子
として実装します。 -
TNewDate
クラスのフィールドとしてFDate
は定義していませんでしたが、問題なく使えます。これは親のTDate
がFDate: TDatetime;
のフィールドを持っているからで、「継承」を使った場合の特徴の一つとして、親クラスがもっているフィールドは子クラスにおいても同様に持っているものとして、新たに定義せずとも引き継いで使うことができるのです。※ちなみに子クラスのTNewDate
でGetText
を新たに定義しなければ、親のTDade
クラスて定義・実装してあるGetText
をそのまま使うことができます。
###親クラスと子クラスのメソッド動作の違い
上のセクションでTDate
クラスを継承して定義したTNewDate
クラスの新メソッドGetText
の実装を行いました。ここではその実装したGetText
について、親のクラスと子のクラスでそれぞれ使ってみてどのように動作するか確認してみましょう。
procedure TForm1.Button1Click(Sender: TObject);
var
myDay: TDate; //親クラスのオブジェクト識別子
myNewDay: TNewDate; //子クラスのオブジェクト識別子
begin
myDay := TDate.Create; //親オブジェクトのCreate
myNewDay := TNewDate.Create; //子オブジェクトのCreate
try
show('親クラス: '+ myday.GetText); //親オブジェクトのGetTextメソッドで文字列を取得して表示
show('子クラス: '+ myNewDay.GetText); //子オブジェクトのGetTextメソッドで文字列を取得して表示
finally
myDay.Free; //親オブジェクトの解放
myNewDay.Free; //子オブジェクトの解放
end;
このコードでのポイント
-
継承先で新たな同名メソッドを記述して動作を変更することが可能
-
新クラス(
TNewDate
)のオブジェクト識別子を使用した場合には、新クラス(TNewDate
)が持つメソッドの処理が行われる -
親クラス(
TDate
)のオブジェクト識別子を使用した場合には、親クラス(TDate
)が持つメソッドの処理が行われる
-
新クラス(
-
このコードを走らせた結果表示されるのが下記となります
-
TDate
型ののオブジェクト識別子にTDate
のオブジェクト参照が入っていて、そのGetText
を実行すると、TDate
クラスのGetText
メソッドが返す文字列であるyyyy/mm/dd の文字列が返ります。 -
TNewDate
型のオブジェクト識別子にTNewDate
のオブジェクト参照が入っていて、そのGetTextを実行すると、TNewDate
クラスのGetText
メソッドが返すフォーマットされた文字列が返ります。
-
このように親のクラスを継承した子のクラスで、親クラスの持つフィールド(やクラス)を引き継いで使いながら、また、親クラスの持っていたメソッドを上書き・更新して使うことができます。
###classを作ると、ある一つのクラスを継承している
- Delphi (Object Pascal) の
class
はデフォルトでTObject
クラスを自動的に継承-
type TDate = class
の定義はtype TDate = class(TObject)
定義と同じ意味となる - すべてのクラスは基本的に
TObject
のサブクラス(子孫クラス)
-
* `Create`, `Destroy`, `Free` などは `TObject` で定義・実装されているメソッド * コンストラクタの実装例で使用した `inherited Create;` は、`TObject` の `Create` を実行 * デストラクタの実装例で使用した `inherited;` は `TObject` の 同名の `Destroy` を実行
type Tクラス定義識別子 = class
と書くと必ず TObjectを親として継承します。なのでコード上、わかりやすくtype Tクラス定義識別子 = class(TObject)
と書いても問題ありません。TObject
を継承しない方法もありますが、チュートリアルシリーズでは割愛します
TObjectが持っているメソッドにいては下記のDocWikiを参照してください。
参考DocWiki: http://docwiki.embarcadero.com/Libraries/Berlin/ja/System.TObject
###実は既に継承を使っています
ここで、ちょっと寄り道。オブジェクト指向と、そのクラス継承がどんなに便利なものかを再確認していただくために…
クラスの継承の定義、実装、使い方の例を見てきましたが、Delphiを使っているフレンズはすでに継承を使っています。
- 自動的に作成されたフォーム(TForm1)もクラスTFormを継承
- 「マルチデバイスアプリケーションの新規作成」を行ったときに、自動で作られているフォーム(ウインドウ)はすでに用意されているTFormを継承してTForm1として子クラスを自動で作っているものです。
- 以下、自動で作られている定義を引用します(下記コード例ではボタンコンポーネントを一つ追加して、オンクリックイベントハンドラを追加してある状態)
type
TForm1 = class(TForm)
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
{ private 宣言 }
public
{ public 宣言 }
end;
はい。継承を学んだ今、見てみると、継承してクラス定義をする記述がなされているのがわかりますよね。そして追加しているボタンなどはTButton(これもクラス)をフィールドとして追加していることが読めます。こうして親クラスを継承して子クラスを作り、カスタマイズして使っているわけです。Delphiのビジュアル開発もオブジェクト指向によるものであります。
###親・祖先のクラス型のオブジェクト識別子の下位互換性
継承を学んだところでさらに深みに入ってまいります。便利な使い方を説明する前に、先に知っておいていただきたいオブジェクト識別子の使い方があります。
- 親・祖先のオブジェクト識別子に、子のクラス型のオブジェクト参照を代入可能
上記ができると…
- ルーチン・メソッドのパラメーターとして、親のクラス型を定義して起き、実装時に引数としてはその親のオブジェクト識別子に子のオブジェクト参照をいれて引数としてルーチン・メソッドに渡せる
そして
- 親クラス型のオブジェクト識別子に入っている子クラスのオブジェクトのメソッドなどを使用する場合には、
as
キーワード(as演算子)を を使用して子クラス型のオブジェクトとして扱う
コード例を見てみましょう
var
myObject: TObject; //親クラス中の親、TObject型のオブジェクト識別子を宣言
str : string;
begin
myObject := TDate.Create; //TDateの参照は親クラスであるObject型のオブジェクト識別子に代入、保持できる
try
str := (myObject as TDate).Gettext; //myObjectをTDate型のオブジェクトとして、TdateのメソッドGetTextを使用
// str := myObject.Gettext; //TObjectクラスはメソッドGetTextを持っていないので、これはコンパイル通らず
finally
myObject.Free;
end;
end;
このコードの例のポイント
- すべてのクラスの親元 TObject型の オブジェクト識別子を
myObject
として宣言しています。 - TObject型オブジェクト識別子
myObject
に子クラスのTData
クラスのオブジェクト参照を代入しています。(すべてのクラスはTObject
から継承されています) - TObject型オブジェクト識別子
myObject
にas
キーワードを使ってTData
として取り扱うものとして、myObject
に入っているTData型のオブジェクトのGetText
メソッドを使用しています。 -
as
を使って TDataクラス型として使うことを明示しないと、宣言されているTObject
クラス型の識別子ではTData
クラスが持っているGetText
を持っていないため、コンパイルがとおりません
###実は既に親オブジェクト識別子への子クラス参照の代入は使っています
親のクラス型のオブジェクト識別子には、子のクラスのオブジェクト参照をいれられることを学んだところで、また便利な話をしておきます。
※ちなみに子のクラス型のオブジェクト識別子には親のクラスのオブジェクト参照は入れられません。私はこのルールを個人的にカンガルー方式と呼んでいます。親のカンガルーの袋に子供は入るけど、逆はムリ。と。
便利な話に戻ります。
- イベントハンドラのパラメータの
TObject
クラスのオブジェクト識別子に、子クラスのオブジェクト参照が代入されている - オブジェクト識別子に入っているクラスの型を判定する
is
キーワード(is演算子) が便利につかえる - オブジェクト識別子を実際のクラスに合わせて使うには前ページの
as
を使うか、キャストする
(例外として、この後に説明するvirtual
–override
キーワードを使う方法はこの限りではない)
例としてボタンコンポーネントのOnClick
イベントのイベントハンドラを見てみましょう。
//Button1 の OnClickイベント
procedure TForm1.Button1Click(Sender: TObject);
begin
if Sender is TButton then //Senderオブジェクト識別子に入っているオブジェクトが TButtonか判定
show(TButton(Sender).Text);//SenderをTButton型にキャストし、TButton型のTextプロパティを使用
// show(Sender.Text); //TObjectはTextプロパティを持っていないので、これはコンパイル通らず
end;
このコードのポイント
- イベントハンドラで
Sender : TObject
をパラメータとして持っているイベントハンドラにおいて、Sender : TObject
には、そのイベントを発生させている元となるコンポーネントのオブジェクト参照が入っています。この例ではボタンのオンクリックイベントなので、Sender
オブジェクト識別子に、クリックされたTButton
コンポーネントのオブジェクト参照が代入されています。 -
is
キーワードを使うことで、TObject型オブジェクト識別子のSender
に入っているオブジェクト参照がどんな型なのか確認することができます。ここではif Sender is TButton then ~
としてSender
に入っているのが TButton型のオブジェクト参照なのか確認しています。 - そして
Sender
を使うときにはTButton型にキャストしています。 前のセクションで使っているas
でもOKです。(キャストは第2回の「変数と型」で紹介している方法で、どの型に変化させるか型名を書き、その後ろに変化させたい変数などをカッコでくくて書く方法です)
###親クラス型のオブジェクト識別子に子クラスの参照を入れる際に使えるキーワードとテクニック: virtual, abstract, override
いよいよDelphi Starter Edition チュートリアルシリーズ シーズン2 の言語を覚えるシリーズも佳境です。(残る2回は「作ってみよう」の回となり、実際に一つのプログラムを作って試してみる回となります。)
このセクションでは親のオブジェクト識別子に子のオブジェクト参照を入れて使う際にキャストや as
を使わずとも、動的に、代入されているオブジェクト参照が持っているメソッドを使用できるようになるテクニックをご紹介します。
実際にオブジェクト識別子に入っている参照のクラス型で実行されるメソッドが決定される方法を成し遂げるキーワードたちが virtual
, abstract
, override
三人衆です。
オブジェクト識別子の中に実際に入っているオブジェクト参照に基づいて、オブジェクト参照が持っているメソッドの処理が実行されることを「遅延バインディング」と言ったりもします。ですが、この遅延バインディングといった呼び名はさほど重要ではないので覚えなくてもOKですが、プログラムの話をする時に現れる用語であったりします。その時に思い出す程度に覚えておいてあげても良いでしょう。(上から目線)
では、この virtual
, abstract
, override
について、まずは説明します。
\ 遅延バインディング三銃士をつれてきたよ! /
- virtual : 親クラスのメソッド定義時に後方に付加しておくキーワード
- 継承する子のクラスにて、当該メソッドが上書き(
override
)されることがあることを宣言しておくもの - 実行時にオブジェクト識別子に入っているクラス型によって実行するメソッドが変わる可能性があることを宣言している
- 親のクラスで宣言しておくと、親クラス型のオブジェクト識別子に子クラス型のオブジェクトの参照が入っている時、子クラスのメソッドをキャストせず実行可能
-
virtual
キーワードが付いていないと、オブジェクト識別子の宣言に使われたクラスのメソッドが実行される
* **abstract**: 親クラスのメソッド定義時に後方に付加しておくキーワード * メソッド定義時の親クラス内ではメソッドの実装をせず、子のクラスにおいて`override`で実装することを宣言しておくもの * `virtual`とセットで使う(`override`されることを前提としているキーワードなので) * このクラスを継承する子クラスにおいて`override`キーワードで処理を実装して使用する * 親オブジェクト識別子に子オブジェクト参照を入れて使うとき、親オブジェクト識別子で子クラスのメソッドを使うために宣言のみしておく
* **override**: 子のクラスのメソッド定義時に後方に付加するキーワード * 親クラスの`virtual`キーワードのついているメソッドを上書きすることを宣言するもの * 親クラスの`virtual`キーワードがついているメソッドの上書き時にのみ使用可能 * 親オブジェクト識別子に、子オブジェクト参照を代入しているケースで、実行時に実際に代入されている子クラス型のメソッドが実行されるようになる
###親、祖先のオブジェクト識別子に子オブジェクト参照を代入可能・その使用テクニック
それでは実際にどのようにしてキャストせずとも、親のオブジェクト識別子に、実際に代入されているオブジェクト参照に基づいたメソッドを実行させることができるのか見てみましょう。
####下記のような親・子クラスの定義のモデルを例に説明
####typeブロックのコード例
上記のモデルの定義部 type
ブロックのコード例は下記のとおりです
type
// 親クラスとなるTKemonoクラス 定義
TKemono = class(TObject) // TServel, TFennecの親クラス
public
function Voice: string; virtual; abstract;
// Voice 文字列を返すメソッド。実装は子に任せるためvirtual; abstract; キーワードをつけている
end;
TServal = class(TKemono) // TKemonoを継承
public
function Voice: string; override;
// 親クラス内のVoice 文字列を返すメソッド abstractのVoiceメソッド overrideするキーワード付き
end;
TFennec = class(TKemono) // TKemonoを継承
public
function Voice: string; override;
// 親クラス内のVoice 文字列を返すメソッド abstractのVoiceメソッド overrideするキーワード付き
end;
このコードのポイント
- モデル図どおりの親クラスとそれを継承した子クラスの定義です。
- 親クラスのメソッド
Voice
には、子のクラスにおいて上書きされ、動的に実行内容が判断される可能性があることを示すvirtual
キーワードが付加されています。そして、このクラスでは実装を行わず、実際に実装されるのは子のクラスにて行われることを示すabstract
キーワードが付いています。 - 子クラスの
TServal
,TFennec
クラスのいずれも親クラスと同名のVoice
メソッドを持っています。親クラスでvirtual
キーワードが付加されて宣言されているVoice
メソッドを 上書きして、動的に実行されるようにoverride
キーワードを付加しています
####voiceメソッドの実装
voiceメソッドの実装コードは下記のとおりです。
implementation
{ TServal }
function TServal.Voice: string; //TServalのVoiceメソッド実装。実装部にはoverrideキーワードは不必要
begin
Result := 'meow'; //Voiceとして‘meow’文字列を返す
end;
{ TFennec }
function TFennec.Voice: string; //TFenncのVoiceメソッド実装。実装部にはoverrideキーワードは不必要
begin
Result := 'yelp'; //Voiceとして‘yelp’文字列を返す
end;
このコードのポイント
- 親クラスの
TKemono
クラスのVoice
メソッドは 定義部でabstract
キーワードを着けて当クラス(親クラス)内では実装しないよう定義していました。ですので、TKemono
クラスのVoice
メソッド実装の記述はありません。 -
TServal
とTFennec
クラスのVoice
メソッドがそれぞれ記述されています。実装部implementation
においてはoverride
キーワードを改めて記述する必要はありません。 -
TServal
クラスのVoice
メソッドは 'meow'の文字列を、TFennec
のVoice
メソッドは'yelp'文字列を返すように実装されています。
####使用コード例
実際に使用してみるコード例がこちらです。
procedure TForm1.Button2Click(Sender: TObject);
var
myAnimal: TKemono; // 親クラスのオブジェクト識別子を宣言
begin
if rbServal.IsChecked then // 選ばれているチェックボックスによって TServalかTFennecクラスのどちらかをCreate
myAnimal := TServal.Create // TServalのオブジェクト参照を親クラス型のオブジェクト識別子に代入
else
myAnimal := TFennec.Create; // TFennecのオブジェクト参照を親クラス型のオブジェクト識別子に代入
try
show(myAnimal.Voice); // 親の型のオブジェクト識別子に子のクラスのオブジェクト参照が代入されている
finally
myAnimal.Free; // 解放
end;
end;
そしてこのコードを使ったサンプルコードを実行した結果のイメージショットがこちら
このコードとサンプルのポイント
- 「なきごえ」ボタンが押されたとき、サーバル、フェネック、どちらのラジオボタンが選択されているかで、
TServal
,TFennec
どちらかのオブジェクトをCreate
しています。 -
Create
したオブジェクトはTKemono
(親クラス)型のオブジェクト識別子myAnimal
に代入しています - 親クラスのオブジェクト識別子 + Voiceメソッド(
myAnimal.Voice
)で、キャストすることなく、代入されているオブジェクト参照のVoice
メソッドが実行されます。 - このキャストすることなく、実際に代入されているオブジェクト参照に基づいたメソッドが実行されるようになるのは、以下の条件が揃えられているからでもあります。
- 親のクラスが
Voice
メソッドを持っているため、親のオブジェクト識別子.Voice
とコードを書いてもコンパイルできる - 親の
Voice
メソッドがvirtual
として、動的に実行メソッドが決められる方法が取られることを定義している - 子の
Voice
メソッドがoverride
として、動的に実行メソッドが決められる方法が取られることを定義している
- 親のクラスが
###さらに使えるメタクラス (ご参考)
さらに、上のセクションで記述しているコードについて、クラスのCreate
をif
文で分岐させず、Create
をする部分をひとつにしてコードをクリアに記述したいような場合、どのクラスを使うのか、ルーチンに伝えて、別ルーチンでCreate
させる、といったこともできます。
- クラス型名を扱えるよう型を定義する : class of (実クラスのオブジェクト参照ではなく、クラスの型名そのものを渡せる)を使います
- 親のクラスの型名を型として宣言しておくと、メソッドのパラメータとしてクラスの型名(子のクラスの型名)を渡せる
- 渡された先のメソッドでクラスの型名からオブジェクトのインスタンスを作れる(Createできる)
まずはtype
ブロック
type
TFriends = class of TKemono; // TKemonoクラスの型名を表す [TFriends]を定義
すでに TKemonoクラスは上のセクションで定義してあるものとして、ここでは新たに TKemonoクラスのクラス名を扱える型としてTFriendsクラス名型識別子を定義しています。なんだかわかりにくい表現ですが、TKemono
, TServal
, TFennec
といったクラスを表す名を、このTFriends型は取り扱えるってことです。
次に、このTFriends をパラメータとして使って、新たなルーチンを実装したコード例です。
function KemonoVoice(KemonoClassName: TFriends): String;
var
myAnimal: TKemono; //親クラスとなるTkemonoのオブジェクト識別子を宣言
begin
myAnimal := KemonoClassName.Create; //パラメータで渡されたクラスの型(TServalかTFennec)をCreate
try
Result := myAnimal.Voice; //パラメータで渡されたクラス型でCreateしたオブジェクトのクラスメソッドを使用
finally
myAnimal.Free;
end;
end;
- パラメータとして使用されている
TFirends
型の変数KemonoClassName
には、TKemono
,TServal
,TFennec
といったTKemono
クラスに属するクラス名を代入することができます。 - そしてコード内で行っている通り、そのクラス名が入っている
KemonoClassName
を使って、KemonoClassName
に入っているクラス型のオブジェクトをCreate
することができます。 -
Create
したオブジェクト参照は、TKemono型のオブジェクト識別子に入れておきます。 - 先のセクションで行ったように、実際に入っているオブジェクト参照の型によって、実行されるメソッドが動的に決まり、1合致したクラスの
Voice
メソッドが実行されます。
ちなみに、この新たに作られたKemonoVoice
ルーチンを呼び出しているコードがこちら。
- ポイント : 引数として、TServal や TFennec といったクラス名を渡しています。
procedure TForm1.Button3Click(Sender: TObject);
begin
if rbServal.IsChecked then
show(KemonoVoice(TServal)) //クラスの型名 TServal を引数としてルーチンを実行
else
show(KemonoVoice(TFennec)); //クラスの型名 TFennec を引数としてルーチンを実行
end;
より正確にいえば、「クラス名」を渡しているのではなく、クラスの型そのもののありかである参照(ポインタ・アドレス)を渡しています。
クラスの型そのものの参照を渡されたその先のルーチンで、クラスの型そのものの参照によって、クラスの型のありかにアクセスできるようになるので、そのクラスを使ってCreateしてオブジェクトとして確保することができる、といったことをやっています。
参考docWiki : http://docwiki.embarcadero.com/RADStudio/Seattle/ja/クラス参照
##おわりに
第7回、3月7日分の Delphiパート「オブジェクト指向」の、その1、その2 は以上です。
これにてシーズン2の 言語を学ぶ回は ひと段落となり、次回は学んだ言語を使って「作ってみよう」Delphiの部 になります。お楽しみに。