Adaの日本語情報を増やす(7)・OOPその2
OOP続き。実は書いている人も勉強しながらメモまくってるだけなので、間違った情報があるかも。
前回の記事で派生型が副プログラムを継承することを説明しました。その知識を踏まえた上で、今回はAda85で追加されたtagged record
による、本格的なOOPに入ります。
さて、前回の派生型では、record
型の派生を例に副プログラムの継承(らしきもの)を解説しました。本来派生型はrecord
型だけでなく、整数型や配列など、あらゆる型から派生することができるのですが、record
型を使って解説を行ったのには理由があります。
OOPにおけるクラスとは大ざっぱに言えば一連のインスタンス変数(メンバ変数でもプロパティでも何でも良いですが)と、それを操作するメソッドの集合です。このうち、メンバ変数とはこれまた大ざっぱに言えば、変数名と値の対応関係を持った記憶域ということになります。Adaにおいてこのような対応関係を持つ型と言えば――そう、reocrd
型です。Ada95で導入された本格的なOOPの仕組みも、このrecord
型を拡張したtagged record
型により実現されています。前回の記事でrecord
型を使用したのは、今回の記事への土台だったわけですね。
前回の記事で解説したとおり、いくつかの問題点を除けば、Ada83でもrecord
型とprocedure
・function
の組み合わせにより、OOPの基本的な操作は可能となっています。しかし、この組み合わせによるOOPには、サブクラスでのインスタンス変数――つまりはrecord
の要素――の拡張ができないという大きな問題があります。前回の記事ではVolume
というインスタンス変数をスーパークラス・サブクラス共通で使用していましたが、仮にサブクラスであるCoffees
にのみ、Beans
というインスタンス変数を追加したい場合、単純なrecord
型で実現できません。単純なrecord
型から派生しても、要素の追加はできませんからね。
この問題を解決するためにAda95で導入されたのがtagged record
型です。tagged record
型は派生時の宣言において、新しい要素の追加が可能になったrecord
型です。
さて、ではコードを読んでみましょう。
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;
procedure OOP2 is
--- ほぼは前回と同じ
package Drinks is
type Drink is tagged private; -- カプセル化のためにprivate
procedure Add(O : in out Drink; V : in Positive);
procedure Spilt(O : in out Drink; V : in Positive);
function Get_Volume(O : in Drink) return Natural;
private
type Drink is tagged record
Volume : Natural := 0;
end record;
end Drinks;
package body Drinks is
procedure Add(O : in out Drink; V : in Positive) is
begin
O.Volume := O.Volume + V;
end Add;
procedure Spilt(O : in out Drink; V : in Positive) is
begin
O.Volume := O.Volume - V;
end Spilt;
function Get_Volume(O : in Drink) return Natural is
begin
return O.Volume;
end Get_Volume;
end Drinks;
-- Coofeesクラス
package Coffees is
type Coffee is new Drinks.Drink with private; -- やっぱりカプセル化
-- 豆の種類に関する型とサブプログラム。Coffeesクラス専用
type Beans_Type is (Blue_Mountain, Crystal_Brend, European_Brend);
procedure Set_Beans_Type(O : in out Coffee; T : in Beans_Type);
function Get_Beans_Type(O : in Coffee) return Beans_Type;
private
-- 拡張するインスタンス変数を定義
type Coffee is new Drinks.Drink with record
Beans : Beans_Type;
end record;
end Coffees;
-- 実装
package body Coffees is
procedure Set_Beans_Type(O : in out Coffee; T : in Beans_Type) is
begin
O.Beans := T;
end Set_Beans_Type;
function Get_Beans_Type(O : in Coffee) return Beans_Type is
begin
return O.Beans;
end Get_Beans_Type;
end Coffees;
Cup_C : Coffees.Coffee;
begin
Coffees.Add(Cup_C, 200);
Coffees.Set_Beans_Type(Cup_C, Coffees.Blue_Mountain);
put("Cup_C: "); Put(Coffees.Get_Volume(Cup_C)); New_Line;
Put(" "); Put(Coffees.Beans_Type'Image(Coffees.Get_Beans_Type(Cup_C))); New_Line;
end OOP2;
コード内の強調した部分を見れば分かるとおり、まず親クラス側では、
type Super_Record is tagged
record ...;
とtagged
を付けて型の宣言を行います。この宣言により、派生型を宣言する際に要素の拡張が可能となります。
さて、前回から(tagged) record
型をスペック内で宣言するときはprivate
として宣言してきました。インスタンス変数を隠蔽し、内部状態へはアクセサメソッドを経由してアクセスするという、カプセル化を意識した設計によるものです。コードを見れば分かるとおり、Adaにおけるインスタンスメソッドの可視性は全部見せるか全部隠すの二者択一となります。Javaの様に変数ごとの設定が出来ないため、やや不便ではありますが、一部だけpublic
にすることは余りないので実害は余りないでしょう。なお、この場合の可視性がJavaでいうprivate
になるかprotected
になるかはパッケージ構成によって異なります。詳しい話は後述しますが、上のコードのような親子関係のないパッケージ間ではprivateとして扱われます。つまり、サブクラスであるCoffees
パッケージ内からはスーパークラスであるDrinks.Drink
の要素については一切振れることが出来ません。
もう1つここでの注意点として、private
とlimited private
の使い分けがあります。この2つの型の違いは主に代入の可否と演算子(特に比較演算子)の使用の可否にあります。コードをみてみましょう。
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;
procedure Private_Test is
package Test is
type Foo is tagged limited private;
type Bar is tagged private;
procedure Set(O : out Foo; V : in Integer);
function Get(O : in Foo) return Integer;
procedure Set(O : out Bar; V : in Integer);
function Get(O : in Bar) return Integer;
private
-- 定義自体は同じ
type Foo is tagged limited record
P : Integer;
end record;
type Bar is tagged record
P : Integer;
end record;
end Test;
package body Test is
-- まったく同じメソッドを用意する
procedure Set(O : out Foo; V : in Integer) is
begin
O.P := V;
end Set;
function Get(O : in Foo) return Integer is
begin
return O.P;
end Get;
procedure Set(O : out Bar; V : in Integer) is
begin
O.P := V;
end Set;
function Get(O : in Bar) return Integer is
begin
return O.P;
end Get;
end Test;
X : Test.Foo;
Y : Test.Foo;
N : Test.Bar;
M : Test.Bar;
begin
Test.Set(X, 10);
Test.Set(Y, 20);
-- X := Y;
-- if Test."="(X, Y) then
-- Put("X = Y"); New_Line;
-- else
-- Put("X /= Y"); New_Line;
-- end if;
Test.Set(N, 10);
Test.Set(M, 20);
Put("N: "); Put(Test.Get(N)); New_Line;
Put("M: "); Put(Test.Get(M)); New_Line;
M := N;
Put("N: "); Put(Test.Get(N)); New_Line;
Put("M: "); Put(Test.Get(M)); New_Line;
if Test."="(N, M) then
Put("N = M"); New_Line;
else
Put("N /= M"); New_Line;
end if;
end Private_Test;
private
型であるM
とN
がそれぞれ代入や比較が可能なのに対し、limited private
型であるX
とY
は代入や比較が出来ません(コメントを外すとコンパイルエラーになります)。
さて、この2つの違いはOOPにおいてどのような差を生むでしょうか。まずは、代入について考えてみましょう。
Adaにおいて、変数間での(tagged) record
の代入は要素の値のコピーを意味します。とても当たり前のような感じがしますが、Javaの代入が基本的には参照のコピーであることを思い出してください。Javaの代入ではインスタンスそのものは2つに増えません(名前が2つに増えただけです)。対してAdaの代入はまるでJavaのclone()
メソッドを呼んだかのように、インスタンスそのものが2つに増えることになります。しかもこれはディープコピーです。
次に、比較について考えてみましょう。
これまたJavaとの比較になりますが、String
型を比較するときに使用するequals()
メソッドを思い出してください。これは該当インスタンスが同一でなくても、内容である文字列が同じならばtrue
を返すメソッドです。もし、内容ではなく、違う変数名を持つ同一のインスタンスかどうかを調べたい場合は、単純に比較演算子==
を使用して比較を行います。Adaの比較演算子はどちらのタイプでしょうか?
Adaおける比較演算子は――実はどちらでもありません。Adaにおいてはrecord
型の比較は全ての要素が=
ならばそのreocrd
も=
である、という仕組みで動作します。これはつまりインスタンスそれ自身の同一性には無頓着であるということを意味しますが、だからといってJavaのequals()
の様ほど中身を気にしてくれるかといえばそうでもありません。
たとえば何かの集合を配列で実装した場合、2つのインスタンスに含まれる何かが同じならば、配列中でどの位置に保存されていようと同じ集合であるとと見なしたいところです。このようなクラスの比較を行う場合、Adaの比較演算子は役に立ちません。配列の比較は同じ添字を持つ要素を順番に比較することで行われるので、順序が違うとその時点でfalse
となってしまうからです。こういった時、Javaならば==
演算子の使用が適切でないことがよく知られているのでequals()
メソッドが存在するかどうかを調べるのが自然です。しかしAdaの場合、Ada83の時代から比較演算子が普通に使われているという事情から、第三者が用意したクラスを使用するときなど、無意識のうちに単純な比較演算子で2つの集合を比較してしまうことが普通に起こり得ます
だんだんデスマス口調に疲れてきました。まぁとにかく、Adaの比較演算子は手続き型のプログラムを書くときには便利ですが、OOPで使用するといらぬ混乱を招きかねない、ということです。
このように、考え無しに増殖するインスタンスと意図しない比較による混乱を防止するために、初めのうちはlimited private
の使用をおすすめします。ただし、この後説明するaccess
型を使用したよりJavaに近いプログラミングスタイルを使用する場合、private
型で定義しておいた方が便利なこともあるため、一概には断定できないのが難しいところです。