ユニット

ユニットとはVBにおける標準モジュールのようなものです。 ただ、標準モジュールとは異なり、Object Pascalではインターフェイス部と呼ばれる外部に公開するプロシージャ・関数・定数・型などを記述する部分があります。 これはC/C++でいうプロトタイプ宣言によく似たものです。 また、ユニットはメインプログラムのあるファイルとは別のファイルに記述されます。 簡単なユニットとそれを用いたサンプルを次に示します。

ユニット LearningDelphi.dpr
{$APPTYPE CONSOLE}

program LearningDelphi;

uses
  SysUtils,
  SampleUnit in 'SampleUnit.pas';

begin

    SampleProcedure;

    Writeln( SampleFunction );

    // 終了メッセージを表示
    Write( 'Press any key to continue' );
    Readln;

end.
SampleUnit.pas
unit SampleUnit;

interface

uses
    SysUtils;

procedure SampleProcedure;

function SampleFunction : string;


implementation

var
    sampleString: string;

procedure SampleProcedure;
begin

    Writeln( sampleString );

end;

function SampleFunction : string;
begin

    result := sampleString;

end;

initialization
begin

    Writeln( '初期化処理がなされます' );

    sampleString := 'SampleUnit';

end;

finalization
begin

    // メインプログラム終了後に呼び出されます

end;

end.
実行結果
初期化処理がなされます
SampleUnit
SampleUnit

まず、ユニットを利用する側ではuses文で使用するユニットとそれが記述されているファイルを指定します。 それ以外に特別な記述は必要ありません。 後はユニットにあるプロシージャや型などを利用するだけです。 そして、それらの要素を実際に記述するのがユニットの方です。 ユニットは次のような構造になっています。

ユニットの構造

まず、ユニットヘッダではユニットの名前を指定します。 Delphiではここで指定した名前とファイル名が一致している必要があります。 ユニット本体の記述では、インターフェイス部と実現部の二つの部分を記述します。 実現部は実装部と言い換えてもいいかもしれません。 インターフェイス部でユニット外に公開するプロシージャ・関数の宣言部だけを記述し、実現部でその実装を行います。

また、実装部にはinitialization文とfinalization文をオプションで記述することができます。 ユニットをインスタンスかできないクラスと考えれば、この部分はコンストラクタ・デストラクタに相当するといえます。 この例ではusesをインターフェイス部で使用しましたが、ユニット同士の循環参照が起こる場合にはいずれかのusesを実現部に移すことで解決できます。 また、usesはできるだけ実現部で使用するべきとされています。

実行結果を検証してみると、メインプログラムから呼び出されたユニットのプロシージャ・関数は適切に動作しています。 また、すべてのプロシージャ・関数などが使用される前にユニットの初期化部が動作しています。 この例の終了処理部にWritelnなどを記述しても、アプリケーション終了直前に呼び出されるため、実際には表示された結果を見ることはできません。

クラス 基本編

Object Pascalにおけるクラスの概念もC/C++のクラスと大域的には同じといえます。 また、記述方法もC/C++と非常によく似ています。

クラス宣言 SampleUnit.pas
unit SampleUnit;

// インターフェイス部
interface

// 型宣言
type
    TMusume = class

        // プライベートスコープ
        private

            // フィールド
            m_Age: integer;
            m_BirthDate: TDateTime;

            // メソッド
            function CalcAge: integer;

            // プロパティプロシージャ
            function getAge: integer;

            function  getBirthDate: TDateTime;
            procedure setBirthDate( const birthDate: TDateTime );

        // パブリックスコープ
        public

            // フィールド
            Name: string;

            // プロパティ
            property Age: integer read getAge;
            property BirthDate: TDateTime read getBirthDate write setBirthDate;

            // メソッド
            procedure Introduce;

            // コンストラクタ
            constructor Create( nam: String );

            // デストラクタ
            //destructor Destroy;

    end;


// 実装部
implementation

uses
    SysUtils;

// コンストラクタ
constructor TMusume.Create( nam: String );
begin

    Name := nam;

end;

// プロパティプロシージャ
function TMusume.getAge: integer;
begin

    result := m_Age;

end;

procedure TMusume.setBirthDate( const birthDate: TDateTime );
begin

    m_BirthDate := birthDate;
    m_Age := CalcAge;

end;

function TMusume.getBirthDate: TDateTime;
begin

    result := m_BirthDate;

end;


// メソッド
procedure TMusume.Introduce;
var
    y, m ,d: Word;

begin

    DecodeDate( m_BirthDate, y, m, d );

    Write( Name, ' ' );
    Write( y, '年', m, '月', d, '日生まれ ' );
    Writeln( m_Age, '歳' );

end;


function TMusume.CalcAge: integer;
var
    age: Word;
    by, bm, bd: Word;
    ny, nm, nd: Word;

begin

    DecodeDate( m_BirthDate, by, bm, bd );
    DecodeDate( Now, ny, nm, nd );

    age := ny - by;

    if ( bm > nm ) or ( ( bm = nm ) and ( bd > nd ) ) then
        age := age - 1;

    result := age;

end;


end.

まず、type文で「識別子 = class」としてクラス型を宣言します。 さらに、そのクラスのフィールド・インターフェイスを定義します。 各メンバはスコープを持つことができ、基本的にはC/C++と同じ「private, proteced, public」の三つです。 これ以外にもいくつか特殊なものがありますがここでは省略します。  プロシージャ・関数・フィールドのインターフェイスは今までどおりです。 また、Object Pascalのクラスはプロパティを持つことができます。 プロパティとなるフィールドはpropertyで宣言し、別途プロパティプロシージャを用意してある必要があります。

プロパティプロシージャの名前は任意ですが、書き込み用プロシージャは値をひとつだけ取り戻り値なし、読み取り用関数は引数なしで戻り値ありのものを用意します。 これらのプロパティプロシージャはプロパティ宣言部より前で宣言されている必要があります。 また、プロパティは読み取り専用にも書き込み専用にもすることができ、そうする場合はreadないしwriteを省略します。 この例ではプロパティプロシージャはprivateスコープですが、publicにすることで普通のプロシージャ同様に呼び出すことも可能です。 また、コンストラクタおよびデストラクタを実装することもできます。 その場合は「constructor 識別子(引数リスト);」ないし「destructor 識別子;」などとします。

インターフェイスの宣言が完了したら、実現部でその実装を行います。 これはC/C++のクラスにおけるメンバ関数の外部定義と同様の方法で行います。 つまり、「procedure クラス名.プロシージャ名」、「function クラス名.関数名」のように、関数・プロシージャの名前の前にクラスの名前を指定します。 また、これらのプロシージャの中はクラスの中と同様なので、クラスのメンバにクラス名を付加する必要はありません。 こうしてできたクラスは次のようにして使用します。

クラスインスタンス生成 LearningDelphi.dpr
{$APPTYPE CONSOLE}

program LearningDelphi;

uses
    SysUtils,
    SampleUnit in 'SampleUnit.pas';

var
    // クラス型変数を宣言
    aibon: TMusume;

begin

    // インスタンスを生成
    aibon := TMusume.Create( '加護亜依' );

    // プロパティ値を設定
    aibon.BirthDate := EncodeDate( 1988, 2, 7 );

    // メソッドを呼び出し
    aibon.Introduce;

    // インスタンスを破棄
    aibon.Free;

    // 終了メッセージを表示
    Write( 'Press any key to continue' );
    Readln;

end.

まず、クラス型変数の宣言は普通の変数宣言と変わりありません。 インスタンスを生成するには「クラス型.コンストラクタ(引数)」とします。 このときコンストラクタに渡すべきパラメータがないときは省略できますし、クラスにコンストラクタ宣言がないときは暗黙の基底クラスであるTObjectから継承したCreateメソッドを利用することができます。 また、デストラクタも同様で、特にクラスで宣言されていない場合はFreeまたはDestroyメソッドを使用することができます。 その他メソッドの呼び出しやフィールド・プロパティへのアクセスはC/C++やVBと変わりありません。 このコードを実行すると次のようになります。

実行結果
加護亜依 1988年2月7日生まれ 15歳
Press any key to continue

参考までに、これと同様の動作をするクラスとそれを利用するコードを、VB.NETで組んだものを例示しておきます。 見た目もできる限り似せています。

クラス VB.NET版
Imports System

Class TMusume

    Private m_Age As Integer
    Private m_BirthDate As DateTime

    Public Name As String

    ' コンストラクタ
    Private Sub New(ByVal name As String)

        Me.Name = name

    End Sub

    Public Shared Function Create(ByVal name As String) As TMusume

        Return New TMusume(name)

    End Function

    ' プロパティ
    Public ReadOnly Property Age() As Integer

        Get

            Return m_Age

        End Get

    End Property

    Public Property BirthDate() As DateTime

        Get

            Return m_BirthDate

        End Get

        Set(ByVal Value As DateTime)

            m_BirthDate = Value
            m_Age = CalcAge()

        End Set

    End Property

    ' メソッド
    Public Sub Introduce()

        Dim y, m, d As Integer

        y = m_BirthDate.Year : m = m_BirthDate.Month : d = m_BirthDate.Day

        Console.Write(Name + " ")
        Console.Write("{0}年{1}月{2}日生まれ ", y, m, d)
        Console.WriteLine("{0}歳", m_Age)

    End Sub

    Private Function CalcAge()

        Dim age As Integer
        Dim by, bm, bd As Integer
        Dim ny, nm, nd As Integer

        by = m_BirthDate.Year : bm = m_BirthDate.Month : bd = m_BirthDate.Day
        ny = Now.Year : nm = Now.Month : nd = Now.Day

        age = ny - by

        If (bm > nm) OrElse ((bm = nm) AndAlso (bd > nd)) Then age -= 1

        CalcAge = age

    End Function

End Class


Class SampleClass

    Shared aibon As TMusume

    Public Shared Function Main(ByVal args() As String) As Integer

        aibon = TMusume.Create("加護亜依")

        aibon.BirthDate = New DateTime(1988, 2, 7)

        aibon.Introduce()

        ' 解放処理は無用

        Return 0

    End Function

End Class

クラス 継承編

Object Pascalのクラスでは、当然継承もサポートされます。

SampleUnit.pas
unit SampleUnit;

// インターフェイス部
interface

// 型宣言
type
    TMusume = class

        // プライベートスコープ
        private

            // フィールド
            m_Age: integer;
            m_BirthDate: TDateTime;

            // メソッド
            function CalcAge: integer;

            // プロパティプロシージャ
            function getAge: integer;

            function  getBirthDate: TDateTime;
            procedure setBirthDate( const birthDate: TDateTime );

        // パブリックスコープ
        public

            // フィールド
            Name: string;

            // プロパティ
            property Age: integer read getAge;
            property BirthDate: TDateTime read getBirthDate write setBirthDate;

            // メソッド
            procedure Introduce; virtual;

            // コンストラクタ
            constructor Create( nam: string );

    end;

    // TMusumeの派生クラス
    TDerived = class(TMusume)

        public

            // フィールド
            BloodType: string;

            // メソッド(オーバーライド)
            procedure Introduce; override;

            // コンストラクタ
            constructor Create( nam: string; bldType: string );

    end;


// 実装部
implementation

uses
    SysUtils;

// コンストラクタ
constructor TMusume.Create( nam: string );
begin

    Name := nam;

end;

// プロパティプロシージャ
function TMusume.getAge: integer;
begin

    result := m_Age;

end;

procedure TMusume.setBirthDate( const birthDate: TDateTime );
begin

    m_BirthDate := birthDate;
    m_Age := CalcAge;

end;

function TMusume.getBirthDate: TDateTime;
begin

    result := m_BirthDate;

end;


// メソッド
procedure TMusume.Introduce;
var
    y, m ,d: Word;

begin

    DecodeDate( m_BirthDate, y, m, d );

    Write( Name, ' ' );
    Write( y, '年', m, '月', d, '日生まれ ' );
    Writeln( m_Age, '歳' );

end;


function TMusume.CalcAge: integer;
var
    age: Word;
    by, bm, bd: Word;
    ny, nm, nd: Word;

begin

    DecodeDate( m_BirthDate, by, bm, bd );
    DecodeDate( Now, ny, nm, nd );

    age := ny - by;

    if ( bm > nm ) or ( ( bm = nm ) and ( bd > nd ) ) then
        age := age - 1;

    result := age;

end;


// 派生クラスでのコンストラクタ
constructor TDerived.Create( nam: string; bldType: string );
begin

    Name := nam;
    BloodType := bldType;

end;


// オーバーライドされたメソッド
procedure TDerived.Introduce;
var
    y, m ,d: Word;

begin

    DecodeDate( m_BirthDate, y, m, d );

    Write( Name, ' ' );
    Write( y, '年', m, '月', d, '日生まれ ' );
    Write( m_Age, '歳 ' );
    Writeln( BloodType, '型' );

end;


end.
LearningDelphi.dpr
{$APPTYPE CONSOLE}

program LearningDelphi;

uses
    SysUtils,
    SampleUnit in 'SampleUnit.pas';

var
    // クラス型変数を宣言
    aibon: TDerived;
    musume: TMusume;

begin

    // インスタンスを生成
    aibon := TDerived.Create( '加護亜依', 'AB' );

    // プロパティ値を設定
    aibon.BirthDate := EncodeDate( 1988, 2, 7 );

    // メソッドを呼び出し
    aibon.Introduce;

    // 基底クラス型へキャスト
    musume := aibon;

    // メソッドを呼び出し
    musume.Introduce;

    // インスタンスを破棄
    aibon.Free;

    // 終了メッセージを表示
    Write( 'Press any key to continue' );
    Readln;

end.
実行結果
加護亜依 1988年2月7日生まれ 15歳 AB型
加護亜依 1988年2月7日生まれ 15歳 AB型
Press any key to continue

あるクラスを基底クラスとしてクラスを派生するにはtype文で「派生クラス = class(基底クラス)」とします。 Delphiにおける継承は、.NET FrameworkのCLSにおける継承と同じく、クラスの多重継承は認められていないものの、インターフェイスは複数実装することができます。 この例ではTMusumeを継承してTDerivedという派生クラスを作成しています。 まず、Introduceメソッドをオーバーライドするために、 TDerivedでのIntroduceメソッドの宣言の最後にoverrideを付加します。 当時に、オーバーライドされる基底クラスの Introduceメソッドにもvirtualを付加します。 後は各メンバの実装を行うだけです。

ちなみに、実行結果を見ると、TDerived型のインスタンスをTMusume型に代入しても、当然TDerived型の Introduceメソッドが呼び出されます。 しかし、派生クラスでのメソッド宣言でoverrideを取り去ると、派生クラスのメソッドは基底クラスのメソッドをシャドウしたかのような動作になります。

クラスの継承 SampleUnit.pas
{ 途中省略 }

// TMusumeの派生クラス
TDerived = class(TMusume)

    public

        // フィールド
        BloodType: string;

        // メソッド(シャドウ)
        procedure Introduce;

        // コンストラクタ
        constructor Create( nam: string; bldType: string );

end;
実行結果
加護亜依 1988年2月7日生まれ 15歳 AB型
加護亜依 1988年2月7日生まれ 15歳
Press any key to continue

ただし、このままだと「'Introduce'メソッドが基本型'TMusume'の仮想メソッドを隠しました」という警告が発生します。 これを解決するにはさっき消し去ったoverrideの変わりにreintroduceを付け替えます。

クラス TObject編

Object Pascalのクラスは、暗黙的にTObjectを継承します。

LearningDelphi.dpr
{$APPTYPE CONSOLE}

program LearningDelphi;

uses
  SysUtils,
  SampleUnit in 'SampleUnit.pas';

var
    // クラス型変数を宣言
    aibon: TDerived;

begin

    // インスタンスを生成
    aibon := TDerived.Create( '加護亜依', 'AB' );

    // クラス名
    Writeln( aibon.ClassName );

    // 基底クラス名
    Writeln( aibon.ClassParent.ClassName );

    // インスタンスのサイズ
    Writeln( aibon.InstanceSize );

    // インスタンスを破棄
    aibon.Free;

    // 終了メッセージを表示
    Write( 'Press any key to continue' );
    Readln;

end.
実行結果
TDerived
TMusume
28
Press any key to continue

ここで呼び出したメソッドは全てTObjectから継承したメソッドです。 このほかにもいくつかメソッドが存在しますが、その多くは特別な場合を除いてあまり使用しないでしょう。

Windows アプリケーション

Delphiが賞賛される要因、そしてVisual Basicと比較される要因の一つが、RADにあると思います。 コンソールアプリで遊ぶのをそろそろ切り上げて、簡単なWindowsアプリケーションを作ってみます。 ところで一般的にはよくRAD環境と言いますけど、RADとはRapid Application Developmentの略なので「RAD環境」はRapid Application Development Developmentと冗長な表現になっています。 SerializableAttribute属性とか言っているのと同じ気がするのですがいいのでしょうか。 (確かに日本語の語感からするとRAD環境の方がしっくりくるのですが・・・)

よけいな話しになりましたが、まずはじめは生成されるテンプレートについて見てみることにします。

生成されたテンプレート

FirstWindowsApplication.dpr
program FirstWindowsApplication;

uses
  Forms,
  MainForm in 'MainForm.pas' {Form1};

{$R *.res}

begin
  Application.Initialize;
  Application.CreateForm(TForm1, Form1);
  Application.Run;
end.
MainForm.pas
unit MainForm;

interface

uses
    Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
    Dialogs;

type
    TForm1 = class(TForm)
    private
        { Private 宣言 }
    public
        { Public 宣言 }
    end;

var
    Form1: TForm1;

implementation

{$R *.dfm}

end.

生成されたフォーム

生成されたフォーム

メインプログラムの方は特に問題ありません。 アプリケーションを初期化して、フォームを作成して、実行してメッセージループに入るだけです。 ただ、 MainFormユニットの方はフォームの実装を記述する部分ですが、VBとかなり異なります。 まず注目すべきは、フォームはTForm型から継承したクラス型であることです。 VBでもフォームは一種のクラスでしたが、Delphiでのフォームは継承が可能です。

それでは早速ログインダイアログのようなものを作ってみます。 このアプリケーションはエディットボックスに入力された文字をメッセージボックスを利用して表示するだけの単純なアプリケーションです。

デザイン
MainForm.pas
unit MainForm;

interface

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

type
    TForm1 = class(TForm)
    Edit1: TEdit;
    Button1: TButton;
    Label1: TLabel;
    procedure Button1Click(Sender: TObject);
    procedure Edit1KeyPress(Sender: TObject; var Key: Char);
    private
        { Private 宣言 }
    public
        { Public 宣言 }
    end;

var
    Form1: TForm1;

implementation

{$R *.dfm}

// ボタンのクリック
procedure TForm1.Button1Click(Sender: TObject);
begin

    MessageDlg( 'Hello, ' + Edit1.Text + '!', mtInformation, [mbOK], 0 );

end;

// Enterキーによる入力
procedure TForm1.Edit1KeyPress(Sender: TObject; var Key: Char);
begin

    if Key = #13 then Button1Click( nil );

end;

end.
実行結果

灰色の部分はデザイン時に自動的に追加された部分と、イベントハンドラとして作成された部分です。 青いところが実際に記述したコードです。 これを実行するとこんな感じになります。