クラス

クラスとは

クラスはオブジェクトの性質や動作を定義しています。

画面を構成する部品(ボタン、ウィンドウなど)、印刷される文字及び、処理されるデータの各要素は、すべてクラスにより表現されています。

クラスは、プロパティ、メソッド、イベント、定数が定義されています。


オブジェクトはクラスのインスタンスです。

クラスにより定義されるメソッドやプロパティをCRSスクリプトで操作することで、オブジェクトの挙動を指示することができます。

プロパティ

プロパティとは、プロパティ名とプロパティ値の組で表現されるオブジェクトの属性で、プロパティの値によりオブジェクトの性質を決定する要素です。

プロパティの値を変更する事で、オブジェクトの見えかたや、ユーザの操作に対する反応が変わります。

変化の状態はどのプロパティを変えたかによって異なります。


プロパティはオブジェクトのクラスに応じた多くの種類があります。

またプロパティごとにアクセス制御が行われており、動的に変更ができるもの、読取専用のもの、作成時にしか指定ができないものなどがあります。

メソッド

メソッド(Method)は一種の関数です。オブジェクトは実行されるメソッドに応じた動作を行います。


Methodの呼び出し例

Form1.deleteChild();        /* Form1の内容を消去します。 */
Form1.get("/mainmenu.crs"); /* Form1にmainmenu.crsをロードします */

スタティック(静的)メソッド

スタティックメソッド(Static Method)は一種の関数です。クラスに直接属し、クラスが実行するメソッドです。

通常のメソッドとは異なり、オブジェクトに依存しない動作を行います。

そのため、オブジェクトを生成することなしに実行が可能です。


Static Methodの呼び出し例

var sess = HttpSession.findSession("https://app.biz-browser.jp");   /* 指定のURLへの通信セッションを取得します */
var host = Runtime.getHostName(); /* OSのコンピュータ名を取得します */

イベント

オブジェクトが何かに反応する場合、イベントが発生します。

イベントの種類や発生する契機はクラスにより定義されます。例えば、Buttonクラスの場合、ボタンを押下するとTouchイベントが発生します。

イベントは、「"on" + イベント名」の名前をもつFunction(イベントハンドラと呼びます)により捕捉することができ、パラメータにEventオブジェクトが渡されます。

イベントハンドラでは、イベントに応じた処理を記述することができます。

:
Button btn {
    x = 10;
    y = 10;
    width = 120;
    height = 30;
    function onTouch(e) {
        title = "Clicked!";
    }
}

クラスを操作するオペレータ

クラスに関する操作を行うオペレータとして、instanceofとsuperが用意されています。

instanceofは、あるオブジェクトがあるクラス、またはmixinクラスのインスタンスかどうかを確認するために利用します。

:
if (object1 instanceof TextBox) {
    messageBox("object1はTextBoxです");
}
:

この例では、object1がTextBoxオブジェクトか、またはTextBoxを基にして定義したクラスのオブジェクトの場合にメッセージボックスが表示されます。

instanceofのクラス判定は派生関係を考慮して行われる点に注意してください。

CRSの全てのクラスは、Objectクラスからの派生ですので、object1 instanceof Objectはobject1が何らかのオブジェクトを指している場合、常に真となります。


superオペレータは、Function内だけで利用できるオペレータです。superにより派生元クラスのメソッドを呼び出すことが可能となります。

class ClassA extends String {
    function toString() {
        return "ClassA : " + super toString();
    }
}
class ClassB extends ClassA {
    function toString() {
        return "ClassB : " + super toString();
    }
}
var A = new ClassA;
var B = new ClassB;
A.value = "test";
B.value = "test";
print(A.toString(), "\n");
print(B.toString(), "\n");

実行結果

ClassA : test
ClassB : ClassA : test

この例では、B.toString()の呼び出しにより、ClassBで定義されているtoString()が実行され、superによりClassAのtoString()が呼び出されます。

ClassAのtoString()ではさらにsuper toString()を実行しているため、StringのtoString()が呼び出されています。


また、superは、具体的なFunction名を指定せずに関数形式で呼び出すことができます。

コンストラクタFunctionの中から関数のように呼び出すと、派生元クラスのコンストラクタを呼び出されます。

通常のFunction内から呼び出すと、派生元クラスの同名のFunctionを呼び出します。

class MyClassA extends String {
    function MyClassA(arg) {    ... (A)
     :
    }
}
class MyClassB extends MyClassA {
    function MyClassB(arg) {
        super(arg);    ... (B)
        :
    }
}

この例では、MyClassBのコンストラクタからMyClassAのコンストラクタを呼び出しています。

クラスの定義

Biz/Browserに内蔵されているクラスを基にして、別のクラスをCRSプログラムで新しく定義(派生と言います)することができます。

新しく定義したクラスは、基のクラスの特性を受け継ぎますが、新しい特性を定義したり元の特性を変更することができます。

 :
class CustCode extends TextBox {
    width = 120;
    height = 24;
    maxLength = 12;
    font = new Font("", 12, /*bold*/true);
    function OnGetFocus(e) {
        BgColor = Color.YELLOW;
    }
    Function OnLostFocus(e) {
        BgColor = Color.STD;
    }
}

この例では、TextBoxを基にしてCustCodeクラスを定義しています。

CustCodeクラス固有の動作として、フォーカスを受けると黄色くなり、フォーカスを失うと元に戻るTextBoxを実現することができます。


アプリケーション画面を作成する場合、画面ごとに入力欄を個別に定義するよりも、このように入力項目に対応したクラスを定義して、画面にはそのオブジェクトを配置したほうが開発効率とメンテナンス性が向上します。


CustCodeクラスの利用例

 :
Form form1 {
    :
    CustCode cd1 {
        x = 10;
        y = 10;
    }
    :
}
 :

ユーザ定義メソッド

クラス定義内に記述されたファンクションは、メソッドとして扱われます。

staticステートメントを先頭に記述するとスタティックメソッドとして定義されます。

クラスに定義されるファンクションは、定義するクラス単位に管理されます。

例えば、クラスAから派生したクラスBの両方にファンクションFが定義されている場合、クラスAのファンクションFとクラスBのファンクションFは別のものとして共存します。

オブジェクトから同じ名前で定義されている、こうしたファンクションを呼び出すと、クラス階層上でもっとも上位に定義されたクラスのファンクションが実行されます。

class ClassA extends String {
    function functionF() {
        return "ClassA : " + value;
    }
}
class ClassB extends ClassA {
    function functionF() {
        return "ClassB : " + value;
    }
}
var A = new ClassA;
var B = new ClassB;
A.value = "test";
B.value = "test";
print(A.functionF(), "\n");
print(B.functionF(), "\n");

実行結果

ClassA : test
ClassB : test

このように最上位のファンクションが呼び出される動作は、呼び出し元のスクリプトの位置には関係なく、オブジェクトのクラス階層のみにより決定されます。

class ClassA extends String {
    function printFunc(text) {    ... (A)
        print("ClassA.printFunc : ", text, "\n");
    }
    function functionA() {    ... (B)
        printFunc("ClassA : " + value);
    }
    static function SFunctionA() {    ... (E)
        printFunc("ClassA : static");
    }
}
class ClassB extends ClassA {
    function printFunc(text) {    ... (C)
        print("ClassB.printFunc : ", text, "\n");
    }
    function functionB() {    ... (D)
        printFunc("ClassB : " + value);
    }
    static function SFunctionB() {    ... (F)
        printFunc("ClassB : static");
    }
}
var C = new ClassB("test");
C.functionA();    ... (X)
C.functionB();    ... (Y)

ClassA.SFunctionA(); .... (Z)
ClassB.SFunctionB(); .... (W)

実行結果

ClassB.printFunc : ClassA : test
ClassB.printFunc : ClassB : test
ClassA : static
ClassB : static

この例では、(X)のfunctionAの呼び出しにより、(B)と(C)のファンクションが実行されます。

(Y)のfunctionBの呼び出しでは、(D)と(C)のファンクションが実行されます。

(Z), (W)では、それぞれClassA, ClassBで定義したスタティックなファンクションが実行されます。


(X),(Y)については、ClassAを派生させたClassBによりClassAのprintFuncを差し替えるように動作します。


また、次のようにClassBを派生させてClassCを定義してprintFuncを再定義すると、さらに別の動作を与えることができます。

class ClassC extends ClassB {
    function printFunc(text) {
        //.messageBox("ClassC.printFunc : " + text);
    }
}
var C = new ClassC("test");
C.functionA();

実行結果

../_images/core14_image003.png

ユーザ定義プロパティ

クラス定義内では、そのクラスに対するプロパティ定義が記述可能です。

class ClassA extends Object {
    # 読取専用プロパティ
    default property propA { # "default"指定を行うことで、デフォルトプロパティをオーバーライド可能です
        # プロパティgetter : 値を取得する際に呼び出されます
        get {
            return 123;
        }
    }

    # 読み書き可能なプロパティ
    property propB {
        # プロパティgetter : 値を取得する際に呼び出されます
        get {
            # getter, setter共に、ブロック内には複数のステートメントが記述可能です
            print("accessing ClassA.propB");
            return _b;
        }
        # プロパティsetter : 値を設定する際に呼び出されます
        set(new_value) { # 変数の名称は関数の引数と同じ規則内で自由に設定可能です
            # 範囲チェックのようなコードも記述できます
            if(new_value < 0) throw new Exception("USER", 1, 1);
            _b = new_value;
        }
    }

    # プロパティ値を保存するための子オブジェクト
    Number _b;
}

ユーザ定義プロパティは、内蔵クラスのプロパティと同様に振る舞います。

var a = new ClassA;
print(a.propA); # ユーザ定義プロパティpropAを読み取ります
print(a); # propAがデフォルトプロパティ指定されているため、propAを読み取ります
try {
    a.propB = -1; # propBのsetterで範囲チェックを行っており、例外が発生します
}catch(e) {
    print("範囲外です");
}

なお、ユーザ定義プロパティににはいくつかの制約があります。

後述の「スクリプトにより定義されたクラスの制約」を参照して下さい。


コンストラクタ

全てのクラスは、コンストラクタと呼ばれるクラス名と同じ名前を持った初期化専用のメソッドを持っています。

コンストラクタは、クラスのインスタンス生成時に自動的に呼び出されます。


コンストラクタがどのような動作を行うか、また、どのような引数を採るかはクラスにより異なります。

例えば、Stringクラスのコンストラクタに文字列を渡すと、その文字列でStringオブジェクトが初期化されます。

var v = new String("test");
print(v, "\n");

実行結果

test

オブジェクトの初期化以外のコンテキストでコンストラクタを呼び出すことはできません。 例えば以下のようなスクリプトはエラーとなります。

var v = new String();
v.String("test");
print(v, "\n");

クラスを定義する際、コンストラクタは通常のファンクションと同じように定義しますが、ファンクション名をクラス名と同じ名前にします。


コンストラクタ定義の例

class MyString extends String {
    function MyString(text) {
        super(text);
        value = ">>" + value + "<<";
    }
}

コンストラクタを定義しない場合、派生元クラスのコンストラクタと同じコンストラクタが定義されたように振舞います。

class MyString extends String {
    function MyString(text) {    ... (A)
        super(text);
        value = ">>" + value + "<<";
    }
}
class MyString2 extends MyString {
     :
    (コンストラクタ ファンクション未定義)
     :
}
 :

var c = new MyString2("test");    ... (B)

この例の場合、(B)のnewで呼び出されるコンストラクタは(A)となります。

コンストラクタの引数はnew演算子によるオブジェクトが生成と、super()ステートメントによる派生元クラスのコンストラクタ呼び出しで指定することができます。

var v = new MyString("hello");

コンストラクタが呼び出された場合、必ず派生元クラスのコンストラクタをsuper()ステートメントにより呼び出す必要があります。

class MyString extends String {
    function MyString(text) {
        super(text);
        value = ">>" + value + "<<";
    }
}

通常は、コンストラクタの先頭でsuper()を実行するようにしてください。

コンストラクタにsuperステートメントが含まれない場合、自動的に引数なしでsuperを呼び出す処理が先頭に挿入されます。


superステートメントを含むコンストラクタが、例外や制御文により派生元のコンストラクタを呼び出さずに復帰した場合、オブジェクトの生成を中断し初期化途中のオブジェクトは破棄され、例外がthrowされます。


初期化ブロックとonInitializedメソッド

ユーザ定義クラスの定義文において、"{" と"}"で括られたブロックのことを初期化ブロックと呼びます。

初期化ブロックは後述のように、インスタンスの生成時に呼び出されます。

namespace MyClass extends Number {
    # この波括弧内の、プロパティ/メソッド/コンストラクタ定義を除いた箇所が初期化ブロックです。

    ...
    # 初期化ブロック内
    ...
    value = 123; # 初期化ブロック内 : 初期化ブロック実行時に代入
    Number num1; # 初期化ブロック内 : 初期化ブロック実行時に作成されます
    String str1; # 初期化ブロック内 : 初期化ブロック実行時に作成されます

    function func1() {
      # 初期化ブロック外
    }
    ...
    # 初期化ブロック内
    ...
}

初期化ブロック内では通常、プロパティを初期化したり独自の子オブジェクトを初期化および生成などを行います。

この初期化ブロックが実行されるタイミングはインスタンスの構築中であり、この時点においてはこのインスタンスは オブジェクトツリーに接続されていません。


そこで、オブジェクトツリーに接続していないと行えない処理を記述するためにonInitializedを使用します。

namespace MyButton extends Button {
    ...

    function onInitialized() {
        # 初期化中、オブジェクトツリーに接続された際に呼び出されます
        BgColor = ^.BgColor; # オブジェクトツリーに接続されているため、親オブジェクトを参照可能です
    }
    ...
}

onInitializedメソッドはユーザ定義クラス内のメソッドとして定義されたときのみ効果があります。

オブジェクト生成 時などにユーザ定義関数としてonInitialized関数を定義しても初期化時に呼び出されることはありません。


インスタンス生成時の動作

パッケージとクラス定義内のスクリプトの実行契機と変数スコープが通常のCRSスクリプトは異なることに注意してください。

01: namespace Package1 {
02:    Number Pdata1 = 10;
03:    String Pdata2 = "sample " + str(Pdata1);
04:    class MyClass extends String {
05:        MyData Cdata1 = Package1.Pdata1;
06:        String Cdata2 = "sample " + str(Cdata1);
07:        function MyClass(t, d1, d2) {
08:            super(t);
09:            Cdata1 = d1;
10:            Cdata2 = d2;
11:        }
12:        function onInitialized() {
13:            print("MyClass : parent.className = ", ^.className);
14:        }
15:        function FunctionA() {
16:            print(value, Package1.Pdata1  , Cdata1, Cdata2, "\n");
17:        }
18:    }
19:    class MyClass2 extends MyClass {
20:        Number Cdata3 = 20;
21:        function MyClass2(t) {
22:            super(t, 20, "hello");
23:        }
24:        function onInitialized() {
25:            super();
26:            print("MyClass2 : parent.name = ", ^.name);
27:        }
28:    }
29: }

このようなパッケージをimportした場合、次のように動作します。


パッケージ内のオブジェクト定義部分 (2行目から3行目)

パッケージのロード時に実行されます。

この部分の変数スコープはPackage1です。 従って、Pdata2の初期値の設定にPdata1を使う事ができます。


クラス定義部分( 4行目から28行目)

パッケージのロード時点ではコンパイルとクラス名の登録が行われます。 実行は行われません。

クラス定義内に文法的なエラーがある場合にはロード時に検出されますが、 更新するオブジェクトが見つからないなどの実行時エラーは、MyClassのインスタンス生成時まで検出されないことに注意してください。


5行目でMyDateという未定義のクラスが使用されていますが、コンパイル時にはクラスの有効性は検査されないため、 MyClassの最初のインスタンス生成時までにMyDataクラスが定義されればエラーとはなりません。

この部分の変数スコープは、クラスのインスタンスとなります。 従って、インスタンス生成の状況により、オブジェクトツリーに関連するスコープが変化することに注意してください。

15行目で、2行目で定義されているPdata1の参照を期待していますが、実際にどのオブジェクトが参照されるかは実行時の状況により不定となります。

/* パッケージ名と同じ名前のオブジェクトの影響2 */
String Package1 {
    String Pdata1 = "ABC";
    MyClass C;
}
:
Package1.C.FunctionA();

この場合、FunctionAを実行中のthisはPackage1.Cとなり、FuntionA内からPackage1を参照すると、 パッケージ名のPackage1ではなく、StringのPackage1が参照されます。

その結果、Pdata1は、2行目で定義されたNumberオブジェクトではなく、StringのPdata1が参照されます。


また、次の場合、

/* パッケージ名と同じ名前のオブジェクトの影響2 */
String foo {
    MyClass C {
        String Package1;
    }
}
:
foo.C.FunctionA();

変数スコープの原点であるCの直下にあるPackage1が検索ルールに従って優先的に検索されるため、 Stringのfoo.C.Package1.Pdata1が参照されますが、未定義となります。


このように、クラス定義内からクラス外部のオブジェクトを参照する場合、スコープ原点がインスタンスになる点に特に注意が必要です。

パッケージ名よりも子オブジェクトや先祖のオブジェクトの検索が優先される仕様により、 今回の例で期待したようにMyClass.FunctionAから、Package1.Pdata1を確実に参照する方法はありません。


パッケージに定義されているクラス、MyClass2のインスタンスを生成する場合、以下のように実行されます。


1. クラスのインスタンスが生成されます。

---:               MyClassのコンストラクタとFunctionAが定義されます。 コンストラクタはまだ実行されません。
---:               MyClass2がメモリ上に構築され、メソッドやプロパティなどと紐付けられます。

2. クラスの初期化ブロック(ブレース内にインラインで記述された部分)が、派生元から派生先 (String→MyClass→MyClass2)の順に実行されます。

5行目:           MyNumberオブジェクトが生成され、10に初期化されます
6行目:           Stringオブジェクトが生成され、"Sample 10"に初期化されます
20行目:         Numberオブジェクトが生成され、20に初期化されます。

3. 派生先から派生元 (MyClass2→MyClass→String)の順にコンストラクタを検索し、最初に見つかったコンストラクタが呼び出されます。

21行目:         MyClass2のコンストラクタが実行されます。
7行目:           22行目のsuper()により、MyClassのコンストラクタが実行されます。
---:                 8行目のsuper()によりStringのコンストラクタが実行されます。

4. [オブジェクト定義式の場合] オブジェクトがオブジェクトツリーに接続されます。


5. [オブジェクト定義式の場合] 派生先から派生元 (MyClass2→MyClass→String)の順にonInitializedメソッドを検索し、最初に見つかったonInitializedメソッドが呼び出されます。

24行目:         MyClass2のonInitializedメソッドが実行されます。
12行目:          25行目のsuper()により、MyClassのonInitializedメソッドが実行されます。

以下、呼び出しシーケンス順に復帰します。

クラス定義の留意点

クラス名

クラス定義は必ず名前空間の中に定義する必要があります。名前空間に属さないクラスを定義することはできません。
クラス名は、Biz/Browserにロードされる全てのクラスの内、同一の名前空間内で一意な名前にする必要があります。
名前空間が異なる場合、同一のクラス名の定義は許可されます。
また、クラスはBiz/Browserにロードされると、セッションの切断まで解放されることはありません。
この点からもクラス名の命名は、コーディング規約等で重複やオブジェクト名とのバッティングを起こさないようにご注意ください。

派生関係

クラスは必ず既存のクラスを基にした派生として定義する必要があります。
基本となるクラスが何もないまったく新しいクラスを定義することはできません。
基本クラスを指定しない場合、Objectクラスが基底クラスとして選択されます。
CRSにより定義したクラスを更に派生させて新しいクラスを定義することは可能ですが、新しいクラスの定義位置よりも前に、基となるクラスが定義されている必要があります。

スクリプトにより定義されたクラスの制約

現在のCRSでは、クラス定義に多くの制約があります。
制約の多くは内蔵されているクラスの挙動とスクリプト定義のクラスの挙動の違いによるものです。
クラスを利用する場合、これらの制約を考慮して設計する必要があります。
新しく定義するクラスに、内蔵されているクラスと同じように動作するメソッドを定義することはできません。
クラス定義内のFunction定義はメソッドとして扱われますが、内蔵されているクラスのメソッドと異なり再計算式の中で利用したり、引数の数を規制したりすることはできません。
新しく定義するクラスに、内蔵されているクラスと同じように動作するプロパティを定義することはできません。
propertyステートメントにてプロパティを定義したりアクセス制御は可能ですが、参照演算式の右辺にすることはできません。
内蔵されているクラスのプロパティをオーバーライドする事は可能ですが、内蔵クラスはオーバーライドしたプロパティのgetterから取得できる値に沿った振る舞いをするとは限りません。
現在のCRSではプロパティの値を保存するには子オブジェクトを使用する必要があります。この子オブジェクトについてはアクセス制御を行うことはできません。
プロパティ自体は削除不可能ですが、子オブジェクトは削除可能です。
新しく定義するクラスに、内蔵されているクラスと同じように動作するイベントを定義することはできません。
ユーザ定義イベントを発行することは可能ですが、内蔵しているイベントと異なりBiz/Designerのイベントビューに表示されることはありません。
クラス定義内のFunctionはクラス別に管理されますが、子オブジェクトをクラス別に管理することはできません。

mixinクラス

mixinクラスはクラスに対し継承とは独立して機能を注入することが可能な特殊なクラスです。
mixinクラスは通常のクラスと同様にプロパティ、メソッド、クラス変数などを定義することが可能です。
mixinクラスはインスタンス化することはできません。
mixinクラスを派生(extends)することはできませんが、mixinクラスに別のmixinクラスをmixinすることは可能です。
クラスは複数のmixinクラスをmixinすることが可能です。
mixin class Mixin1 {
    const ConstA = "111";
    static function sfuncA() {
        return "AAA";
    }
    function funcA() {
        return "aaa";
    }
    String childA = "1";
}

/* Mixin1クラスをmixinしたMixin2クラス */
mixin class Mixin2 mixin Mixin1 {
    function funcB() {
        return "bbb";
    }
    String childB = childA + "22";
}

mixin class Mixin3 {
    function funcC() {
        return "ccc";
    }
}

/* Mixin2とMixin3をmixinしたクラスを定義 */
class Class1 mixin Mixin2, Mixin3 {
    String childC = childB + "333";
}

/* mixinしたクラスを派生 */
class Class2 extends Class1 {
    String childD = childC + "4444" + funcC();
}

オブジェクトに対しmixinクラスがmixinされているかどうかはクラスと同様に「instanceof演算子」で検査することができます。


mixinの優先順位

mixinしたクラスで定義が重複した場合、クラス継承と同様にオーバーライドされます。

一番最後にmixinされたもの(,で複数定義した場合、後のもの)が呼び出しの上位となります。

@metaアノテーション

ユーザー定義のGUIクラス(DisplayObject派生クラス)において、Biz/Designerで扱う際の付加情報を @meta アノテーションを利用して記述することが可能です。

@metaアノテーションに記載された内容は実行時には無視され動作に影響はありません。

class定義の直前に記述された@metaアノテーションが対象となります。

  • Biz/Designerでサイズ変更を許可/不許可

    @meta(ide.resize.allow = true/false)

  • クラスセレクタに表示する/しない

    @meta(ide.placeable = true/false)

  • 子オブジェクト自動生成のクラス指定(空文字で指定キャンセル)

    @meta(ide.child.classname = "")

  • クラスセレクタのクラス説明(日本語ロケール時)

    @meta(ide.description.ja_JP = "拡張テキストボックス")

  • クラスセレクタのクラス説明(指定言語の定義がない場合の代替)

    @meta(ide.description = "Extended ListBox")

使用例

namespace jp::co::opst::biz {
  # サイズ固定 / クラス説明(日本語)
  @meta(ide.resize.allow = false)
  @meta(ide.description.ja_JP = "拡張テキストボックス")
  class TextBoxEx extends TextBox {
    width = 200;
    height = 20;
    title = "入力...";
  }

  # クラスセレクタ非表示
  @meta(ide.placeable = false)
  class BaseButton extends Button {
    width = 100;
    height = 50;
  }

  @meta(ide.placeable = true)
  @meta(ide.resize.allow = false)
  class ButtonEx extends BaseButton {
    width = 100;
    height = 50;
  }

  # 子オブジェクトを生成しない / クラス説明(グローバル)
  @meta(ide.description = "Extended ListBox")
  @meta(ide.child.classname = "")
  class ListBoxEx extends ListBox {
    ListItem fixedItem[10];
  }
}