#Delphi Starter Edition チュートリアルシリーズ シーズン2 第7回 「オブジェクト指向」その1
2017年1月23日より 「Delphi / C++Builder Starter チュートリアルシリーズ」 シーズン2、全9回、3月27日まで、毎週月曜日、Delphiパートが 17時00分~17時20分、 リアルタイム放送スペシャルコンテンツが5分~10分、C++Builderパートが 17時30分~17時50分の時間割でお送りしています。
無料でダウンロード & 利用できる開発環境のDelphi / C++Builder Starter エディションを使用して、プログラミング言語のDelphi (Object Pascal ), C++の基礎を学ぶオンラインセッションです。
##使用したスライドなどの情報を掲載します。
###Webセミナースライド
第7回 「オブジェクト指向」のスライドは下記アドレスよりご参照いただけます。
###アジェンダ
- ねらい
- クラス・オブジェクトの機能を知る
- 隠蔽、継承を知る
- 実施内容
- 隠蔽、継承とはどんなものか
- 隠蔽する方法
- 継承する方法
- まつわるあれこれ
今回の第7回で紹介しているコードを実際にみれるサンプルをGithubに上げております。
以下のリンクからダウンロードしてご確認ください。
##以下、Delphi Webセミナーの本筋
###クラスとオブジェクト
これは先週行ったクラスとオブジェクトの概略説明のおさらいです。
- クラス・オブジェクトとは
- 任意のデータ型(フィールド)を保持できる
- データにアクセス、処理するルーチン(メソッド)を持つ
- 独自のフィールドとメソッドを保持するための、ひとかたまりとして定義されたもの(クラス)
* データやメソッドの隠蔽、クラスの継承ができる * 他のユニットやクラスからフィールド・メソッドへのアクセスを制限して隠しておくことができる * 既存のクラスが持っているフィールド・メソッドをそのまま保持しつつ、新しいフィールド・メソッドを追加したり、既存のメソッドを上書き(変更)したりして、機能を受けついた新しいクラスを構築することができる
* クラスとオブジェクトの関係 * `class`として`type`ブロックにてユーザーに定義されたフィールドやメソッドをもつデータ型が「クラス」 * クラスの定義を使用するときの実体(インスタンス とも言います)、つまり`Create`してメモリ上に確保してあるものが「オブジェクト」 * 今まで使用してきた`Integer`のような「データ型」とその識別子`I`のような「変数」の関係とほぼ同じ。 データ型を→ クラス ・変数を→ オブジェクト
余談:※実際にはオブジェクト識別子を
var
ブロックで宣言しときには、そのオブジェクト識別子はオブジェクトの実体のある場所を入れるための変数として宣言されただけで、実体は入っていないので、例として「データ型を→ クラス ・変数を→ オブジェクト」と表すと少しだけ違いがあります。クラスのオブジェクト識別子はオブジェクトの実体がある場所(アドレス・ポインタ)を入れるもので、Integerのようなデータ型の変数は、まさしくそのデータの入っている入れ物そのものを指し示しているという違いがあります。
この「実体がある場所のアドレス」を入れるといった取り扱い方は、第5回の配列とレコードの回において、動的配列の確保の時に「参照型」としてご紹介いたしました。クラスのオブジェクト識別子は、この「参照型」であります。 クラスの場合には「オブジェクト参照モデル」と呼ばれもします。
さらに余談ですが、この説明において「オブジェクト識別子」という言葉を使っていますが、これは定められた正しい呼称、というわけではありません。 一般的にはクラス変数 といった言い方をする場合もありますし、マルコカントゥ著のObject Pascal Handbookにおいては単に変数(variable)と言ったり、クラス型の変数(variable of a class type)と表現しています。
当説明においては、型が「クラス」であり、その実体が「オブジェクト」である、というところから、そのオブジェクトの参照を入れる変数として「オブジェクト識別子」と記述しています。もしかしたら正しく書こうとすると「オブジェクト参照変数識別子」 なのかもしれませんね。長い。(私の余談も長い。)
###クラスの定義・宣言の例
これはセミナーで使用したスライドのページをそのまま引用します。
このスライドで説明しようとしている要点は以下の通り
-
type
ブロックで= class
として定義され、フィールドや、メソッドを持つ「型」がクラス - 定義したクラスを使うには、まず
var
ブロックでオブジェクトの実体のポインタを入れておくためのオブジェクト識別子を用意 - オブジェクト識別子を用意したら、
implementation
(実装部)にて、そのクラス型をCreate
して実体を作り(確保し)、そのCreate
によって返されるオブジェクトの実体のあるアドレス(ポインタ、もしくは参照と言われる)をオブジェクト識別子にいれて、使います - オブジェクト識別子の中に入っているのは、オブジェクトの実体の参照(ポインタ) なんだよ~ ってことを絵で説明しています
再び余談:参照型を扱う上で、動的配列と異なる点は、CreateしたらFreeしなくちゃならんということ。これも先週お伝えしました。
そして参照型なので、オブジェクト識別子を別のオブジェクト識別子へと代入演算子:=
でコピーしたとしても、コピーされるのはオブジェクトの実体ではなく、オブジェクトの実体のありかを示している参照のみがコピーされます。これは動的配列の参照型と同じ動きですね。
###オブジェクト指向の便利さ
クラスやオブジェクトを使うメリットとは何なんでしょうか?
クラスを使わなくともプログラムは組めそうな気がしますよね。実際にその昔はオブジェクト指向の概念がないC言語主流の時代にはそれでプログラムを組んでいたわけですし…
しかしオブジェクト指向が提唱されてから、クラスを使ったプログラムは本当に便利になっています。そのあたりを説明してみます。
-
クラス(設計図)
- すでにあるパーツの設計図、もしくは自らパーツの設計図を、使いたいときに向けて用意しておくことができます。
-
オブジェクト(実体・インスタンス)
- クラス(設計図)が用意されてあれば、それをCreateして実際に部品を作り上げて、すぐ使うことができます
-
クラス継承
- そして既にあるクラス(設計図)を基に、さらに使いがってよくカスタマイズを施して、より良いものを使うこともできます
-
ポリモーフィズム(多態性)・遅延バインド
- 作った部品の機能に基づいた結果をそれぞれ導き出せるといった面もあります
###オブジェクト指向の便利さ… 実は既に使っています
便利さについて簡単に説明しましたが、Delphiを使用している方々は、そのクラスの便利を、既に享受しておられます。どのような部分でその便利さを受けているのでしょうか?
実は「マルチデバイスアプリケーションの新規作成」を行ってすぐにその便利さを体験しています。
-
クラス(設計図)
- 最初にウインドウとして配置されるフォーム。このTForm1はTFormというクラス。
-
オブジェクト(実体・インスタンス)
- TFormが既に用意されTForm1として(継承して)定義されているので…
- ビルドすればすぐにフォーム(ウインドウ)として利用できている
-
クラス継承
- TFormクラスがあるので、それを継承し、TForm1を作ることができている
- それに様々ButtonやらEditやらコンポーネントを付加して、新たなForm1として設計しなおして利用している
-
ポリモーフィズム(多態性)・遅延バインド
- イベントハンドラで渡されている「Sender」などで利用(後ほど説明)
###private, public , protected… 隠蔽と公開
クラス、オブジェクト指向の便利さを知っていいただいた上で、技術的な話に入ってまいります。
オブジェクト指向の一つの特徴である「隠蔽」と「公開」についてです。
- private, public , protected キーワードとは何か?
- 他ユニット上からのフィールドやメソッドのアクセスを制限、公開するためのキーワード
-
private
→ 他ユニットからは参照できず、アクセスもできない -
public
→ 他ユニットから参照、アクセス可能 -
protected
→ 他ユニットからは使用できない、当クラスを継承するクラスで参照・アクセス可能
private
に指定されたフィールドは、他のユニットのコードから直接触れず、クラス内での実装部分のみがフィールドを参照して操作することになるので、将来のコード改変時においてフィールドの構成を変えたとしても、その変更の影響範囲はクラス内の範囲にとどめておくことができる(かもしれない)といったメリットがあります。
参考DocWiki:http://docwiki.embarcadero.com/RADStudio/Berlin/ja/クラスとオブジェクト(Delphi)
###private, publicの使用例
- では、先週、定義したTDateクラスをprivate, publicを使って再び定義しなおしてみましょう
//まずは先週行ったprivate, publicを使っていないclass定義
type
TDate = class //TDateというクラス定義識別子を、「 = class 」としてクラス定義
Year, Month, Day: Integer; //クラスで保持するデータ(フィールド)の定義
procedure SetValue(Y, M, D: Integer); //クラスに紐づいたメソッド
function UruuDoshi: Boolean;
end; //クラス定義もend;で締める
- TDateクラスをprivate, publicを使って再度定義したコードが下記になります
type //先週クラス定義した TDateクラスをprivate, pubicを使って再度定義
TDate = class //TDateというクラス定義識別子を、「 = class 」としてクラス定義
private //privateキーワードで、他ユニットから参照されないように
Year, Month, Day: Integer; //クラスで保持するデータ(フィールド)の定義
public //publicキーワードで、他ユニットからでもアクセス可能に
procedure SetValue(Y, M, D: Integer); // 年月日をセットするメソッド
function UruuDoshi: Boolean; //うるう年かチェックするメソッド
function GetText: string; //フィールドにアクセスできないので、日付を文字列で取得する関数追加
end; //クラス定義もend;で締める
3行目にprivate
、5行目にpublic
が追加されています
private ブロックに記載されている Year, Month, Day: Integer
は他のユニットからは触れないフィールドとなりました。
そしてYear, Month, Day
を直接に参照することができなくなくなったので、このデータを取得すらためのメソッドをGetText
として別途用意しています。
余談: チュートリアル中にはお話しませんでしたが、Object Pascal言語において、
private
キーワードによる隠蔽は、「Unit」外からアクセスできない、という制限になります。
つまり、同じUnit内に存在する別のクラスからはアクセスできる仕様です。
unit内の他クラスからもアクセスさせないようにするためには、strict private
キーワードを代わりに使うことでより強力にUnit内の他クラスからもアクセスできないようにすることができます。
###private, public使用後の、フィールド変更例
先ほど、private
, public
を使用して定義したTDateクラスですが、年月日を表すフィールド Year, Month, Day : Integer'について、どうも
TDateTime`型を使ったほうが効率が良い気がしてきました…。そんな時、どのように変更するのでしょうか?
type //typeブロックでクラス定義を行う
TDate = class //TDateというクラス定義識別子を、「 = class 」としてクラス定義
private
FDate: TDateTime; //クラスで保持するデータ(フィールド)の定義
public
procedure SetValue(Y, M, D: Integer); // 年月日をセットするメソッド
function UruuDoshi: Boolean; //うるう年かチェックするメソッド
function GetText: String; //フィールドにアクセスできないので、日付を文字列で取得する関数追加
end; //クラス定義もend;で締める
4行目に定義していた Year, Month, Day: Integer;
を →FDate: TDateTime;
へと変更しました。
private で設定されているフィールドであれば、このフィールドを参照しているのは基本的に、このTDateクラス内での実装のみなので、比較的影響範囲は小さく済みそうです。
次のセクションで、クラス内のメソッド実装部について変更される部分を見てみましょう。
###privateフィールド変更後のクラスメソッドの実装変更例
下記がTDateクラスのメソッド実装部です。フィールドがTDateTime型に変更されたことを受けて、実装部を変更しています。 フィールドにYear, Month, Day: Integer;
を使っていたころのコードを//コメントで残してあります。
procedure TDate.SetValue(Y, M, D: Integer); //フィールドTDateTime化に伴い実装変更
begin
//旧実装 Year := Y; Month := M; Day := D;
FDate := EncodeDate(Y,M,D); //FDateフィールドに代入するように変更
end;
function TDate.Uruudoshi: Boolean; //フィールド変更に伴い実装変更
begin
//旧実装 Result := IsLeapYear(Year);
Result := IsLeapYear(YearOf(Fdate)); //FDateから年情報を取り出してチェック
end;
function TDate.GetText: String; //フィールドにアクセス制限があるため、必要情報を提供する新規関数
begin
Result := DateToStr(FDate); //日付情報の文字列を提供
end;
ここでのポイントは、TDateクラス内の'private' なフィールドを変更しましたが、その変更はクラス内の実装にとどまっており、メソッドのパラメーターなどの型や数は変更されていないということです。つまりはこれらメソッドを使っているほかコードにおいて、コード変更の必要がない、ということであります。コード変更の必要があるか、次のセクションで見てみましょう
###クラスメソッド改変後の使用例(使用する側での変更なし)
下記にメソッドを使用しているコード部分の例を挙げておきます。フィールド変更前と変更後で、同じメソッドを同じコードのまま利用できるということであります。
procedure TForm1.Button1Click(Sender: TObject);
var //varブロックでオブジェクトの識別子宣言を行う
Birthday : TDate; {ここではオブジェクトの参照を保存するための識別子を宣言したのみ}
begin
Birthday := TDate.Create; {これでオブジェクトの実データ(インスタンス)を確保して、その参照をオブジェクト識別子に代入した}
try
Birthday.SetValue(1995, 2, 14);
if Birthday.UruuDoshi then
ShowMessage('うるうどしです')
else
ShowMessage('うるうどしではない');
finally
Birthday.Free; // これでオブジェクトのために確保したメモリを解放
end;
end;
ということで「隠蔽」部のまとめとメリット:
- フィールドに他ユニットから直接アクセスしていなければ、コード改変時においても、使用コード上、影響が少ない
- 隠蔽と公開をしっかりわけておくことで、改変後の影響を少なくすることができる
- 内部のフィールドの直接の書き換えを防ぐことで予期せぬ結果も防げる
###クラスを使う上でのお約束 : try - finally
try
- finally
を使うことで、コード上でエラーが発生したとしても、そのエラーでコード実行を中断せず、必ず処理しておくべきことを記述することができます。たとえば、0による除算のようなエラーが発生した場合、エラー(例外)が発生し、以降の処理が全部中断されてルーチンから出てしまうケースがあります。そのような場合、ルーチン冒頭で、オブジェクトの確保を行っていると、ルーチン終了間際のFree
処理を行えず、確保した領域が残ったままになってしまいます。このようなケースを回避するため try
- finally
のセットを使い、必ず確保したオブジェクトを解放しておけるようにしておくことが必要であります。
-
例外(エラーが発生したとき)でも必ず処理すべきことを記述できる
-
try
で例外の処理範囲を開始 - 例外が発生しても、発生しなくても必ず処理する内容を
finally
-end;
で記述
-
-
予期せぬ問題が起こった場合でも
Create
で確保した領域を解放して、リソースを保護- もしかしたら、アプリケーションを継続できるかもしれない
- アプリを正常に終了できるようにしておけるかも
以下、try - finally の利用例
begin
Birthday := TDate.Create; //オブジェクトをCreateしてオブジェクトの参照をオブジェクト識別子に代入
try //tryで例外発生の検知をセット
Birthday.SetValue(1995, 2, 14);
if Birthday.UruuDoshi then
ShowMessage('うるうどしです')
else
ShowMessage('うるうどしではありません');
finally //try以降で例外が発生してもしなくても 必ず実行する処理を finally – end; に記述
Birthday.Free; //確保しているオブジェクトを解放
end; //finally のブロックはend; で閉じる
end;
参考 DocWiki: http://docwiki.embarcadero.com/RADStudio/Berlin/ja/例外ハンドラの記述
余談: try-finally は「例外の処理」といわれる方法の一部です。例外が起こる、起こらないにかかわらず処理するブロックを記述できるfinallyの他、エラー発生時に処理する except ~ on や 例外を生成する raise などの使い方があります。これら例外の処理について、今回のチュートリアルにおいてはオブジェクトをCreate Freeする間にはtry finallyで守っておいてね、と覚えておいてください。例外の処理にさらに興味のある方は、上記の参考DocWikiをご覧ください。
>FreeとDisposeOfについて。コード例ではオブジェクトの解放においてFreeメソッドを使っています。また、FreeだけでなくDisposeOfメソッドも同様に使用可能です。今後、モバイルデバイス向けのコードを書くような場合にはDisposeOfがオススメです。 Windows向けのコードを書くときでもDisposeOfは正しく動作し、内部でFreeを呼び出しています。くわしくは[こちら](http://docwiki.embarcadero.com/RADStudio/Seattle/ja/Delphi_%E3%83%A2%E3%83%90%E3%82%A4%E3%83%AB_%E3%82%B3%E3%83%B3%E3%83%91%E3%82%A4%E3%83%A9%E3%81%A7%E3%81%AE%E8%87%AA%E5%8B%95%E5%8F%82%E7%85%A7%E3%82%AB%E3%82%A6%E3%83%B3%E3%83%88#ARC_.E3.81.AE.E4.B8.8B.E3.81.A7.E3.81.AE_Free_.E3.83.A1.E3.82.BD.E3.83.83.E3.83.89.E3.81.8A.E3.82.88.E3.81.B3_DisposeOf_.E3.83.A1.E3.82.BD.E3.83.83.E3.83.89)をご参照ください
###初期処理・終了処理:コンストラクタ・デストラクタ
クラスは初期処理をする「コンストラクタ」、終了処理をする「デストラクタ」といったルーチンを持つことができます。まずはコンストラクタ・デストラクタの概略から:
- コンストラクタ
- オブジェクト使用時に最初に初期化を行うために用意されているメソッド
- 通常、
Create
として用意されている・用意する
参考DocWiki : http://docwiki.embarcadero.com/RADStudio/Berlin/ja/メソッド(Delphi)
####コンストラクタのCreate, デストラクタのDestroyの定義
ここではコンストラクタの特別な動きについてと、コンストラクタとデストラクタの定義について説明していきます。
- コンストラクタはクラス内定義で
constructor
キーワードに続いて宣言-
constructor
のついているメソッドはそのクラスのコンストラクタとして扱われる(たとえCreate
というメソッド名でなくとも、constructor
キーワードのついているメソッドがコンストラクタとして扱われます。ただし一般的にはCreate
というメソッド名が使われます) - コンストラクタは戻り値を宣言せずとも、確保したオブジェクトの参照を返してくれるメソッドになる
- コンストラクタで作られたオブジェクトは、すべてのデータが0に設定される(またはnil, 空文字列など、データ型に応じて初期化される)
- コンストラクタの主な目的は初期化、初期値の設定
-
* デストラクタはクラス内定義で `destructor` キーワードに続いて `Destroy` メソッド宣言 * `Free`を使ったときに`Destroy`が呼び出されている。つまり`Destroy`というメソッドめいでなければ`Free`の時に実行されない (宣言の後ろについている override は後ほど説明) * デストラクタの主な目的なクリーンアップ
下記にクラスのコンストラクタ、デストラクタの定義例を記述します:
type //typeブロックのクラス定義
TPerson = class //新たにTPersonクラスを定義
private
FName: string;
FBirthDay : TDate; //以前に定義した日付情報をもつTDateクラスをフィールドとして使用
public
constructor Create (name: string); //コンストラクター Create の宣言
destructor Destroy; override; //デストラクタ Destroyの宣言 + override
//そのほか必要なメソッド等…
end; //クラス定義end;
###コンストラクタのCreate, デストラクタのDestroyの実装
コンストラクタ・デストラクタの定義例を見たところで、実装コード例を見てみます
-
コンストラクタ: 紐づいている
クラス識別名.コンストラクタ定義識別子
(通常はCreate
で定義)で実装- フィールドに特別な値を、初期値として入れたい場合はここで行う
- フィールドにクラスを使いたい場合には、ここでCreateしてオブジェクトを確保しておく
-
inherited Create;
: 継承した親クラスにある同名メソッド(コンストラクタ)を実施 (継承の説明は後ほど)
Implementation
constructor TPerson.Create (name: string); //コンストラクター Create の実装
begin
inherited Create; //パラメータなしの親クラスのCreateを実行する
FName := name; //Create時の初期値としてフィールドに代入
FBirthDay := TDate.Create; //フィールドに使用しているTDate型のクラスの確保
end;
destructor TPerson.Destroy; //デストラクタ Destroyの宣言 (実装部に override キーワードは不要)
begin
FBirthDay.Free; //Create時に確保したFBirthDayの破棄
inherited; //何もついていないinheritedは、実装のメソッド名と全く同じメソッド名&パラメータの親メソッドを実行する
end;
この実行コード例のポイント
- 定義内同様に、コンストラクタは
constructor
キーワードに続いてクラス名.メソッド名
で実装 - 定義内同様に、コンストラクタは
destructor
キーワードに続いてクラス名.メソッド名
で実装 - クラス内のフィールドとしてクラスを使っている(
TDate
クラスをフィールドとして使っている)場合には、コンストラクタ内でそのクラスのオブジェクトをCreate
しておく(そうしないとフィールドとしてオブジェクトが使えない - コンストラクタでオブジェクトを
Create
していたら、そのオブジェクトはデストラクタのDestroy
内で破棄(Free
)しておく -
inherited
を使うことで親クラスの同名メソッドを実行する
###同名メソッド、同名コンストラクタを作る 「overload」
- 同じメソッド識別子名、または同じコンストラクタ名で、パラメータの異なるメソッド、コンストラクタを複数記述可能(第4回の関数と手続きの
overload
と同等)- 実行時には、パラメータの数や型で一致するメソッド、コンストラクタが判断され実行される
- 似たような
override
キーワードは後ほどの「継承」の時にご紹介(overload
と混同しないようにしましょう)
overloadキーワードを使った同名コンストラクタ・メソッド定義例
type //typeブロックでクラス定義
TDate = class //TDateというクラス定義識別子を、「 = class 」としてクラス定義
private
FDate : TDateTime; //クラスで保持するデータ(フィールド)の定義
public
Constructor Create; overload; //デフォルトのCreateと同じ記述でOverload指定
Constructor Create(Y, M, D: Integer); overload; //初期値を与えるCreateのoverload
procedure SetValue(Y, M, D: Integer); overload; //年月日をIntegerでセットするメソッド
procedure SetValue(newDate: TDateTime); overload; //年月日をTDateTimeでセットするメソッド
function UruuDoshi: Boolean; //うるう年かチェックするメソッド
function GetText: String; //フィールドにアクセスできないので、日付を文字列で取得する関数
end; //クラス定義もend;で締める
###overload のCreate, SetValue実装例
定義例をみたところで、実装例を見てみましょう
といってもこれは第4回の関数と手続きの overload
と同等なので、特に多くの説明をしていません。定義時にoverload
キーワードを着けておくことで、同名メソッドを定義・実装でき、実行時にはパラメータの型、数でどのメソッドが実行されるか決定されます。
implementation
{ TDate }
constructor TDate.Create; //実装部においてoverloadキーワードは記述不要
begin
inherited; //メソッド名もパラメータも同じであれば inherited の後ろのパラメータは省略可能
Fdate := Date; //初期値としてDateで今日の日付を代入
end;
constructor Tdate.Create(Y, M, D: Integer);//初期値有のCreate.実装部においてoverloadキーワードは記述不要
begin
inherited Create; {パラメータが親クラスのCreateと異なるので、省略できず、inheritedの後ろにCreateを記述して親のCreateを実行することを明記している}
FDate := EncodeDate(Y, M, D); //指定されたパラメータをTDateTime型に変換して初期値として代入
end;
procedure TDate.SetValue(Y, M, D: Integer); //Integer x 3の時実行.実装部においてoverloadキーワードは記述不要
begin
FDate := EncodeDate(Y, M, D); //指定されたパラメータをTDateTime型に変換してFDateフィールドに代入
end;
procedure TDate.SetValue(NewDate: TDateTime);//TDateTime型のパラメータの時実行.実装部にはoverloadキーワード不要
begin
FDate := NewDate;
end;
###その2へ続く
クラスとオブジェクトのお話でした。セミナーでは引き続き、継承の話へと進みましたが、だいぶ長くなりましたので、「その2」として別ページにまとめます。
その2へ>>