【DELPHI STARTER チュートリアルシリーズ】 シーズン2 第7回 ‟オブジェクト指向„ (その2)[JAPAN]

Posted by on in Blogs

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は定義していませんでしたが、問題なく使えます。これは親のTDateFDate: TDatetime; のフィールドを持っているからで、「継承」を使った場合の特徴の一つとして、親クラスがもっているフィールドは子クラスにおいても同様に持っているものとして、新たに定義せずとも引き継いで使うことができるのです。※ちなみに子クラスのTNewDateGetTextを新たに定義しなければ、親の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メソッドが返すフォーマットされた文字列が返ります。

image

このように親のクラスを継承した子のクラスで、親クラスの持つフィールド(やクラス)を引き継いで使いながら、また、親クラスの持っていたメソッドを上書き・更新して使うことができます。

classを作ると、ある一つのクラスを継承している

  • Delphi (Object Pascal) のclassはデフォルトで TObject クラスを自動的に継承
    • type TDate = class の定義は type TDate = class(TObject) 定義と同じ意味となる
    • すべてのクラスは基本的に TObject のサブクラス(子孫クラス)

       

  • CreateDestroyFree などは 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を使わずとも、動的に、代入されているオブジェクト参照が持っているメソッドを使用できるようになるテクニックをご紹介します。
実際にオブジェクト識別子に入っている参照のクラス型で実行されるメソッドが決定される方法を成し遂げるキーワードたちが virtualabstractoverride 三人衆です。

オブジェクト識別子の中に実際に入っているオブジェクト参照に基づいて、オブジェクト参照が持っているメソッドの処理が実行されることを「遅延バインディング」と言ったりもします。ですが、この遅延バインディングといった呼び名はさほど重要ではないので覚えなくてもOKですが、プログラムの話をする時に現れる用語であったりします。その時に思い出す程度に覚えておいてあげても良いでしょう。(上から目線)

では、この virtualabstractoverride について、まずは説明します。

\ 遅延バインディング三銃士をつれてきたよ! /

  • virtual : 親クラスのメソッド定義時に後方に付加しておくキーワード
    • 継承する子のクラスにて、当該メソッドが上書き(override)されることがあることを宣言しておくもの
    • 実行時にオブジェクト識別子に入っているクラス型によって実行するメソッドが変わる可能性があることを宣言している
    • 親のクラスで宣言しておくと、親クラス型のオブジェクト識別子に子クラス型のオブジェクトの参照が入っている時、子クラスのメソッドをキャストせず実行可能
    • virtualキーワードが付いていないと、オブジェクト識別子の宣言に使われたクラスのメソッドが実行される

       

  • abstract: 親クラスのメソッド定義時に後方に付加しておくキーワード
    • メソッド定義時の親クラス内ではメソッドの実装をせず、子のクラスにおいてoverrideで実装することを宣言しておくもの
    • virtualとセットで使う(overrideされることを前提としているキーワードなので)
    • このクラスを継承する子クラスにおいてoverrideキーワードで処理を実装して使用する
    • 親オブジェクト識別子に子オブジェクト参照を入れて使うとき、親オブジェクト識別子で子クラスのメソッドを使うために宣言のみしておく

       

  • override: 子のクラスのメソッド定義時に後方に付加するキーワード
    • 親クラスのvirtualキーワードのついているメソッドを上書きすることを宣言するもの
    • 親クラスのvirtualキーワードがついているメソッドの上書き時にのみ使用可能
    • 親オブジェクト識別子に、子オブジェクト参照を代入しているケースで、実行時に実際に代入されている子クラス型のメソッドが実行されるようになる

       

親、祖先のオブジェクト識別子に子オブジェクト参照を代入可能・その使用テクニック

それでは実際にどのようにしてキャストせずとも、親のオブジェクト識別子に、実際に代入されているオブジェクト参照に基づいたメソッドを実行させることができるのか見てみましょう。

下記のような親・子クラスの定義のモデルを例に説明

image

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 キーワードが付いています。
  • 子クラスのTServalTFennec クラスのいずれも親クラスと同名の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'の文字列を、TFennecVoiceメソッドは'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;

そしてこのコードを使ったサンプルコードを実行した結果のイメージショットがこちら
image

このコードとサンプルのポイント

  • 「なきごえ」ボタンが押されたとき、サーバル、フェネック、どちらのラジオボタンが選択されているかで、TServalTFennec どちらかのオブジェクトをCreateしています。
  • CreateしたオブジェクトはTKemono(親クラス)型のオブジェクト識別子 myAnimal に代入しています
  • 親クラスのオブジェクト識別子 + Voiceメソッド(myAnimal.Voice)で、キャストすることなく、代入されているオブジェクト参照のVoiceメソッドが実行されます。
  • このキャストすることなく、実際に代入されているオブジェクト参照に基づいたメソッドが実行されるようになるのは、以下の条件が揃えられているからでもあります。
    • 親のクラスがVoiceメソッドを持っているため、親のオブジェクト識別子.Voice とコードを書いてもコンパイルできる
    • 親のVoiceメソッドがvirtual として、動的に実行メソッドが決められる方法が取られることを定義している
    • 子のVoiceメソッドがoverride として、動的に実行メソッドが決められる方法が取られることを定義している

さらに使えるメタクラス (ご参考)

さらに、上のセクションで記述しているコードについて、クラスのCreateif文で分岐させず、Createをする部分をひとつにしてコードをクリアに記述したいような場合、どのクラスを使うのか、ルーチンに伝えて、別ルーチンでCreateさせる、といったこともできます。

  • クラス型名を扱えるよう型を定義する : class of (実クラスのオブジェクト参照ではなく、クラスの型名そのものを渡せる)を使います
  • 親のクラスの型名を型として宣言しておくと、メソッドのパラメータとしてクラスの型名(子のクラスの型名)を渡せる
  • 渡された先のメソッドでクラスの型名からオブジェクトのインスタンスを作れる(Createできる)

まずはtype ブロック

type
TFriends = class of TKemono; // TKemonoクラスの型名を表す [TFriends]を定義

すでに TKemonoクラスは上のセクションで定義してあるものとして、ここでは新たに TKemonoクラスのクラス名を扱える型としてTFriendsクラス名型識別子を定義しています。なんだかわかりにくい表現ですが、TKemonoTServalTFennecといったクラスを表す名を、この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には、TKemonoTServalTFennecといった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の部 になります。お楽しみに。

<<シーズン2 第6回の記事はこちら

シーズン2 第8回の記事はこちら>>



About
Gold User, No rank,
Sales consultant - Embarcadero Technologies , at Japan Twitter : @kazaiso

Comments

Check out more tips and tricks in this development video: