MikoScript3 言語仕様
クラスとインスタンス
本言語は、オブジェクト指向プログラミングをサポートします。本章では、それに必要
な「クラス」と「インスタンス」について説明します。これらに関して、まずその概要を
この冒頭で述べ、詳細を以降の各節で述べます。
本言語でのクラスとインスタンスの基本概念は、一般的な通念と大差はありませんが、
その実現形態には、独自の手法を用いています。また、その機能には、他では見られない
特徴があります。
「クラス」は、「メンバー変数」と「メンバー関数」から構成されます。メンバー変数
は、そのクラスが有する内部データです。これは「プロパティー」とも呼ばれます。一方、
メンバー関数は、そのクラスに関連する各種の機能を提供する関数で、「メソッド」とも
呼ばれます。メンバー変数とメンバー関数を総称して、クラスの「メンバー」と言います。
本言語のクラスは、単なる雛型ではなく、それ自身が実体を持つ「オブジェクト」です。
これは、「複合箱」によって実現されています。そのため、本言語のクラスは、実行時に
動的に生成/破棄でき、また、関数の引数や返値にも成り得ます。
クラスは、別のクラスのメンバーを「継承」できます。継承されたメンバーは、それが
あたかも自分のメンバーであるかのようになります。メンバーの継承を受ける側のクラス
を「派生クラス」と言い、メンバーの継承を提供する側のクラスを「基底クラス」と言い
ます。つまり、派生クラスは、基底クラスのメンバーを継承しているということです。
クラスの継承は何段でも可能です。つまり、ある派生クラスの基底クラスは、別の基底
クラスの派生クラスであっても構いません。また、派生クラスの基底クラスは、複数あっ
ても構いません。派生クラスの基底クラスが、単一の場合、「単一継承」と言い、複数の
場合、「多重継承」または「複数継承」と言います。
本言語では、継承は、派生クラスに基底クラスのメンバーをコピーするのではなくて、
派生クラスに基底クラスのスコープを追加設定することによって実現しています。これは、
メモリーの浪費を回避し、実行時間を短縮します。
クラスから、そのクラスの「インスタンス」を生成できます。インスタンスとは、その
クラスが元になって生成されるオブジェクトです。「クラスのオブジェクト」という場合、
それは「クラスのインスタンス」を意味します。
本言語では、インスタンスは、そのクラスのメンバーのコピーを持つのではなく、その
クラスのメンバーを継承します。従って、クラスのメンバーは、各インスタンス間で共有
の実体となります。一方、各インスタンスが個別に持つメンバーは、そのインスタンスが
生成される時に、そのクラスの「コンストラクタ」によって設定されます。インスタンス
が一旦生成された後は、そのメンバー構成を独自に変更することもできます。
インスタンスが破棄される時、そのクラスに「デストラクタ」があれば、それが自動的
に呼び出されます。それによって、各インスタンス固有の終了処理を、行なわせることが
できます。
オブジェクトのメンバーは、「仮想」にもできます。仮想のメンバーは、コンパイル時
にその対象が決まるのでなく、実行時に動的に決められます。これによって、各種の処理
や機能設計においての共通化やパターン化が計れます。また、インターフェイスと実装の
分離も可能です。
本言語では、クラスのインスタンスだけではなく、クラス自身も、或いは、構造体も、
オブジェクトです。これは、「複合箱」によって実現されているので、このオブジェクト
のメンバーや継承関係は、固定的ではなく、実行時に動的に自由に変化できます。また、
このオブジェクトは、その他にも、箱としての各種の操作が可能です。
●クラスの設定(継承がない場合)
クラスは、通常、「クラス設定文」の実行によって、設定します。この構文の書式は、
継承がない場合、次のようになります。(継承がある場合は、次節で説明します。)
class クラス名
{
メンバー設定部
}
ここで、「クラス名」には、設定対象のクラスの名前を規定します。この名前は、その
クラスの複合箱を代表する最外の箱の名前です。これは、単一の識別名だけでなく、一般
に「箱の経路」を規定できます。この箱の経路の基点は、スコープ規定演算子によって、
規定できます。それは通常の箱名の場合と同じです。クラス名に連想名を含む場合、その
インデックスには変数が使えます。
この書式で、波括弧 { } で囲われたブロック内は、「メンバー設定部」になります。
ここには、そのクラスの
・メンバー変数を設定するための一連の実行文と
・メンバー関数の定義
を記述します。なお、両者は、必須というわけではなく、省略も可能です。
「メンバー設定部」のブロック内では、メンバースコープが「クラス名」で規定された
箱内直結のスコープに変わります。そのため、箱名の先頭に「.」のスコープ規定演算子
を付ければ、そのクラスのメンバーになります。箱名の先頭に、何もスコープ規定演算子
が付いていなければ、それは、ブロックの内外で共通のデフォールトスコープになるので、
注意が必要です。ブロックの内外で変わるのは、このメンバースコープだけで、それ以外
は、何も変わりません。
「メンバー設定部」で設定したメンバーは、そのクラスの各インスタンスに、継承され
ますが、各インスタンスにコピーされるわけではありません。そのため、ここで設定した
メンバーは、各インスタンスで共有のメンバーになります。一方、各インスタンスが個別
に所有するメンバーは、コンストラクタで設定します。この詳細は、後述します。
「クラス設定文」は、その末尾(波括弧のブロックの後)に、セミコロンは不要です。
但し、付けても、文法エラーにはなりません。
次に、「クラス設定文」で、クラスを設定する例を示します。
class S
{
.A = 1;
.B = 2;
function F() { print .A, .B; }
}
この実行によって、デフォールトスコープ(この場合、現実行関数のローカルスコープ)
内に、S という名前のクラスが生成され、それにメンバー変数 A, B とメンバー関数 F
が登録されます。メンバー変数 A, B はそれぞれ、整数値 1, 2 の初期値が設定されます。
このように、クラスのメンバー変数は、代入によって生成します。その際に、初期値も
設定できます。また、前章の構造体で説明したように、「入出力形式」の設定によっても、
メンバー変数を登録できます。また、システム組み込み後置型関数 'new! も使えます。
クラスのメンバー関数の登録は、通常の関数定義と同じ書式です。「クラス設定文」の
ブロック内の関数定義は、そのクラスのメンバー関数として登録されます。この場合に、
その関数名は、必ずそのクラスのメンバースコープになるものと解釈されます。そのため、
その関数名には、スコープ規定演算子を付ける必要はありません。もし付けるとしても、
「.」だけに限定されます。上例の function F で、スコープ規定演算子が付いていない
のは、そのためです。
クラスは、そのインスタンスを生成しなくても、それ自身で、オブジェクトになります。
通常、そのクラスのオブジェクトが1つだけの場合には、そのインスタンスを、わざわざ
生成せずに、そのクラス自身を、オブジェクトとして使います。
クラスは、複合箱なので、そのメンバーには、箱の中の箱として、アクセスできます。
例えば、上例のクラス S の各メンバーは、S.X, S.Y. S.F として、アクセスできます。
一般に、関数実行時のメンバースコープは、その関数が検索されたスコープになります。
例えば、上例のクラス S のメンバー関数 F が、
S.F();
として、実行される時は、クラス S 内で、関数 F が検索されるので、この関数の実行時
のメンバースコープは、クラス S 内になります。この関数 F では、
print .A, .B;
を、実行していますが、ここで、.A と .B は、従って、クラス S 内のメンバー A と B
を対象とすることになります。なお、この実行では、次の通りプリントされます。
1, 2
クラスのメンバーの値は、「クラス設定文」の実行後にも、変更できます。例えば、
S.A = "Hello";
S.B = "class object!";
S.F();
を実行すると、次の通りプリントされます。
Hello, class object!
クラスのメンバーは、「クラス設定文」の実行後にも、追加/削除できます。例えば、
S.C = "メンバー変数 C を追加";
delete S.B; // メンバー変数 B を削除
とすれば、クラス S 内のメンバーは、A, F, C になります。
また、次のように、クラス S 自体を削除することもできます。
delete S;
その他にも、複合箱としての各種の操作が可能です。
クラスのメンバー関数の定義は、「クラス設定文」のブロックの外側に置くこともでき
ます。これは、メンバー関数の定義が長くなる場合には、有用です。上例のクラス S の
「クラス設定文」を、この書き方にすると、次のようになります。
class S
{
.A = 1;
.B = 2;
}
S.F = function () { print .A, .B; };
ここでは、クラスのメンバー S.F に匿名関数を代入することによって、登録しています。
これは、結局、上述の「クラス設定文」実行後のメンバーの追加に、該当します。
この例では、匿名関数を使いましたが、これを、通常の関数定義の書き方で、
function S.F() { print .A, .B; }
とすると、関数 S.F は、クラス S 内には登録されません。なぜなら、クラス S は現在、
現実行中の関数のローカルスコープにありますが、この通常の関数定義では、モジュール
ローカルスコープに登録されてしまうからです。もし、クラス S の所在が、モジュール
ローカルスコープ内であれば、これで問題なく登録されます。むしろ、この書き方の方が
自然でしょう。但し、通常の関数定義は、入れ子にできないので、この書き方のメンバー
関数定義は、関数定義の外側(暗黙のメイン関数直属)に置く必要があります。
クラスのメンバー関数の定義は、別のソースファイルに記述することもできます。特に
長大なクラスでは、その必要性があります。このやり方は、基本的に、「クラス設定文」
のブロックの外側で、クラスのメンバーの追加を行なうのと、同じことです。
例えば、何かのモジュールをロードして、グローバルスコープに ::SomeClass という
クラスが生成されていたとします。これに SomeFunc というメンバー関数を追加するには、
次のようにするだけです。
function ::SomeClass.SomeFunc( ・・・ )
{
・・・・・
}
上述のクラス S と同じ構造の複合箱は、「クラス設定文」を使わずに、次のように、
生成することもできます。
S.A = 1;
S.B = 2;
S.F = function() { print .A, .B; };
しかしながら、「クラス設定文」を使わずに生成した複合箱には、クラスとしての属性が
設定されません。そのため、クラスとしての機能は担えません。複合箱を、クラスとして
構築するには、通常、「クラス設定文」を実行する必要があります。
「クラス設定文」のメンバーは、既存のクラスや構造体を使って、設定することができ
ます。例えば、上例のクラス S を使って、新規のクラス T を設定するには、
class ^T
{
.X = S; // メンバー X に既存クラス S を代入
.Y = 3;
function Z() { print .X.A, .X.B, .Y; }
}
とします。この実行で、モジュールローカルスコープ内に、T という名前のクラスが生成
され、それに、メンバー変数 X, Y とメンバー関数 Z が登録されます。メンバー変数 X
には、クラス S のコピーが代入されているので、T.X.A, T.X.B には それぞれ、整数値
1, 2 が入り、T.X.F は、クラス T.X のメンバー関数になっています。また、T.Y には、
3 が入ります。
ちなみに、この例で、メンバー関数 Z の定義を「クラス設定文」の外側に置く場合は、
次のようになります。
class ^T
{
.X = S; // メンバー X に既存クラス S を代入
.Y = 3;
}
function ^T.F() { print .X.A, .X.B, .Y; }
この例の場合、クラス T の格納スコープと、関数のデフォールトの登録スコープが同じ
モジュールローカルスコープなので、
function T.F() { print .X.A, .X.B, .Y; }
としても構いません。また、
^T.F = function () { print .X.A, .X.B, .Y; };
のように、匿名関数にして代入することもできます。
「クラス設定文」は、入れ子にできます。つまり、「クラス設定文」の中で、さらに、
「クラス設定文」を記述することができます。次にこの例を示します。
class ::R
{
class P
{
.A = 1;
.B = 2;
}
.Q = 0;
}
これを実行すると、グローバルスコープ内に、クラス R が生成され、そのメンバーが、
箱 P と箱 Q になります。箱 P は、クラスで、そのメンバーが、箱 A と箱 B です。
「クラス設定文」が、入れ子になっている場合、内側の「クラス設定文」の「クラス名」
のスコープは、必ず、外側の「クラス設定文」の「クラス名」のメンバースコープになる
ものと解釈されます。そのため、内側の「クラス設定文」の「クラス名」には、スコープ
規定演算子を付ける必要はありません。もし付けるとしても、「.」だけに限定されます。
上例の class P で、スコープ規定演算子が付いていないのは、そのためです。
「メンバー設定部」には、任意の実行文が記述できますが、ラベルは設定できません。
次に、「メンバー設定部」に、for 文を使った例を示します。
class ::G
{
for( i = 0 ; i < 10 ; i++ )
.A[i] = 0;
}
これを実行すると、グローバルスコープ内に、クラス G が生成され、そのメンバーには、
連想配列 A[0], A[1] ... A[9] が、登録されます。なお、ここで、使用している変数 i
には、スコープ規定演算子が付いていないので、変数 i は、デフォールトスコープ内の
変数になります。決して、クラスのメンバーではありません。
今までの例では、「クラス設定文」の実行時点で、「クラス名」の対象となる箱が存在
していませんでした。その場合、既に述べた通り、そのクラス箱は、新規に生成されます。
「クラス設定文」の実行時点で、「クラス名」の対象となる箱が存在している場合、事情
が若干異なります。まず、「クラス名」の対象の箱が「箱を入れる箱」でない場合、その
箱は、無条件に、空の「箱を入れる箱」に変更されてから、新規生成された場合と同様の
クラス設定が行なわれます。一方、その対象の箱が「箱を入れる箱」の場合、その箱には
無条件にクラスとしての属性が設定され、クラスのメンバー設定は、現状への追加になり
ます。例えば、前述のクラス S が、前述の通り、既に設定されている状態で、
class S
{
.C = 3;
.D = 4;
}
を実行すると、クラス S のメンバーに、箱 C と箱 D が追加されます。もともとあった
箱 A と箱 B と箱 F は、そのまま存続しています。なお、この場合、既にクラスとして
存在している箱 S へのメンバーの追加なので、次のようにするのと同じです。
S.C = 3;
S.D = 4;
これは、また、次のようにすることもできます。
scope S
{
.C = 3;
.D = 4;
}
クラス内の各メンバーは、生成された順に登録されていきます。これは、クラスの箱に
限ったことではなく、一般の「箱を入れる箱」の場合でも同様です。この原則によって、
例えば、上述のクラス S 内の各メンバーの登録順は、箱 A, B, F, C, D となります。
本言語では、クラス内のメンバーの隠蔽やアクセス制限の機構はありません。これらは、
メンバー名に慣習上の規約を設けることによって、補完することになります。たとえば、
アンダースコア(_)で始まるメンバー名には、クラスのメンバー関数以外アクセスしては
いけないという取り決めにしておくとかです。
一方、本言語では、文法上、メンバー関数で、そのクラス/インスタンスのメンバーに
アクセスする時には、メンバー名の先頭にピリオッド(.)のスコープ規定演算子を付ける
必要があります。そのため、ある識別子がメンバーか否かを識別するための特別な慣習上
の規約は不要です。例えば、メンバー名には必ず、m_ で始まる名前を使うというような
コーディング規約を設ける必要は全くありません。
●クラスの設定(単一継承の場合)
単一継承の場合の「クラス設定文」の書式は、次のようになります。
class クラス名 : 基底クラス名
{
メンバー設定部
}
ここで、「クラス名」と「メンバー設定部」は、継承がない場合の「クラス設定文」と
同じです。「基底クラス名」には、設定対象のクラスが継承する基底クラスの名前を指定
します。
次に、単一継承の場合のクラス設定例を示します。ここでは、基底クラス A とその
メンバーを継承する派生クラス B を生成しています。
class A // 基底クラス A
{
.X = 1;
function F() { print "F(): X=" : .X; }
}
class B : A // 派生クラス B
{
.Y = 2;
function G() { print "G(): X=" : .X, "Y=" : .Y; }
}
派生クラス B では、基底クラス A のメンバーを継承しているので、 派生クラス B から
基底クラス A のメンバー X と F に、B.X と B.F() として、アクセスできます。これは、
派生クラス B 内に基底クラス A のメンバー X と F がコピーされて来たのではなくて、
継承によって、派生クラス B 内に基底クラス A 内のスコープが追加設定されたためです。
スコープの追加に関しては、「スコープ」の章の
・「箱を入れる箱」内のスコープの追加
の節で説明していますが、クラスの継承によるスコープの追加も、内部の原理は同じです。
ちなみに、クラスの継承をメンバーのコピーで行なったとすると、メモリー使用量が増え、
実行時間が長くなってしまいます。
派生クラス B のメンバー関数 G は、メンバー X とメンバー Y とをプリントします。
ここで、メンバー X は、基底クラス A から継承しているメンバーで、メンバー Y は、
自分自身のメンバーになります。そのため、
B.G();
を実行すると、次の通り、プリントされます。
G(): X=1, Y=2
クラス B のメンバーを継承する派生クラス C は、次のように生成します。
class C : B // 派生クラス C
{
.Z = 3;
function H() { print "H(): X=" : .X, "Y=" : .Y, "Z=" : .Z; }
}
前例では、クラス B は、クラス A の派生クラスでしたが、ここでは、クラス C の基底
クラスになっています。このような継承の継承は何段でも可能です。しかし、あまりにも
段数の多い継承は、推奨しかねます。継承の段数は、できるだけ少なくするのが鉄則です。
派生クラス C は、クラス B を継承して、クラス B は、クラス A を継承しているので、
クラス C は、クラス A を継承することになります。そのため、
C.H();
を実行すると、次の通り、プリントされます。
H(): X=1, Y=2, Z=3
ここで、
メンバー X は、クラス A から継承しているメンバーで、
メンバー Y は、クラス B から継承しているメンバーで、
メンバー Z は、自分自身のメンバーになります。
継承は、コピーではなく、スコープの追加なので、基底クラスのメンバーは、それから
派生した全クラスにとって、共有のメンバーになります。例えば、
A.X = 5;
B.G();
C.H();
を実行した場合、次の通り、プリントされます。
G(): X=5, Y=2
H(): X=5, Y=2, Z=3
派生クラスに、基底クラスと同じ名前のメンバーがあると、派生クラスの方が優先され
ます。例えば、
class T : A // 派生クラス T
{
.X = 100;
}
T.F();
の実行では、メンバー X は、基底クラス A のメンバーではなく、自分自身のメンバーに
なるので、次の通り、プリントされます。
F(): X=100
派生クラスを設定する時、その基底クラスは、それよりも前に、設定されている必要が
あります。例えば、上例の基底クラス A と派生クラス B の設定順序を逆にすると、例外
が発生します。ただし、空の基底クラスを、とりえあず、派生クラスの設定前に生成して
おけば、基底クラスの具体的なメンバーの設定は、派生クラスの設定の後でも構いません。
例えば、次のようにできます。
class A {} // 空の基底クラス A を生成
class B : A // 基底クラス A を継承する派生クラス B を生成
{
.Y = 2;
function G() { print "G(): X=" : .X, "Y=" : .Y; }
}
class A // 基底クラス A のメンバーを設定(空のクラスに追加)
{
.X = 1;
function F() { print "F(): X=" : .X; }
}
●クラスの設定(多重継承の場合)
多重継承の場合の「クラス設定文」の書式は、次のようになります。
class クラス名 : 基底クラス名の並び
{
メンバー設定部
}
ここで、「クラス名」と「メンバー設定部」は、今までの「クラス設定文」と同じです。
「基底クラス名の並び」には、設定対象のクラスが継承する各基底クラスの名前をコンマ
で区切って指定します。
次に、多重継承の場合のクラス設定例を示します。ここでは、基底クラス A, B と、
それらを継承する派生クラス C を生成しています。
class A // 基底クラス A
{
.X = 1;
function F() { print "F(): X=" : .X; }
}
class B // 基底クラス B
{
.Y = 2;
function G() { print "G(): Y=" : .Y; }
}
class C : A, B // 多重継承の派生クラス C
{
.Z = 3;
function H() { print "H(): X=" : .X, "Y=" : .Y, "Z=" : .Z; }
}
この継承関係を図示すると次のようになります。
C ─┬→ A
└→ B
派生クラス C では、基底クラス A と B のメンバーを継承しているので、基底クラス A
のメンバー X と F に、C.X と C.F() として、アクセスできて、かつ、基底クラス B の
メンバー Y と G にも、C.Y と C.G() としてアクセスできます。そのため、
C.H();
を実行すると、次の通り、プリントされます。
H(): X=1, Y=2, Z=3
多重継承でも、勿論、多段の継承が可能です。例えば、上例の続きで、以下を実行する
と、派生クラス E は、多段の多重継承になります。
class D // 基底クラス D
{
.X = 10;
}
class E : C, D // 多段多重継承の派生クラス E
{
.X = 100;
}
この継承関係を図示すると次のようになります。
E ─┬→ C ─┬→ A
│ └→ B
└→ D
一般に、クラスのメンバーは、次に述べる手順で検索されて、最初に見つかったものが、
採用されます。まず、そのクラス内で検索されます。そこになけれが、次に、そのクラス
直結の各基底クラスが、登録順に、検索の対象になります。各基底クラスの検索において
も同様に、まず、そのクラス内で検索されます。そこになけれが、次に、そのクラス直結
の各基底クラスが、登録順に、検索の対象になります。例えば、上例の場合、
E → C → A → B → D
の検索順になります。そのため、
E.H();
を実行すると、次の通り、プリントされます。
H(): X=100, Y=2, Z=3
多重継承では、同じ基底クラスを重複して継承してしまう場合があります。たとえば、
上例のクラス D と E の代わりに、以下を実行すると、派生クラス E は、基底クラス A
を重複して継承している状態になります。
class D : A // 派生クラス D
{
}
class E : C, D // 重複多段多重継承の派生クラス E
{
}
この継承関係を図示すると次のようになります。
E ─┬→ C ─┬→ A
│ └→ B
└→ D ──→ A
クラスのメンバーの検索においては、既に検索済みのクラスが、再度検索されることは
ありません。もし、この原則がなければ、この例の場合のクラスの検索順は、
E → C → A → B → D → A
となりますが、この原則によって、クラス A の2回目の検索は回避されるので、実際は、
E → C → A → B → D
の検索順になります。
多重継承では、継承がループしてしまう場合があります。この継承関係を図示すれば、
例えば、次のようになります。
┌──────┐
↓ │
E ─┬→ C ─┬→ A │
│ └→ B ─┘
└→ D ──→ A
このような場合でも、クラスのメンバーの検索は、上記の原則に準拠して行なわれます。
そのため、この例の場合のクラスの検索順は、 E → C → A → B → D になります。
●インスタンスの生成とコンストラクタ
「クラスのインスタンスを生成する式」は、次のような形式になります。
クラス名( 引数の並び )
ここで、「クラス名」には、対象のクラスを特定する名前を規定します。これは、単一
の識別名だけでなく、一般に「箱の経路」を規定できます。この「クラス名」に連想名が
ある場合、そのインデックスには、変数が使えます。これらの原則は、通常の箱名を規定
する場合と同様です。「クラス名」は、対象のクラスへの参照であっても構いません。
「引数の並び」は、そのクラスのコンストラクタの呼び出しの際に渡されるものです。
これに関しては、後で説明します。
この形式は、基本的に、関数コール、または、純粋配列の要素指定の形式と同じです。
この3者を区別するのは、丸括弧演算子( ) の左項の箱の中身の種別です。
「クラスのインスタンスを生成する式」は、通常、次のどれかで使います。
・代入文の右辺
・関数の引数
・関数の返値( retuen 文内 )
・連想配列のデータ設定ブロック内の要素
このうち、本節でまず、代入文の右辺で使う場合を説明します。他の場合は、後に別節で
説明します。
「クラスのインスタンスを生成する式」を、代入文の右辺で使う場合、次のような形式
になります。
代入先 = クラス名( 引数の並び );
この文を実行すると、「クラス名」に対応するクラスのインスタンスが、まず、現実行中
の関数のローカルスコープ内に、一時的に生成されます。この「一時インスタンス」には、
そのクラスへのスコープが設定されています。これによって、そのクラス内のメンバーを
継承していますが、それ以外中身はありません。次に、そのクラスに、コンストラクタが
定義されていれば、それが自動的にコールされます。この時、「クラスのインスタンスを
生成する式」での「引数の並び」が、コンストラクタに渡されます。コンストラクタは、
この一時インスタンスに対して、初期設定を行ないます。その後、この一時インスタンス
の実体は、「代入先」へ移動されます。従って、その「代入先」に格納されるのは、その
インスタンスへの参照ではなく、その実体そのものです。なお、この一時インスタンスの
代入においては、:= や <- の代入演算子も使えますが、その結果は同じになります。
「コンストラクタ」は、クラスのメンバー関数として、
Construct
という専用の名前で定義します。この名前は、それ以外のメンバー名には、使えません。
次に、インスタンスの生成とコンストラクタの例を示します。ここでは、以下のように、
基底クラス B とその派生クラス C の2つのクラスがあります。
class B
{
.X = 1;
}
class C : B
{
.Y = 2;
function Construct( v ) { .Z = v; }
function Show() { print "X=" : .X, "Y=" : .Y, "Z=" : .Z; }
}
クラス C のインスタンスを生成するには、例えば、次のようにします。
G = C( 3 );
これを実行すると、クラス C のインスタンスが G に格納されます。この過程をもう少し
詳細に述べると、次のようになります。まず、クラス C の一時インスタンスが、現実行
中の関数のローカルスコープ内に生成されます。この一時インスタンスは、クラス C 内
のメンバーを継承していますが、実質的なメンバーはまだ不在です。次に、クラス C の
コンストラクタが、自動的にコールされます。この時、このコンストラクタには、引数と
して、3 が渡されます。コンストラクタは、この引数を、対象の一時インスタンスの Z
というメンバーに代入します。この時点で、このメンバーは存在していないので、これは
このインスタンス内に新規生成されることになります。その後、この一時インスタンスの
実体は、代入先の G へ移動されます。
このように生成されたインスタンス G は、クラス C を継承しているので、クラス C の
メンバー関数 Show を適用できます。これを、
G.Show();
のように実行すると、次の通りプリントされます。
X=1, Y=2, Z=3
ここで、メンバー X と Y は、インスタンス G 内にあるわけではなく、メンバー X は、
クラス B 内にあり、メンバー Y は、クラス C 内にあります。そして、メンバー Z は、
インスタンス G 内にあります。次に、
H = C( 4 );
を実行すると、クラス C のインスタンスがもう1つ H に生成されます。これで、
H.Show();
を実行すると、次の通りプリントされます。
X=1, Y=2, Z=4
ここで、メンバー X, Y, Z は、それぞれ、クラス B, クラス C, インスタンス H 内に、
あるものです。
このように、同じクラスから生成された各インスタンスにとって、そのクラス、または
その基底クラスにあるメンバーの実体は、「共有メンバー」になります。これに関しては、
本章の別節で詳説します。
同じクラスから生成した各インスタンスは、必ずしも、同じメンバー構成になる必要は
ありません。各インスタンス個別に「固有メンバー」を持たせることもできます。これに
関しては、本章の別節で詳説します。
ところで、クラス C のメンバー関数 Show() では、Z というメンバーが、使用されて
いますが、これは、このクラスのスコープ内には、存在しません。ところが、この関数の
実行時のメンバースコープ内には、存在します。このようなメンバーを「仮想メンバー」
と言います。これに関しては、本章の別節で詳説します。
インスタンスの直結クラス、つまり、そのインスタンスが直接継承しているクラスは、
その生成の元になったクラスです。上例では、インスタンス G の直結クラスは、クラス
C です。この直結クラスは、インスタンスの生成直後は、1つだけですが、後で追加する
こともできます。これに関しては、本章の別節で詳説します。
インスタンスのメンバーの検索は、クラスのメンバーの検索順序と同様に、次の手順で
検索されます。その際、最初に見つかったものが採用されます。まず、そのインスタンス
内で検索されて、そこになけれが、そのインスタンス直結の各クラスが、登録順に、検索
の対象になります。この各クラスの検索においては、まず、そのクラス内が検索されて、
そこになければ、そのクラスの各基底クラスが、登録順に、検索の対象になります。この
各基底クラスにおいても、同様に検索されます。各クラスの検索において、既に検索済み
のクラスが、再度検索されることはありません。
このようにインスタンスのメンバーが検索されるので、裏に隠れた同名のメンバーには、
通常、アクセスされません。しかし、基底クラスのコンストラクタを呼び出したい場合等
もあるので、このアクセス方法が提供されています。これに関しては、本章の別節で詳説
します。
コンストラクタが定義されているクラスのインスタンスが生成されるときに、自動的に
呼び出されるのは、そのクラスのコンストラクタだけです。そのクラスに、基底クラスが
あり、それにコンストラクタが定義されていても、それは自動的には呼び出されません。
その呼び出しが必要な場合には、プログラムでその実行を明記することになります。これ
に関しては、本章の別節で詳説します。
一方、コンストラクタが定義されていないクラスでは、前述の「一時インスタンス」に
対する初期設定が行なわれずに、それがそのまま、正規のインスタンスになります。この
場合、そのインスタンスは、そのクラスへ設定されたスコープによって、そのメンバーを
継承していますが、それ以外中身は何もありません。これは、かなり軽量な存在です。
●インスタンスの破棄とデストラクタ
インスタンスは、「delete 文」で、明示的に直接削除(破棄)できます。この書式は、
次のようになります。
delete インスタンス名の並び;
ここで、「インスタンス名の並び」は、1つの「インスタンス名」か、または、コンマで
区切られた複数の「インスタンス名」になります。「インスタンス名」は、単一の識別名
だけでなく、一般には「箱の経路」になります。この「インスタンス名」に連想名がある
場合、そのインデックスには、変数が使えます。これらの原則は、通常の箱名を規定する
場合と同様です。「インスタンス名」は、削除対象のインスタンスへの参照でも可能です。
例えば、前述のインスタンス G を削除するには、次のようにします。
delete G;
また、前述のインスタンス G と H をまとめて削除するには、次のようにします。
delete G, H;
インスタンスが、構造体のメンバーや連想配列の要素になっている場合、その構造体や
連想配列が、「delete 文」で削除されると、その中にあるインスタンスも破棄されます。
インスタンスは、それが属するシステムスコープが消滅する際に、自動的に(暗黙的に)
破棄されます。システムスコープが消滅するタイミングは、「スコープ」の章で詳説して
いますが、次の通りです。
・関数ローカルスコープは、その関数がリターンする時
・モジュールローカルスコープは、そのモジュールが除去される時
・関数スタティックスコープは、その関数が定義されたモジュールが除去される時
・スレッドローカルスコープは、そのスレッドが終了する時
これらのシステムスコープの消滅に伴って、その中にあるインスタンスは、自動的に破棄
されます。
なお、グローバルスコープは、本言語システムの終了時に消滅しますが、その時点では、
スクリプトの全実行スレッドが終了しているので、グローバルスコープ内のインスタンス
は、現状、最後まで破棄されません。
以上述べたインスタンスの破棄の場合では、そのインスタンスを構成する複合箱自身の
存在が無くなります。一方、インスタンスを構成する複合箱に、別のデータが代入される
場合や、その箱の中身が空にされる場合には、そのインスタンスは無くなりますが、その
箱自身は存続しています。この後者の場合を、「インスタンスの消失」ということにして、
前者の「インスタンスの破棄」の場合と区別します。
インスタンスのクラスに「デストラクタ」が定義されていると、そのデストラクタは、
そのインスタンスが破棄される時に、自動的に呼び出されます。しかし、インスタンスが
消失する時には、現状、デストラクタは呼び出されません。
「デストラクタ」は、クラスのメンバー関数として、
Destruct
という専用の名前で定義します。この名前は、それ以外のメンバー名には、使えません。
デストラクタでは通常、破棄されるインスタンスの終了処理を行ないます。デストラクタ
関数には、引数はありません。
次に、インスタンスの破棄とデストラクタの例を示します。ここでは、以下のように、
クラス C のインスタンス G を生成して、すぐに削除しています。
class C
{
function Destruct() { print "破棄対象: " : this'name; }
}
G = C();
delete G;
print "プログラム終了";
これを実行すると、次の通りプリントされます。
破棄対象: G
プログラム終了
ここで、this は、現メンバースコープを指す予約語です。この例では、デストラクタ
内で使われていますが、この場合、this が指すのは、現破棄対象のインスタンスです。
上例で、delete G; の行をなくして(或いはコメントにして)、実行すると、今度は、
次の通りプリントされます。
プログラム終了
破棄対象: G
この経緯は、次の通りです。インスタンス G は、現実行中の関数(この場合は、暗黙
のメイン関数)のローカルスコープに作成されています。この関数がリターンする時に、
そのローカルスコープが消滅するので、それに伴って、その中にあるインスタンス G も
自動的に破棄されます。この破棄の時に、そのクラスのデストラクタがコールされます。
なお、このローカルスコープが実際に消滅するのは、インスタンスの破棄が終ってからに
なるため、このデストラクタの実行中は、まだ、クラス C は存在しています。
今度は、関数ローカルスコープ以外のシステムスコープ内にあるインスタンスが破棄さ
れる時に、デストラクタが呼ばれる例を示します。ここで、クラス C は、前と同じ内容
ですが、グローバルスコープに登録しています。そうしないと、前のままでは、ここでの
各インスタンスが破棄される時には、暗黙のメイン関数を抜けているので、そのローカル
スコープが既に消滅していて、その中のクラスも無くなっているからです。
class ::C { function Destruct() { print "破棄対象: " : this'name; } }
@P = ::C(); // インスタンス P を 関数スタティックスコープ内に生成
^Q = ::C(); // インスタンス Q を モジュールローカルスコープ内に生成
$R = ::C(); // インスタンス R を スレッドローカルスコープ内に生成
print "プログラム終了";
これを実行すると、次の通りプリントされます。
プログラム終了
破棄対象: P
破棄対象: Q
破棄対象: R
ここで、システムスコープは、関数スタティックスコープ、モジュールローカルスコープ、
スレッドローカルスコープの順に、消滅していっています。
スコープの消滅に伴って、その中にあるインスタンスが破棄される順は、同じスコープ
の階層では、生成順のちょうど逆になります。また、現破棄中のインスタンスの中にある
インスタンスの破棄は、その外にあるインスタンスよりも優先されます。
例えば、次のように、前と同じクラス C を使って、現実行関数のローカルスコープ
内に、インスタンス X, Y, Z と、インスタンス Y の中にインスタンス U を、この順で
生成して、現実行関数(暗黙のメイン関数)を抜けたとします。
class ::C { function Destruct() { print "破棄対象: " : this'name; } }
X = C();
Y = C();
Z = C();
Y.U = C(); // インスタンス Y の中にインスタンス U を生成
すると、次の通りプリントされます。
破棄対象: Z
破棄対象: U
破棄対象: Y
破棄対象: X
これは、暗黙のメイン関数を抜ける時、そのローカルスコープの消滅に際して、その中に
あるインスタンス X, Y, Z とインスタンス X 内のインスタンス U の各デストラクタが、
上述の順で呼び出されて、そこでプリントされた結果です。
インスタンスの破棄で自動的に呼ばれるのは、そのクラスのデストラクタだけで、その
クラスの基底クラスにデストラクタがあっても、自動的には呼び出されません。その呼び
出しが必要な場合には、プログラムでその実行を明記することになります。これに関して
は、本章の別節で詳説します。
クラスを破棄しても、そのデストラクタは呼び出されません。例えば、
delete C;
として、前記のクラス C を破棄しても、そのデストラクタ C.Destruct() は、呼び出さ
れません。
●共有メンバー
同じクラスから生成された各インスタンスにとって、そのクラスと、その基底クラスの
メンバーは、「共有メンバー」になります。また、同じ基底クラスから派生した各クラス
にとって、その基底クラスのメンバーは、「共有メンバー」になります。
例えば、次のように、基底クラス B とその派生クラス C, D があって、クラス C の
インスタンス G1 と G2 を生成したとします。
class B
{
.X = 1;
function Show()
{ print this'name : ": X=" : .X, "Y=" : .Y, "Z=" : .Z; }
}
class C : B
{
.Y = 2;
.Z = 3;
function Construct( v ) { .Z = v; }
function SetX( v ) { .X = v; }
function SetY( v ) { .Y = v; }
}
class D : B
{
.U = 4;
}
G1 = C( 5 );
G2 = C( 6 );
ここで、G1.X, G2.X, C.X, D.X は、B.X と同じ実体になります。また、G1.Y と G2.Y は、
C.Y と同じ実体になります。この共有されている実体 B.X と C.Y の値を変えると、それ
を共有している全メンバーに影響します。例えば、
B.X = 10; // 基底クラスの B.X は、インスタンス G1 と G2 の共有メンバー
C.Y = 20; // 派生クラスの C.Y は、インスタンス G1 と G2 の共有メンバー
C.Show();
G1.Show();
G2.Show();
を実行すると、次の通り、プリントされます。
C: X=10, Y=20, Z=3
G1: X=10, Y=20, Z=5
G2: X=10, Y=20, Z=6
インスタンスやクラスに適用されるメンバー関数からは、「共有メンバー」に代入でき
ません。例えば、
G1.SetX( 100 );
を実行すると、インスタンス G1 の直接のメンバー X が新規に生成されて、それに 100
が代入されます。そのため、
G1.Show();
G2.Show();
を実行すると、次の通りプリントされます。
G1: X=100, Y=20, Z=5
G2: X=10, Y=20, Z=6
ちなみに、この後に、
delete G1.X; // インスタンス G1 の直接のメンバー X を削除
G1.Show();
G2.Show();
を実行すると、次の通りプリントされます。
G1: X=10, Y=20, Z=5
G2: X=10, Y=20, Z=6
なお、この後さらに、
delete G1.X;
を実行すると、今度は、共有メンバーの X ( つまり、B.X )が削除されます。
●固有メンバー
同じクラスから生成された各インスタンスは、それぞれ個別に「固有メンバー」を持つ
ことができます。また、この各インスタンスは、必ずしも、同じメンバー構成にしておく
必要はありません。例えば、
class C
{
.X = 1;
.Y = 2;
function Construct( v ) { .Z = v; }
}
G1 = C( "**G1**" );
G2 = C( "**G2**" );
を実行すると、クラス C のインスタンス G1, G2 が生成されますが、ここで、
G1.Z は、G1 の固有メンバーで、
G2.Z は、G2 の固有メンバーです。
一方、
G1.X と G2.X は、C.X を共有するメンバーで、
G1.Y と G2.Y は、C.Y を共有するメンバーです。
次に、例えば、次のようにして、インスタンス G1 と G2 とで異なる固有メンバーを
追加すると、これらは、異なるメンバー構成になります。
G1.U = "G1 の固有メンバー U";
G2.V = "G2 の固有メンバー V";
また、次のように、インスタンス G1 と G2 に、同名の異なる固有メンバー関数を、
追加することもできます。
G1.F = function( v ) { print "G1: X=" : .X, "Y=" : .Y, "Z=" : .Z, v; };
G2.F = function( v ) { print "G2: X=" : .X, "Y=" : .Y, "Z=" : .Z, v; };
この例では、両者とも同じような処理内容ですが、勿論、全然違う処理内容にすることも
できます。これらは、単純には、次のように実行できます。
G1.F( 100 );
G2.F( 200 );
また、次のように、上記のクラス C に、ExecFunc というメンバー関数を追加してから、
これを使って実行することもできます。
C.ExecFunc = function( v ) { .F( v ); };
G1.ExecFunc( 100 );
G2.ExecFunc( 200 );
また、次のように、ObjFunc という関数を定義してから、これを使って実行することも
できます。
function ObjFunc( obj, v ) { obj.F( v ); }
ObjFunc( G1, 100 );
ObjFunc( G2, 200 );
いずれの場合でも、その結果は、次の通り、プリントされます。
G1: X=1, Y=2, Z=**G1**, 100
G2: X=1, Y=2, Z=**G2**, 200
●仮想メンバー
メンバー関数のメンバースコープは、その関数が実行される時点で、それが対象とする
オブジェクト内のスコープに、動的に変わります。そのため、同じメンバー関数内の同じ
名前のメンバーでも、実際の対象は、同じではありません。その時点で対象となっている
オブジェクト内の当該名のメンバーが対象になります。このメンバーは、その元のクラス
に存在していなくても、実行時点でそのメンバースコープに存在していれば、それが実際
の対象になります。このようなメンバーを「仮想メンバー」と言います。例えば、
class S
{
.X = 1;
function F1() { print "X=" : .X, "Y=" : .Y; }
function F2() { print .H(); }
}
では、クラス S 内には、Y というメンバー変数はありませんが、これは、メンバー関数
F1 内で使われています。また、クラス S 内には、H というメンバー関数もありませんが、
これは、メンバー関数 F2 内で使われています。次に、これらの仮想メンバーを、次の
ように、クラス S の派生クラス T で実装することにします。
class T : S
{
.Y = 2;
function H() { return "*** T.H() ***"; }
}
このクラス T のインスタンスを生成して、それにメンバー関数 F1 と F2 を適用すると、
A = T();
A.F1();
A.F2();
となります。ここで、メンバー関数 F1, F2 がコールされた時、そのメンバースコープは、
インスタンス A 内になるので、そこでは、仮想メンバー Y と H の対象が確定します。
そのため、これを実行すると、次のようにプリントされます。
X=1, Y=2
*** T.H() ***
今度は、クラス S の仮想メンバー Y, H を、次のように、クラス S のインスタンス
B で実装する場合を示します。
B = S();
B.Y = "*** B.Y ***";
B.H = function() { return "*** B.H() ***"; };
B.F1();
B.F2();
ここで、メンバー関数 F1, F2 がコールされた時、そのメンバースコープは、クラス S
のインスタンス B 内になります。この時点で、その中には、既に、メンバー Y と H が
設定されているので、クラス S の仮想メンバー Y, H の対象は、これらに確定します。
そのため、これを実行すると、次のようにプリントされます。
X=1, Y=*** B.Y ***
*** B.H() ***
仮想メンバーを活用する形態には、他にもいろいろあります。例えば、前節でも、仮想
メンバーを使っていましたし、また、次節で説明する「継承の追加と削除」を利用すれば、
その応用範囲は広がります。
●継承の追加と削除
クラス間の継承関係は普通、前述のように、クラスの設定時に指定しますが、本言語
では、その形態だけでなく、随時任意に追加/削除できるようになっています。さらに、
インスタンスにおいても、その生成後に、そのクラスの継承関係を自由に変更できます。
それには、システム組み込みのリレー型関数 'inherit と 'disherit を使います。
なお、'AddScope と 'DelScope を使っても構いませんが、注意が必要です。ちなみに、
これらに関する一般的な説明は、「スコープ」の章の以下の節で述べています。
・スコープの追加
・スコープの削除
クラスの継承の追加と削除を、随時動的に行なう例を以下に示します。ここでは、
次のような簡単な3つのクラス A, B, C があるとします。
class A { .a = "イ"; }
class B { .b = "ロ"; }
class C { .c = "ハ"; }
この時点では、クラス A, B, C に継承関係はありません。次に、
A'inherit( B );
とすると、クラス A は、クラス B を継承します。そのため、
print A.a, A.b;
で、
イ, ロ
とプリントされます。また、
B'inherit( C );
とすると、クラス B は、クラス C を継承します。従って、クラス A は、クラス C を、
間接的に継承します。そのため、
print A.a, A.b, A.c;
で、
イ, ロ, ハ
とプリントされます。今までに追加した継承を削除するには、次のようにします。
A'disherit( B );
B'disherit( C );
今度は、インスタンスでの継承の追加と削除の例を示します。ここでは、クラス A の
インスタンス X を生成して使います。
X = A();
この時点では、このインスタンス X の直接のクラスは、A だけです。次に、
X'inherit( B );
とすると、インスタンス X の直接のクラスに、クラス B が追加されます。そのため、
print X.a, X.b;
で、
イ, ロ
とプリントされます。次に、以下のようにして、クラス B の基底クラスを、クラス C に
してみます。
B'inherit( C );
とすると、インスタンス X は、クラス C を間接的に継承します。そのため、
print X.a, X.b, X.c;
で、
イ, ロ, ハ
とプリントされます。次に、以下のように、クラス A, B, C のデストラクタを設定して、
インスタンス X を削除します。
A.Destruct = function() { print "A 破棄: " : this'name; };
B.Destruct = function() { print "B 破棄: " : this'name; };
C.Destruct = function() { print "C 破棄: " : this'name; };
delete X;
これを実行すると、次の通り、プリントされます。
B 破棄: X
A 破棄: X
インスタンスの破棄では、その直接のクラスのデストラクタしか呼ばれないので、クラス
A と B のデストラクタしか呼ばれていません。呼ばれる順序は、登録の逆順になるので、
ここでは、B, A の順になっています。
●継承の段数と基底クラスの取得
継承に関連したシステム組み込みリレー型関数には、'from と 'base もあります。
A'from( B )
は、AがBを継承する段数になります。この段数は、AがBを継承していなければ 0 に、
AがBを直接継承していれば 1 に、AがBを間接的に継承していれば、その間接の深さ
に応じて、2,3,4,... となります。例えば、上例のインスタンス X の破棄直前で、
X'from( A ), X'from( B ), X'from( C ) は、それぞれ、1, 1, 2 になります。
A'from( B ), B'from( C ), C'from( A ) は、それぞれ、0, 1, 0 になります。
A'base( i )
は、Aの第 i 番目の直接の基底クラスへの参照になります。基底クラスが無ければ、
null になります。なお、i = 0,1,2,... で、第1、第2、第3、・・・ の直接の
基底クラスに対応します。間接の基底クラスは、対象外です。i が省略、つまり、
A'base
は、Aの最初の直接の基底クラスへの参照になります。それがなければ、null です。
例えば、上例のインスタンス X の破棄直前で、
X'base( 0 ), X'base( 1 ) は、それぞれ、クラス A, B への参照になります。
X'base( 2 ), X'base( 3 ), ... は、null です。
A'base, B'base, C'base は、それぞれ、null, クラス C への参照, null です。
ちなみに、上例のインスタンス X の破棄直前で、
print X'base'name, X'base(1)'name, B'base'name;
を実行すると、次の通り、プリントされます。
A, B, C
●任意関数のメンバー関数としての適用
本言語では、次の形式で、任意の関数を、オブジェクト(クラスやインスタンス)の
メンバー関数として、コールできます。
オブジェクト名.[ 関数名 ]( 引数の並び )
ここで、「オブジェクト名」と「関数名」には、単一の識別名だけではなく、一般に、
「箱の経路」を規定できます。また、この中に連想名がある場合は、そのインデックスに
変数が使えます。
この形式で「関数名」の関数がコールされると、その関数の実行時のメンバースコープ
は、「オブジェクト名」のオブジェクト内のスコープになります。
例えば、次のような関数 Show があるとします。この関数は、特定のオブジェクトの
メンバー関数ではありません。この関数は、渡された引数 v の値と、コールされた時に
確定するメンバースコープ内の X と Y という変数の値をプリントします。
function Show( v ) { print v : "X=" : .X, "Y=" : .Y; }
また、次のようなクラス C があって、そのインスタンス A を生成したとします。
class C
{
.X = 1;
.Y = 2;
function Construct( x, y ) { .X = x; .Y = y; }
}
A = C( 3, 4 );
さて、次のようにして、関数 Show を、クラス C とインスタンス A のメンバー関数と
して、コールできます。
C.[ Show ]( "C: " );
A.[ Show ]( "A: " );
これを実行すると、次の通りプリントされます。
C: X=1, Y=2
A: X=3, Y=4
オブジェクト(クラスやインスタンス)のメンバー関数内で、その現メンバースコープ
を保持したまま、所望の関数を呼び出すには、次の形式を使います。
.[ 関数名 ]( 引数の並び )
例えば、先程のクラス C に、メンバー関数 Print を、次のように追加したとします。
C.Print = function ( v ) { .[ Show ]( v ); };
この関数 Print は、先程の非メンバー関数 Show を、メンバー関数として、コールして
います。このメンバー関数を使って、次のように実行すると、先程と同じプリント結果
になります。
C.Print( "C: " );
A.Print( "A: " );
また、以下のように、クラス C に、先程の非メンバー関数 Show を、メンバー関数と
して、登録することもできます。
C.Show = Show;
ここでは、同名で登録していますが、別名であっても構いません。このメンバー関数を
使って、以下のように実行すると、先程と同じプリント結果になります。
C.Show( "C: " );
A.Show( "A: " );
●裏に隠れた同名メンバーへのアクセス
クラスやインスタンスの継承関係において、そのメンバーの検索順序は既に述べた通り
ですが、その場合、最初に見つかったものが対象になるので、裏に隠れた同名メンバーは、
通常、アクセスされません。しかし、そのメンバーに、アクセスしたい場合もあります。
例えば、派生クラスのコンストラクタで、基底クラスのコンストラクタをコールする必要
がある場合等です。
次は、派生クラスから、裏に隠れた基底クラス内の同名のメンバー関数をコールする
例です。なお、ここでは、各クラスを、モジュールローカルスコープ内に格納しています。
この意味は、後で述べます。
class ^A // 基底クラス
{
.X = 1;
.Y = 2;
function Construct( x, y )
{
.X = x;
.Y = y;
print "構築: X=" : .X, "Y=" : .Y;
}
function Destruct() { print "破棄: X=" : .X, "Y=" : .Y; }
function Show() { print "表示: X=" : .X, "Y=" : .Y; }
}
class ^B : ^A // 派生クラス
{
function Construct( x, y ) { .[ ^A.Construct ]( x, y ); }
function Destruct() { .[ ^A.Destruct ](); }
function Show() { .[ ^A.Show ](); }
function CopyFromA() { .X = ^A.X; .Y = ^A.Y; }
}
G = ^B( 3, 4 ); // 派生クラス B のインスタンス G を生成
G.Show(); // G のメンバー X, Y の値を表示
G.CopyFromA(); // G のメンバー X, Y の値に、クラス A の各値をコピー
G.Show(); // G のメンバー X, Y の値を表示
delete G; // G を削除
これを実行すると、次の通り、プリントされます。
構築: X=3, Y=4
表示: X=3, Y=4
表示: X=1, Y=2
破棄: X=1, Y=2
この例では、前節の「任意関数のメンバー関数としての適用」で説明した形式を使って、
基底クラスのメンバー関数を、派生クラスのメンバーとして、コールしています。また、
基底クラスのメンバー変数は、基底クラスに直接アクセスしています。
ここで、ちょっと注意しておくべきことがあります。各クラスは、モジュールローカル
スコープ内に格納しましたが、これを、現実行関数( この場合、暗黙のメイン関数 )の
ローカルスコープ内に格納した場合は、どうなるでしょう。この場合、派生クラス B の
メンバー関数のローカルスコープと、暗黙のメイン関数のローカルスコープは、違うので、
派生クラス B のメンバー関数内から、基底クラス A は見えません。そのため、例えば、
次のようにすると、派生クラス B の各メンバー関数の実行時にエラーが発生します。
class A // 基底クラス
{
・・・・
}
class B : A // 派生クラス
{
function Construct( x, y ) { .[ A.Construct ]( x, y ); }
function Destruct() { .[ A.Destruct ](); }
function Show() { .[ A.Show ](); }
function CopyFromA() { .X = A.X; .Y = A.Y; }
}
基底クラスのメンバー関数で、派生クラスのメンバー関数を、そのままコールするだけ
なら、もう少し簡便で効率的な書き方ができます。例えば、上例の場合は、次のように
することができます。このようにして実行しても、プリント結果は前と同じになります。
class ^B : ^A // 派生クラス
{
.Construct = ^A.Construct;
.Destruct = ^A.Destruct;
.Show = ^A.Show;
function CopyFromA() { .X = ^A.X; .Y = ^A.Y; }
}
基底クラスに対象の関数があるかどうかを確認してから、コールすることもできます。
例えば、派生クラス B のコンストラクタで、基底クラス A のコンストラクタがある場合
にだけ、それをコールするには、次のようにします。
function ^B.Construct( x, y )
{
if( ^A.Construct'exist? ) // 基底クラスにコンストラクタあり?
.[ ^A.Construct ]( x, y ); // 基底クラスのコンストラクタをコール
}
●クラスを返す関数
一般に、関数は、そのローカルスコープ内にある「箱を入れる箱」の実体を返すことが
できます。本言語では、クラスは複合箱なので、関数は、そのローカルスコープ内にある
クラスの実体を返すことができます。
なお、関数のリターン時に、そのローカルスコープは消滅してしまうので、その前に、
そのクラスの実体は、その関数の呼び出し元の関数のローカルスコープ内へ移動されます。
但し、関数のリターン時に消滅しないスコープ内にあるクラスを返す場合、そのクラスへ
の参照が返るので、このような移動は、行なわれません。
次の例では、GetClass( type ) という関数が、引数で指定された type に対応する
クラスの実体を返します。このクラスは Base というクラスの派生クラスになります。
class ^Base // GetClass 関数が返すクラスの基底クラス
{
.x = 123;
function Print()
{
print "Name: " : this'name ,
"Kind: " : this'kind ,
"Ref?: " : this'ref? ? "YES" : "NO" ;
}
}
function GetClass( type ) // クラスの実体を返す関数
{
switch( type )
{
case "Type-A":
class T : ^Base
{
function Construct( v ) { .a = v; }
function Print()
{ print this'name: ": a=" : .a, "x=" : .x; }
}
break;
case "Type-B":
class T : ^Base
{
function Construct( v1, v2 ) { .b1 = v1; .b2 = v2; }
function Print()
{ print this'name: ": b1=" : .b1, "b2=" : .b2, "x=" : .x; }
}
break;
}
return T;
}
この関数を使って、次ように、A と B のクラスを設定して、そのインスタンスを生成
して、各内容を表示します。
A = GetClass( "Type-A" ); // A は "Type-A" に対応するクラスになる
A.[ ^Base.Print ](); // クラス A の内容表示
IA = A( 100 ); // クラス A のインスタンス IA 生成
IA.Print(); // インスタンス IA の内容表示
B = GetClass( "Type-B" ); // B は "Type-B" に対応するクラスになる
B.[ ^Base.Print ](); // クラス B の内容表示
IB = B( 200, 300 ); // クラス B のインスタンス IB 生成
IB.Print(); // インスタンス IB の内容表示
これを実行すると、次の通り、プリントされます。
Name: A, Kind: structure, Ref?: NO
IA: a=100, x=123
Name: B, Kind: structure, Ref?: NO
IB: b1=200, b2=300, x=123
なお、このように、クラスを返す関数の返値の代入によるクラスの設定では、上述の通り、
その返値は、そのクラスの実体であって、それは既に現ローカルスコープ内に移動されて
いて、それが代入元になります。この代入元は、前述の「一時インスタンス」が代入元に
なる場合と同様に、その代入先へは、その実体が移動されます。したがって、この場合、
代入演算子に、:= や <- を使っても、結果は同じです。
●クラスへの参照によるインスタンス生成
前述の「クラスのインスタンスを生成する式」内の「クラス名」は、対象のクラスへの
参照であっても構わないということは、既に述べましたが、本節では、その例を示します。
なお、以下の例では、グローバルスコープ内に GenClass という名前のクラスが既にある
ものとしています。
クラスへの参照を明示的に設定して、それで、インスタンスを生成する場合は、次の
ようになります。
ClassRef := ::GenClass;
Instance = ClassRef( ・・・ );
クラスへの参照を返す関数の返値でインスタンスを生成する場合は、次のようになり
ます。なお、前節で述べた通り、関数が返すクラスの実体がその関数のローカルスコープ
外にある場合、返値は、そのクラスの実体への参照になります。
function GetClass( ・・・ )
{
・・・・・
return ::GenClass; // クラス GenClass への参照が返る
}
ClassRef := GetClass( ・・・ );
Instance = ClassRef( ・・・ );
関数がクラスへの参照を受け取って、そのインスタンスを生成して返す場合は、次の
ようになります。なお、関数返値でのインスタンス生成式の使用については、後述します。
Instance = MakeInstance( ::GenClass );
function MakeInstance( cls ) // 生成インスタンスのクラスへの参照を受け取る
{
・・・・・
return cls( ・・・ ); // 指定クラスのインスタンスを返す
}
●関数引数でのインスタンス生成式の使用
関数の引数に、前述の「クラスのインスタンスを生成する式」を使うことができます。
その場合、まず、そのクラスのインスタンスが、現実行中の関数のローカルスコープ内に、
一時的に生成されます。この一時インスタンスは、そのクラスを継承していますが、それ
以外中身はありません。次に、そのクラスにコンストラクタが定義されていれば、それが
自動的にコールされます。この時、インスタンス生成式で指定されている「引数の並び」
が、コンストラクタに渡されます。コンストラクタは、この一時インスタンスに対して、
初期設定を行ないます。その後、この一時インスタンスへの参照が、呼び出し先の関数の
引数に設定されます。従って、そのクラスのインスタンスの実体は、呼び出し元の関数の
ローカルスコープ内に存在しています。このインスタンスの実体は、通常、呼び出し先の
関数から戻って来た後で破棄されます。
以下に、この例を示します。
class C // インスタンスを生成するクラス
{
function Construct( x, y )
{
.X = x;
.Y = y;
print "構築: X=" : .X, "Y=" : .Y;
}
function Destruct() { print "破棄: X=" : .X, "Y=" : .Y; }
function Show() { print "表示: X=" : .X, "Y=" : .Y; }
}
ShowInstance( C( "太郎", "花子" ) ); // インスタンス生成式を引数で使用
print "プログラム終了!";
return;
function ShowInstance( I ) // インスタンスの内容を表示する関数
{ // 引数は、インスタンスへの参照
print "ShowInstance 開始";
print "引数は参照か? --- " : I'ref? ? "YES" : "NO";
I.Show();
print "ShowInstance 終了";
}
これを実行すると、次の通り、プリントされます。
構築: X=太郎, Y=花子
ShowInstance 開始
引数は参照か? --- YES
表示: X=太郎, Y=花子
ShowInstance 終了
破棄: X=太郎, Y=花子
プログラム終了!
●関数返値でのインスタンス生成式の使用
関数の返値で、前述の「クラスのインスタンスを生成する式」を使うことができます。
つまり、return 文の式に使えます。その場合、まず、そのクラスのインスタンスが、現
実行中の関数(return 文を実行する関数)のローカルスコープ内に、一時的に生成され
ます。この一時インスタンスは、そのクラスを継承していますが、それ以外の中身はあり
ません。次に、そのクラスにコンストラクタが定義されていれば、それが自動的にコール
されます。この時、インスタンス生成式での「引数の並び」が、コンストラクタに渡され
ます。コンストラクタは、この一時インスタンスに対して、初期設定を行ないます。その
後、このインスタンスの実体は、呼び出し元の関数(復帰先の関数)のローカルスコープ
内へ移動されます。このインスタンスが、この関数コールの返値になります。この返値は、
通常、どこかに代入されます。この代入は、「インスタンスの生成とコンストラクタ」の
節で述べた代入の形態と同様で、代入先にインスタンスの実体が移動されます。
以下に、この例を示します。
function MakeInstance( cls ) // 指定クラスのインスタンスを生成する関数
{ // 引数は、クラスへの参照
print "MakeInstance 開始";
return cls( "太郎", "花子" ); // インスタンスを生成して返す
}
class C // インスタンスを生成するクラス
{
function Construct( x, y )
{
.X = x;
.Y = y;
print "構築: X=" : .X, "Y=" : .Y;
}
function Destruct() { print "破棄: X=" : .X, "Y=" : .Y; }
function Show() { print "表示: X=" : .X, "Y=" : .Y; }
}
I = MakeInstance( C ); // 関数にクラスを渡して、そのインスタンスを受け取る
print "返値は参照か? --- " : I'ref? ? "YES" : "NO";
I.Show();
print "プログラム終了!";
return;
これを実行すると、次の通り、プリントされます。
MakeInstance 開始
構築: X=太郎, Y=花子
返値は参照か? --- NO
表示: X=太郎, Y=花子
プログラム終了!
破棄: X=太郎, Y=花子
●データ設定ブロック内でのインスタンス生成式の使用
連想配列のデータ設定ブロック内の要素に、前述の「クラスのインスタンスを生成する
式」を使うことができます。この場合は、連想配列の要素へのインスタンスの代入になる
ので、基本的には、通常の代入の場合と同じです。
以下に、この例を示します。
class C // インスタンスを生成するクラス
{
function Construct( x, y )
{
.X = x;
.Y = y;
print "構築: X=" : .X, "Y=" : .Y;
}
function Destruct() { print "破棄: X=" : .X, "Y=" : .Y; }
function Show() { print "表示: X=" : .X, "Y=" : .Y; }
}
A = { C(1,2), C(3,4), C(5,6) }; // 連想配列のデータ設定ブロック内に
// インスタンス生成式を使用
A[0].Show();
A[1].Show();
A[2].Show();
print "プログラム終了!";
return;
これを実行すると、次の通り、プリントされます。
構築: X=1, Y=2
構築: X=3, Y=4
構築: X=5, Y=6
表示: X=1, Y=2
表示: X=3, Y=4
表示: X=5, Y=6
プログラム終了!
破棄: X=5, Y=6
破棄: X=3, Y=4
破棄: X=1, Y=2
●クラス/インスタンスのコピー/移動
本言語では、クラスやインスタンスは、複合箱になるので、そのコピーや移動は、一般
の箱と同様に、簡単に行なえます。次に、その各形式を示します。
クラスA = クラスB; // クラスA に クラスB をコピー
クラスA <- クラスB; // クラスA に クラスB を移動
インスタンスA = インスタンスB; // インスタンスA へ インスタンスB をコピー
インスタンスA <- インスタンスB; // インスタンスA へ インスタンスB を移動
ここで、「クラスA」,「クラスB」,「インスタンスA」,「インスタンスB」には、
単一の識別名だけでなく、一般に「箱の経路」を規定できます。これらに、連想名を使う
場合、そのインデックスには、変数が使えます。これらの原則は、通常の箱名を規定する
場合と同様です。また、これらは、対象への参照であっても構いません。
なお、インスタンスのコピーの際には、そのコンストラクタがあっても呼ばれません。
一方、コピーされたインスタンスが破棄される時には、そのデストラクタがあれば呼ばれ
ます。
●各種補足説明
(1)ソース変更不可の既存クラスへのメンバー関数の追加
既存クラスへのメンバー関数の追加については、既に述べましたが、ここでは、ソース
が変更できない、あるいは、変更すべきでない既存のクラス、例えば、共通のライブラリ
として提供されているクラスやシステム組み込みクラスへのメンバー関数の追加について
述べます。
本言語では、このような既存クラスに対して、わざわざそのクラスの派生クラスを設定
しなくても、今までに述べた方法で、その既存クラスへ直接メンバー関数を追加できます。
例えば、あるライブラリから、グローバルスコープ内に ClassA というクラスがロード
されていて、それに、NewMethod というメンバー関数を追加する場合には、次のようにし
ます。
function ::ClassA.NewMethod( ・・・ )
{
・・・・・
}
また、次のようにすることもできます。
::ClassA.NewMethod = function( ・・・ )
{
・・・・・
}
あるいは、次のようにしても、構いません。
class ::ClassA
{
function NewMethod( ・・・ )
{
・・・・・
}
}
このうち、最初の方法では、その関数定義がその関数の使用よりも後に記述されていても
構いません。しかし、それ例外の方法では、その関数の追加がその関数の使用よりも先行
する必要があります。
ちなみに、このようにして追加された関数は、次のように、削除することもできます。
delete ::ClassA.NewMethod;
なお、メンバー関数を、共通のライブラリやシステム組み込みクラスに追加する場合、
整合性や他との干渉等には、充分配慮する必要があります。
(2)各インスタンスの個別メンバー関数/変数
各々異なるメンバー構成ごとにクラスを定義しなくても、本言語では、既述のように、
同じクラスのインスタンスでも、各々のメンバー構成を、自由に変更できます。そのため、
例えば、イベント駆動処理での各イベントやGUI関連の各ボタンなどをオブジェクトに
する場合、個々のオブジェクトのデータ構成や処理関数の内容が異なっても、その各々に
対応するクラスをわざわざ定義する必要はありません。
以下の簡単なイベントハンドラーの例では、各イベントのインスタンスは、同じクラス
から生成されますが、各イベントに対応して、個別の処理を行なう同名のメンバー関数と、
個別のデータを保持するメンバー変数が設定されています。この例のイベントハンドラー
のクラスのオブジェクトは、1つだけなので、そのインスタンスは生成せずに、それ自身
で機能します。イベントクラスの各インスタンスは、イベントリストの連想配列の要素と
して任意の順に登録されていますが、イベントハンドラー内では、処理の効率化のために、
各イベントの識別値をインデックスとした連想配列に置き換えています。
まず、イベントハンドラークラスは、次のようになります。これは、各イベント処理
の窓口になりますが、個々の具体的処理は、各イベントに委任します。
class ^EventHandler // イベントハンドラークラス
{
function SetEvents( list ) // 各イベントの登録
{
.EventCount = list'count; // イベントの個数
for( i = 0 ; i < .EventCount ; i++ ) // 各イベントについて
{
// 現イベントへの参照を連想配列に登録(その ID がインデックス)
.EventAtId[ list[i].Id ] := list[i];
// 現イベントの初期設定関数があれば呼び出す
if( list[i].InitEvent != null )
list[i].InitEvent();
}
}
function OnEvent( id ) // 指定の ID に対応するイベントの処理
{
.EventAtId[ id ].OnEvent(); // 対応するイベント処理関数の呼び出し
}
}
イベントクラスは、以下のようになります。このクラスのコンストラクタは、受け取った
各引数の値を、インスタンスの各固有メンバーに設定します。このうち、最後の2つは、
関数への参照になっています。これらの関数の処理内容は、各インスタンスごとに異なり
ますが、どれも、そのインスタンスのメンバー関数として機能します。
class ^EVENT // イベントクラス
{
.Handler := ^EventHandler;
function Construct( id, name, init, proc )
{
.Id = id; // イベント ID
.Name = name; // イベント名称
.InitEvent = init; // イベント初期設定関数
.OnEvent = proc; // イベント処理関数
}
}
各イベント個別の識別値、初期設定関数、イベント処理関数は、次のようになります。
これらは、後に、インスタンス生成式の引数で使われます。
// 各イベントの識別値
#set ID_EventA 101
#set ID_EventB 102
#set ID_EventC 103
// 各イベントインスタンスの初期設定関数(各個別のメンバー変数を設定)
function InitEventA() { .X = "XXX"; }
function InitEventB() { .Y = "YYY"; }
// 事象Cの初期設定関数は無し
// 各イベントの処理関数
function EventProcA()
{
print "〜事象処理A〜", .X, .Id;
}
function EventProcB()
{
print "〜事象処理B〜", .Y, .Name;
}
function EventProcC()
{
print "〜事象処理C〜", .Handler.EventCount;
}
イベントクラスの各インスタンスは、次のように、連想配列のデータ設定ブロック内の
要素として生成されます。
^EventList =
{
^EVENT( #ID_EventA, "事象A", InitEventA, EventProcA ),
^EVENT( #ID_EventB, "事象B", InitEventB, EventProcB ),
^EVENT( #ID_EventC, "事象C", null, EventProcC ),
};
ここまで整ったところで、次のように、イベントハンドラーに各イベントを登録して、
各イベントの処理を、イベントハンドラーを通して行なってみます。
^EventHandler.SetEvents( ^EventList ); // 各イベントの登録
^EventHandler.OnEvent( #ID_EventA ); // イベントAの処理実行
^EventHandler.OnEvent( #ID_EventB ); // イベントBの処理実行
^EventHandler.OnEvent( #ID_EventC ); // イベントCの処理実行
このプログラムを実行すると、次の通り、プリントされます。
〜事象処理A〜, XXX, 101
〜事象処理B〜, YYY, 事象B
〜事象処理C〜, 3
(3)クラスライブラリ
クラスライブラリは、通常、グローバルスコープ内に、そのライブラリ名の箱を設けて、
その中に、各クラスを登録します。
例えば、XLib というライブラリに、ClassA, ClassB, ClassC というクラスを登録する
場合は、次のようになります。
class ::XLib.ClassA
{
・・・・・
}
class ::XLib.ClassB
{
・・・・・
}
class ::XLib.ClassC
{
・・・・・
}
このようにして登録されているクラスのインスタンスを生成するには、例えば、次の
ようにします。
A = ::XLib.ClassA( ・・・ );
クラスライブラリは、グループ化して登録することもできます。例えば、XLib という
ライブラリの中の GroupA というグループに、ClassG, ClassH を登録する場合は、次
のようになります。
class ::XLib.GroupA.ClassG
{
・・・・・
}
class ::XLib.GroupA.ClassH
{
・・・・・
}
このようにして登録されているクラスのインスタンスを生成するには、例えば、次の
ようにします。
G = ::XLib.GroupA.ClassG( ・・・ );
クラスライブラリの各クラスは、任意のソースファイルに分割して記述できます。その
際に、1つのメンバー関数は、複数のソースファイルに分離できませんが、同一クラスの
各メンバー関数は、必ずしも同一のソースファイル内でなくても構いません。
なお、1つのソースファイルは、1つのモジュールに対応し、ライブラリのロードは、
モジュール単位で行ないます。これに関しては、「モジュール」の章で詳述します。
(4)変数インデックスの連想名のクラス設定
クラス設定で、クラス名に連想名を含む場合、そのインデックスに変数が使えるという
ことは既に述べましたが、以下にその例を示します。なお、ここでは、「クラス設定文」
を、for ループの中で使っています。
Name = { "A", "B", "C" };
for( i = 0 ; i < 3 ; i++ )
{
class Class[ Name[i] ]
{
switch( i )
{
case 0: .X = i;
.F = function() { print .X; };
break;
case 1: .Y = i;
.G = function() { print .Y; };
break;
case 2: .Z = i;
.H = function() { print .Z; };
break;
}
}
}
この実行は、次のようにするのと同じです。
class Class["A"] { .X = 0; .F = function() { print .X; }; }
class Class["B"] { .Y = 1; .G = function() { print .Y; }; }
class Class["C"] { .Z = 2; .H = function() { print .Z; }; }
ちなみに、上記のようにして設定した各クラスを使って、
Class["A"].F();
Class["B"].G();
Class["C"].H();
を実行すると、次の通り、プリントされます。
0
1
2
(5)生成インスタンスに格納先がない場合
生成されたインスタンスが、どこにも格納されない場合、そのインスタンスは即時破棄
されます。例えば、次のようなクラスがあったとします。
class ^C
{
function Construct() { print "インスタンス生成"; }
function Destruct() { print "インスタンス破棄"; }
}
このクラスのインスタンスを、次のように生成しても、どこにも格納されないために、
このインスタンスは、即時破棄されます。
^C(); // 生成されたインスタンスの格納先がない!
print "プログラム終了!";
これを実行すると、次の通り、プリントされます。
インスタンス生成
インスタンス破棄
プログラム終了!
また、上記のクラスのインスタンスを返す、次のような関数があったとします。
function F()
{
return ^C();
}
この関数を次のようにコールしても、返されたインスタンスは、どこにも格納されない
ので、即時破棄されます。
F(); // 返されたインスタンスの格納先がない!
print "プログラム終了!";
これを実行すると、先程と同じプリント結果になります。