11-4 手続き・関数の上書き

【練習問題】

せっかくボスインベーダーを作ったのですから、他のインベーダーとは違う外見にしたいものです。そこで、赤色で描画する手続き Draw を作りましょう。invader_boss ユニットに次のように書き加えてください。

unit invader_boss;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls, Spin, Invader;

type
  TFormInvaderBoss = class(TFormInvader)
  private
    { Private 宣言 }
  protected
    procedure Draw; virtual;
  public
    { Public 宣言 }
    procedure RandomMove; virtual;
  end;

var
  FormInvaderBoss: TFormInvaderBoss;

implementation

uses display, bullet;

{$R *.DFM}

procedure TFormInvaderBoss.RandomMove;
var
  NewSpeed: Integer;
begin
  NewSpeed := GetSpeed + Random(7) - 3;
  SetSpeed(NewSpeed);
  Move;
end;

procedure TFormInvaderBoss.Draw;
begin
  FormDisplay.ImageField.Canvas.Pen.Color := clRed; // 赤でインベーダを描く
  FormDisplay.ImageField.Canvas.MoveTo(X, Y - 3);
  FormDisplay.ImageField.Canvas.LineTo(X - 5, Y + 3);
  FormDisplay.ImageField.Canvas.LineTo(X + 5, Y + 3);
  FormDisplay.ImageField.Canvas.LineTo(X, Y - 3);
end;

end.

実行してみましょう。「未定義の識別子 : 'X'」というエラーが出ましたね。よく考えると「インベーダー」フォームにおける変数 X, Yprivate 部にあるのでした。そこで、変数 X, Y を読み出す関数 GetX, GetY を作ります。invader ユニットの冒頭部を次のように変更してください。

type
  TFormInvader = class(TForm)
  private
    { Private 宣言 }
    X: Integer;
    Y: Integer;
    Speed: Integer;
    Alive: Boolean;
  protected
    procedure Draw; virtual;
    procedure Erase; virtual;
    procedure SetSpeed(S: Integer); virtual;
    function GetSpeed: Integer; virtual;
    function GetX: Integer; virtual;
    function GetY: Integer; virtual;
    procedure GoHorizontal; virtual;
    procedure GoDown; virtual;
    procedure HitCheck; virtual;
  public
    { Public 宣言 }
    procedure Move; virtual;
    procedure Init; virtual;
  end;

ユニットの末尾には、関数本体を書き加えます。

// X 呼び出し
function TFormInvader.GetX: Integer;
begin
  GetX := X;
end;

// Y 呼び出し
function TFormInvader.GetY: Integer;
begin
  GetY := Y;
end;

end.

最後に invader_boss ユニットに戻って Draw 手続きを書き換えます。

procedure TFormInvaderBoss.Draw;
begin
  FormDisplay.ImageField.Canvas.Pen.Color := clRed; // 赤でインベーダを描く
  FormDisplay.ImageField.Canvas.MoveTo(GetX, GetY - 3);
  FormDisplay.ImageField.Canvas.LineTo(GetX - 5, GetY + 3);
  FormDisplay.ImageField.Canvas.LineTo(GetX + 5, GetY + 3);
  FormDisplay.ImageField.Canvas.LineTo(GetX, GetY - 3);
end;

実行してみましょう。


残念ながらボスインベーダーは赤く表示されません。これは、今作った Draw メソッドとインベーダーフォームに元からある Draw メソッドがかち合っていること (そしてインベーダーフォームに元からある Draw メソッドが優先されていること) が原因です。これを解消するには、後から作った (ボスインベーダーフォーム側の) Drawvirtual の代わりに override (上書き) と指定してやります。invader_boss ユニットの冒頭部を次のように変えてください。

type
  TFormInvaderBoss = class(TFormInvader)
  private
    { Private 宣言 }
  protected
    procedure Draw; override;
  public
    { Public 宣言 }
    procedure RandomMove; virtual;
  end;

実行して動作を確認しましょう。今度は赤いボスインベーダーが現れるはずです。

ここまでのプログラムをダウンロード(D)

コラム virtual (バーチャル) って何?

virtual は、その関数・手続きを後から上書きできるようにする指令 (メソッド) です。少し順を追って説明しましょう。virtual をつけて関数や手続きを宣言すると、その具体的中身 (処理内容) はプログラム実行時まで確定されません。ただ、名前 (上の例で言うと Draw) だけが登録されます。そして継承を受けたフォーム (クラス) で override メソッドをつけると、(名前は同じでも) 処理内容を再定義することができます。

それでは、なぜこのような上書き (再定義) が必要なのでしょうか? 今の例の場合、Draw (メソッド) の本質的な意味は、形を描くという点であり、これは元のインベーダーでもボスインベーダーでも同じです。ただ、その描き方が異なるだけです。このような場合、インベーダーと (それを継承した) ボスインベーダーで、一々別な手続き名をつけるよりも、同じ Draw という名前で表現した方がプログラミングしやすくなります。そうすれば、どのように描くかという詳細はフォーム (クラス) に任せ、プログラマは形を描くという本質のみに集中できるからです。ちなみに、同じメソッド名でありながら処理内容の詳細がオブジェクトの性質に応じて変わって行く性質を、オブジェクト指向の言葉で多態(たたい)と呼びます。よく「オブジェクト指向のポイントは物事の本質を見抜きそれを表現することだ。」と言われますが、この多態性はそれを実現する重要な機能なのです。

さて、一般にどの関数が上書きされるようになるかは、予測が困難です。そこで、本テキストでは、いつ上書きされてもいいように原則として関数・手続きには virtual 指令を付けておき、「どうしても上書きされたくない」関数・手続きだけ virtual 指令をつけないようにすることを推奨します。 virtual がついた関数・手続きが上書きされずに使われても何の問題もありません。(「必ず上書きしなければならない」という指令として abstract がありますが、ここでは触れません。) 一方、 virtual 指令を付けないでいると、後から上書きしようとしたときに改めて virtual 指令を付けなくてはなりません。これは、既に完成している (継承元の) フォームを変更することであり、望ましくありません。