サイトトップ

Director Flash 書籍 業務内容 プロフィール

Adobe Flash CS3 Professional ActionScript 3.0

□08 カスタムクラスを定義する

本章では、カスタムクラスの定義について学びます。カスタムクラスというのは、ユーザーが定義してActionScriptに追加するクラスです。ユーザー定義クラスと呼ばれることもあります。クラスはFlashムービー(.flaファイル)とは別のActionScript(.as)ファイルを作成して定義しますので、他のプロジェクトに流用したり、ライブラリを構築するのに便利です。

08-01 空のクラスをつくる
はじめは、プロパティもメソッドもない空っぽのクラスを作成します。カスタムクラスの定義には決まったスタイルがありますので、その基本を覚えましょう。

最小限のクラス定義
カスタムクラスを定義する場合も、ユーザー定義関数のように、呼出し方や使い方から考えるとイメージしやすいです。一般にクラスのインスタンスは、new演算子によりコンストラクタを呼出して生成しました(Word 03-001「コンストラクタ」)。

var 変数:クラス = new クラス名([引数0, 引数1, ..., 引数n]);

カスタムクラスも、当然このステートメントでインスタンスが作成されます。まずは、コンストラクタを呼出して、インスタンスが作成できるまでの、最小限のクラスを定義してみましょう。取りあえず、プロパティやメソッドを備えるのは、後に回します。

図08-001■[新規ドキュメント]ダイアログボックスから[ActionScript (AS)ファイル]を作成

[ファイル]メニューから[新規]で[新規ドキュメント]ダイアログボックスを開き、[ActionScript (AS)ファイル]を選択する。

クラスは、[ActionScript (AS)ファイル]として、Flashのムービー(FLA)ファイルとは別に作成する必要があります(図08-001)。新規ActionScriptファイルがスクリプトウィンドウ(Word 02-002「スクリプトウィンドウ」)で開きますので、クラス定義はそこに記述します(図08-002)。

図08-002■クラス定義はスクリプトウィンドウに記述する

[ファイル]メニューから[新規]で[新規ドキュメント]ダイアログボックスを開き、[ActionScript (AS)ファイル]を選択する。

Tips 08-001■[パネルを隠す]
スクリプトウィンドウでコード(スクリプト)を書くときには、[アクション]パネルその他のパネルが邪魔になるかもしれません。[ウィンドウ]メニューから[パネルを隠す](ショートカット[F4])を選ぶと、スクリプトウィンドウを残して、パネルをすべて非表示にすることができます。パネルを改めて表示するには、同じメニュー(ショートカット[F4])をもう1度選択します。

それでは、クラス定義の書き方に移ります。ActionScript 3.0の最小限のクラスを定義する構成は、以下のとおりです。

【最小限のクラス定義】
[編集者用注釈] テキストです。
// ActionScript 3.0クラス定義ファイル: クラス名.as
package [パッケージ名]{
  class クラス名 {
    function クラス名() {   // コンストラクタメソッド
    }
  }
}

クラスは、class定義キーワードに続けて、クラス名を指定することにより定義します。続く中括弧{}内のクラス本体には、クラスと同じ名前の関数をひとつ定義します。クラスに定義された関数はメソッドと呼びました(Word 01-005「関数」参照)。クラスと同じ名前で呼出されるメソッドというのは、つまりコンストラクタです。

そして、クラス全体をpackageという定義キーワードで括ります。バッケージにはパッケージ名をつけることができます(省略することも可能です)。パッケージは、クラスをグループ分けする役割があります。パッケージについて詳しくは、また後の章で説明します。

クラスを定義したActionScriptファイルは、「クラス名.as」の名前で保存しなければなりません。保存すべき場所は、パッケージの指定などにより変わります。そうすると、クラスを定義するActionScriptファイルにおいて、クラス名は都合3箇所で使われるべきことになります。

【クラス名が指定される3箇所】
  1. クラス名
  2. コンストラクタメソッド名
  3. ActionScriptファイル名

Tips 08-002■パッケージはクラスをグループ分けする仕組み
「パッケージ」は、クラスを分類してまとめるための仕組みです。パッケージ名を指定すると、クラスはそのパッケージに属することになります。それは、ファイルを分類して管理するフォルダのような役割です。実際、パッケージを指定すると、クラスを定義したActionScriptファイルはパッケージ名のついたフォルダに格納する必要があります。

なお、ActionScript 3.0の多くのクラスも、パッケージに属しています。たとえば、MovieClipクラスのパッケージは、flash.displayです(図08-003)。

図08-003■[ヘルプ]に示されたMovieClipクラスのパッケージ

[MovieClip]クラスのページの冒頭に、「パッケージ」が記載されている。

[*筆者用参考] WEBワークショップ: 初心者のためのJava講座 「パッケージについて理解する」、Javaプログラミング・ワンポイントレクチャー「パッケージを理解し利用する」、@IT:Eclipseではじめるプログラミング「Javaのパッケージを理解する」、基本情報技術者Web学習室Java「パッケージとは」。



クラスとコンストラクタとActionScriptファイルの3つは、すべて同じクラス名で統一。

タイムラインから呼出せるクラス
それでは、実際にスクリプトエディタでクラスを定義してみましょう。クラス名はMyClassとします。もちろん、名前は識別子です。また、クラス名は、先頭を大文字にするのが一般的です(Tips 02-010「小文字で始めない識別子」)。クラス本体(中括弧{}内)には、クラスと同名のコンストラクタを空のまま定義します。

クラスはFlashムービー(FLAファイル)内で呼出されると、そのムービーファイルをパブリッシュするときにSWFに含まれて書出されます。ActionScript(AS)ファイル単体で、SWFを書出すことはできません。したがって、このクラスを呼出すFlashムーピーファイルを別に用意します。

Tips 08-003■クラスは必ず保存してSWFに書出す
クラスは、それが呼出されているFlashムービー(FLAファイル)をパブリッシュするときに、SWFとして書出されます。また、パブリッシュ時に読込まれるのは、保存されたActionScriptファイルのデータです。ですから、クラス定義の内容に変更が生じたら、変更後のデータをまず必ず保存しなければなりません。そしてつぎに、FLAファイルを、改めてSWFに書出す必要があります。

パブリッシュされたSWFがFlash Player上で再生されるとき(ランタイム時)に、ActionScriptファイルのデータをダイナミックにロードする訳ではないことにご注意ください。したがって、ActionScriptファイルは、サーバーにアップロードしたり、SWFに添付する必要はありません。

以下のActionScript 3.0クラスMyClassの定義には、パッケージ名を指定していません。その場合、クラスを定義したActionScriptファイルは、そのクラスを呼出すFLAファイルと同じ場所に、クラスと同名のMyClass.asとして保存します。

// ActionScript 3.0クラス定義ファイル: MyClass.as
package {
  class MyClass {
    function MyClass() {
    }
  }
}

Flashムービー(FLA)ファイルからクラスのインスタンスをつくるには、はじめに確認したとおり「var 変数:クラス = new クラス名()」(今回定義したMyClassのコンストラクタには引数はありません)でコンストラクタを呼出します。ところが、上記に定義したクラスMyClassのコンストラクタには、戻り値つまりreturnステートメントがありません。しかし、new演算子を使って呼出されたコンストラクタメソッドは、そのクラスのインスタンスを返す決まりになっています。

よって、定義されたクラスMyClassのインスタンスを生成するステートメントは、つぎのようになります。

var myObject:MyClass = new MyClass();

Tips 08-004■コンストラクタに戻り値は指定できない
コンストラクタは、クラスのインスタンスを返すことが決まっています。そのため、returnステートメントで値を返すことはできません。また、コンストラクタメソッドに戻り値のデータ型も指定できません。

Flashムービー(FLA)ファイルのフレームアクションに上記のステートメントを記述して[ムービープレビュー]を確認すると、[コンパイルエラー]がふたつも表示されます(図08-004)。ひとつは型指定のMyClassで、もうひとつはコンストラクタの呼出しのMyClass()です。いずれも、定義したクラスMyClassが認識されていないことに起因するようです。

1046: 型が見つからないか、コンパイル時定数ではありません: MyClass。

1180: 未定義である可能性が高いメソッドMyClassの呼び出しです。

図08-004■クラスの呼出しで表示された[コンパイルエラー]

ふたつの[コンパイルエラー]は、いずれもクラスMyClassが認識されていないことを示唆する。

Tips 08-005■型チェックによるエラー
データ型の指定によるチェックは、デフォルトではSWFに書出すコンパイル時と、SWFをFlash Playerで再生する実行(ランタイム)時の両方で行われます。

エラーの内容は、[ActionScript 3.0コンポーネントリファレンスガイド]の[付録]に、それぞれ[コンパイラエラー]および[ランタイムエラー]として一覧表が掲載されています。

このようにクラスが認識されない場合、まず第1に確認すべきは、クラスを定義したActionScript(AS)ファイルの保存場所です。しかし、今回のサンプルでは、ASファイルを正しくFLAファイルと同じ場所に保存しても、[コンパイルエラー]が発生します。

クラスやそこに定義されたプロパティとメソッドには、アクセスを許す範囲が指定できます。mixi(ミクシィ)などのSNS(ソーシャル・ネットワーキング・サービス)で、自分の日記を全体に公開するとか、友人だけが読めるように設定するというのと似ています。

そして、カスタムクラスはデフォルトでは、Flashムービー(FLA)ファイルからのアクセスを認めません。そこで、class定義キーワードにpublic属性を指定すると、クラスは全体に公開されます(スクリプト08-001)。そうすれば、Flashムービーのフレームアクションからも、クラスにアクセスしてインスタンスを作成することが可能になります。

スクリプト08-001■public属性を指定して全体に公開した最小限のクラス

// ActionScript 3.0クラス定義ファイル: MyClass.as
package {
  public class MyClass {
    function MyClass() {   // コンストラクタメソッド
    }
  }
}

Flashムービー(FLA)ファイルのフレームアクションから、以下のようにクラスMyClassのコンストラクタを呼出せば、空のクラスであってもインスタンスは作成されます。作成したインスタンスをtrace()関数で[出力]すると、クラスMyClassのインスタンスであることが示されます(図08-005)。

var myObject:MyClass = new MyClass();
trace(myObject);   // 出力: [object MyClass]
図08-005■public属性の指定により公開されたクラスをフレームアクションから呼出す


public属性の指定により、クラスはFlashムービーのフレームアクションから呼出しが可能になる。

Tips 08-006■スクリプトウィンドウの[ターゲット]メニュー
スクリプトウィンドウの上部右端には、Flashムービー(FLA)ファイルを選択する[ターゲット]メニューがあります(前掲図08-005上図参照)。このメニューでクラスを呼出すFLAファイルを指定すれば、スクリプトウィンドウから画面を切替えることなく、[ムービープレビュー]が確認できます。

なお、[ターゲット]メニューでFlashムービーファイルを選択するには、そのFLAファイルを開いておく必要があります。


Tips 08-007■スクリプトウィンドウの[ターゲット]メニューと[自動フォーマット]
スクリプトウィンドウの[ターゲット]メニューでFlashムービーファイルが指定されていないとき、[自動フォーマット]を適用すると不具合の生じることがあります。演算子の丸括弧()が消去されてしまうという問題が報告されています(Flash CS3 Professional 9.0.2まで再現を確認)。

図08-006■[ターゲット]が指定されていないと[自動フォーマット]で()が消える


クラスを呼出すFlashムービー(FLAファイル)は開いて、[ターゲット]に指定しておくのが安全。

ですから、クラスを呼出すFlashムービー(FLA)ファイルは予め開いておき、スクリプトウィンドウの[ターゲット]に指定しておくと、[ムービープレビュー]がすぐに確認できますし、上述の[自動フォーマット]のバグも避けられて安心です。

なお、[ターゲット]にFlashムービー(FLA)ファイルが指定されていても、returnステートメントの後にを記述した丸括弧()は、[自動フォーマット]で消去されてしまいます。この問題はスクリプトウィンドウだけでなく、[アクション]パネルでも発生します(F-site「()を使った式が戻り値に指定された関数を[自動フォーマット]すると」<http://f-site.org/articles/2007/06/05092845.html>参照)。

図08-007■returnの後に書いた()が[自動フォーマット]で消える


この問題は、スクリプトウィンドウだけでなく、[アクション]パネルでも起こる。

[*筆者用参考] FLASH-japan「Flash CS3スクリプトウィンドウのかっこがなくなる」。


Maniac! 08-001■クラス定義にコンストラクタの記述は不要
クラスには、コンストラクタが必要です。しかし、カスタムクラスを定義する際には、空のコンストラクタメソッドであれば、記述しなくても差支えはありません。なぜなら、「クラスでコンストラクタメソッドを定義しなかった場合、コンパイラが自動的に空のコンストラクタを作成します」(ヘルプ[ActionScript 3.0のプログラミング] > [ActionScriptのオブジェクト指向プログラミング] > [クラス] > [メソッド]「コンストラクタメソッド」)。

しかし、コンストラクタ自体は、クラスに必要です。また、後からコンストラクタメソッドに処理を加えるかもしれません。ですから、記述は省略せず、明示的にコンストラクタを定義しておく方が堅実でしょう。

[*筆者用参考]「クラスのコードの作成」。


08-02 プロパティとメソッドを定義する
ウォーミングアップは済みましたので、プロパティとメソッドが備わった簡単なクラスを定義してみましょう。つくるのは、単純なタイマーのMyTimerクラスです。

プロパティとメソッドのアクセス制御
まず、コンストラクタの呼出し方です。機能をシンプルにするため、コンストラクタ作成時がタイマーのスタートとします。引数は要りません。たとえば、つぎのようにコンストラクタを呼出します。

var myObject:MyTimer = new MyTimer();

インスタンスがタイマーとして経過時間を計算するためには、コンストラクタの呼出された時刻を保持しておかなければなりません。したがって、そのためのプロパティが必要になります。プロパティ名はmy_dateとし、Dateインスタンスで時刻を格納しておくことにしましょう。

経過時間を知るためのメソッドも必要です。メソッド名はgetElapsedTime()とします。戻り値は取りあえず数値に指定して、経過ミリ秒を返すことにします。したがって、getElapsedTime()メソッドの呼出しは、つぎのようになります。

var nTime:Number = myObject.getElapsedTime();

なお、クラス定義を学習する段階に入りましたので、はじめに予告したとおり(Tips 02-013「ユーザー定義関数名の接頭辞」)、これからとくにクラスに定義するメソッドについては接頭辞を付しません。プロパティやローカル変数、メソッドの引数は、名前に悩むのが煩わしい場合には、一部接頭辞・接尾辞を用います。

さて、プロパティとメソッドがひとつずつ決まりましたので、クラスMyTimerを定義してみます(スクリプト08-002)。プロパティは変数と同じくvar定義キーワードで宣言し、メソッドは関数(function)として定義します。なお、クラスに定義されたプロパティやメソッドを、併せて「メンバー」と呼ぶことがあります

スクリプト08-002■プロパティとメソッドを定義したクラスMyTimer

// ActionScript 3.0クラス定義ファイル: MyTimer.as
package {
  public class MyTimer {
    private var my_date:Date;
    public function MyTimer() {
      my_date = new Date();
    }
    public function getElapsedTime():Number {
      var current_date:Date = new Date();
      var nElapsedTime:Number = current_date.time-my_date.time;
      return nElapsedTime;
    }
  }
}

クラスのプロパティとメソッドがタイムラインに記述する変数や関数と1点異なるのは、publicprivateなどの属性キーワードが指定できることです。他の任意のクラスあるいはタイムラインから参照するプロパティやメソッドは、public属性にします。他方、private属性を指定すると、それを定義したクラス内からしかアクセスできません(表08-001)。アクセス制御の属性キーワードは、まだほかにふたつあります。それらについては、後述します。

表08-001■アクセス制御の属性キーワード(一部)
アクセス制御の属性 説明
private 定義されたクラス内からのみ、アクセスすることができます。
public 任意のクラスやタイムラインから、アクセスすることができます。コンストラクタメソッドは、このpublic属性しか指定できません。

AS1&2 Note 08-001■private属性キーワードの仕様
ActionScript 2.0におけるprivate属性は、サブクラスからアクセスすることができました。3.0では、メンバーを定義したクラスからしか、参照が許されません。その代わり、ActionScript 3.0には、サブクラスからのアクセスが可能なprotected属性キーワードが加わりました。protected属性については、後述します。

クラスMyTimerは、Flashムービー(FLA)ファイルのタイムラインから、たとえば以下のように使います。Stageに登録したInteractiveObject.clickイベントのリスナー関数は、MyTimer.getElapsedTime()メソッドを呼出して、その戻り値をtrace()関数で[出力]します。

// タイムライン: メイン
// フレームアクション
var myObject:MyTimer = new MyTimer();
trace(myObject);   // 出力: [object MyTimer]
// trace(myObject.my_date);   // コンパイルエラー
stage.addEventListener(MouseEvent.CLICK, xTrace);
function xTrace(eventObject:MouseEvent):void {
  var nElapsedTime:Number = myObject.getElapsedTime();
  trace(nElapsedTime);
}

[ムービープレビュー]を行うと、まずtrace()関数により、MyTimerインスタンスが[出力]されます。そして、ステージ上をクリックすれば、リスナー関数xTrace()が呼出されて、MyTimer()インスタンスを作成後の経過ミリ秒数が、[出力]パネルに表示されます(図08-008)。

図08-008■[出力]パネルにMyTimerインスタンスと経過ミリ秒数が表示

ステージ上をクリックするたびに、MyTimerインスタンスを生成してからの経過ミリ秒数が[出力]される。

Tips 08-008■Stageオブジェクトのマウスイベントへのリスナー登録
Stageオブジェクトに対するマウスイベント(InteractiveObject.click)にリスナー関数を登録すると、ステージ上のすべての領域でイベントが発生します。

クラスMyTimerのメソッドgetElapsedTime()は、public属性を指定しましたので、タイムラインから呼出すことができます。しかし、上記フレームアクションでコメントアウトされているtrace()関数の引数my_dateプロパティは、クラスMyTimerにprivate属性として宣言されています。したがって、コメント行区切り記号//を外して有効にすると、[コンパイルエラー]が発生します(図08-009)。

図08-009■private属性のプロパティに外部からアクセスすると[コンパイルエラー]が発生

[コンパイルエラー]パネルに、「アクセスできないプロパティ」である旨のエラーメッセージが表示される。

このように、クラス内でのみ用いるプロパティやメソッドにはprivate属性を指定することによって、クラスの外からの参照を禁じることができるのです。もっとも、メソッドgetElapsedTime()のpublic属性を外すと、タイムラインから呼出せなくなります(図08-010)。つまり、private属性を指定しなくても、タイムラインからアクセスすることができません。

図08-010■属性のないクラスのメソッドはタイムラインから呼出せない

private属性を指定しなくても、メソッドにタイムラインからのアクセスはできなくなる。

クラスのメンバー(プロパティ・メソッド)にアクセス制御の属性を指定しないと、デフォルトであるinternalが適用されます。これは、後述するパッケージ(10-01 「パッケージを使う」)が同じクラスからのアクセスを認める属性です。しかし、タイムラインはそれに当てはまりませんので、アクセスが禁じられることになります。

ところで、前に作成したサンプルのクラスMyClass(スクリプト08-001)を、もう1度見てみましょう。このクラスのコンストラクタメソッドには、つぎのようにアクセス制御の属性が指定されていません。

function MyClass() { // コンストラクタメソッド
}

しかし、スクリプト08-001に定義したクラスMyClassを試したとき、タイムラインから問題なくコンストラクタが呼出せました。実は、ActionScript 3.0においては、コンストラクタメソッドにはpublic属性しか指定することができません。当然、デフォルトの属性もpublicです。ですから、ActionScript 3.0のすべてのクラスについて、コンストラクタメソッドだけは、つねにどのクラスあるいはタイムラインからでも呼出しが可能なのです。


アクセス制御の属性を使うと、mixi日記のように公開範囲が指定できる。

プロパティにアクセスするためのメソッド
プロパティをprivate属性で指定すれば、外から参照されたり、勝手に書替えられることが防げます。しかし、その値を調べたり、設定する必要が生じるかもしれません。そのような場合に備えて、プロパティの現在値を返すメソッドや、値を設定するためのメソッドを定義することがあります。このようなプロパティ値の取得・設定のためのメソッドは、「アクセサ」(accessor)と呼ばれます。

アクセサつまりプロパティにアクセスするためのメソッドを使うことには、いくつかの利点があります。まず第1に、プロパティはprivate属性で指定する前提になりますので、外から勝手に参照・変更されることがありません。設定のためのメソッドをあえて定義しなければ、読取り専用のブロパティにすることも可能です。

第2に、プロパティに設定する値を、細かく制限することができます。たとえば、度数の角度を設定するプロパティがあったとします。プロパティの宣言に数値(Number)型を指定しただけでは、値の範囲は定まりません。そこでメソッドとして定義すれば、その値を-180から180まで(あるいは0から360まで)に換算する処理が加えられます。

第3に、プロパティの値の参照や設定そのもの以外にも、他のプロパティにアクセスしたり、必要なメソッドを呼出したり、付随する処理を行うことも可能になります。具体的なサンプルは、後述する08-04「get/setアクセサメソッド」の解説でご紹介します。

【プロパティにアクセスするためのメソッドを使う利点】
  1. 直接のアクセスを防げる。
  2. 値の細かな制限ができる。
  3. 付随した処理が行える。

プロパティにアクセスするためのメソッドを定義するというのは、タレントが仕事の話は「マネージャを通す」というのと似ています。スケジュールの都合や仕事の内容、ギャランティなど、マネージャが必要な確認と判断を行うことにより、タレントは円滑に仕事ができる訳です。


値については、アクセサを通してね。

クラスMyTimerはシンプルな構成にしますので、privateなプロパティmy_dateへのアクセスとしては、値をリセットするメソッドのみ設けることにします。前記のクラスMyTimer(スクリプト08-002)にメソッドresetTimer()を加え、プロパティmy_dateに新規のDateインスタンスを設定します(スクリプト08-003)。

スクリプト08-003■プロパティ値をリセットするメソッドが追加されたクラスMyTimer

// ActionScript 3.0クラス定義ファイル: MyTimer.as
package {
  public class MyTimer {
    private var my_date:Date;
    function MyTimer() {
      resetTimer();
    }
    public function resetTimer():void {
      my_date = new Date();
    }
    public function getElapsedTime():Number {
      var current_date:Date = new Date();
      var nElapsedTime:Number = current_date.time-my_date.time;
      return nElapsedTime;
    }
  }
}

メソッドresetTimer()では、呼出されたときに新たなDataインスタンスを生成して、プロパティmy_dateに代入します。もっとも、この処理は前掲スクリプト08-002で、コンストラクタが呼出されたときの初期設定と同じです。そこで、上記スクリプト08-003では、コンストラクタもこのresetTimer()メソッドを呼出すことにしました。

新たに加えた、プロパティをリセットするメソッドresetTimer()は、以下のようなフレームアクションで試すことができます。InteractiveObject.clickイベントで呼出されるリスナー関数が、経過ミリ秒数をtrace()関数で[出力]した後、MyTimer.resetTimer()メソッドでタイマーの計測開始時刻をリセットします。したがって、クリックするたびに、直前のクリックからの経過ミリ秒数が、[出力]パネルに表示されることになります。

// タイムライン: メイン
// フレームアクション
var myObject:MyTimer = new MyTimer();
stage.addEventListener(MouseEvent.CLICK, xTrace);
function xTrace(eventObject:MouseEvent):void {
  var nElapsedTime:Number = myObject.getElapsedTime();
  trace(nElapsedTime);
  myObject.resetTimer();
}

メソッドの戻り値を考える
MyTimer.getElapsedTime()メソッドの戻り値のミリ秒は、経過時間を把握するには勝手がよくありません。やはり、時分秒の数値を知りたいところです。しかし、関数(function)はひとつの値しか返すことができません。だからといって、"時:分:秒"という文字列にしてしまうと、計算をしようとしたときに不便です。

そこで、戻り値をObjectインスタンスにして、Dateクラスのようにプロパティで時・分・秒・ミリ秒それぞれの値を格納するようにしましょう。プロパティ名は、Dateクラスと同じく時・分・秒・ミリ秒をそれぞれhours、minutes、seconds、millisecondsとします。

ミリ秒で示された時間から時・分・秒・ミリ秒を得るには、値を1000(ミリ秒)、60(秒)、60(分)で順に割り、その余りを取出します。たとえば、45296789ミリ秒を時・分・秒・ミリ秒に直すには、つぎのように割り算の商と余りを計算して、12時間34分56秒789ミリ秒とすればよいのです。

45296789÷1000(ミリ秒) = 45296…789(ミリ秒)
45296÷60(秒) = 754…56(秒)
754÷60(分) = 12…34(分)
12(時間)

この計算方法にしたがって時・分・秒・ミリ秒を計算し、各値がプロパティとして設定されたObjectインスタンスを返すようにMyTimer.getElapsedTime()メソッドは修正します。経過ミリ秒をこのようなObjectインスタンスに変換するメソッドを、translateToTimeObject()として定義しました(スクリプト08-004)。

スクリプト08-004■getElapsedTime()メソッドは時・分・秒・ミリ秒をプロパティとするObjectで返すよう修正
// ActionScript 3.0クラス定義ファイル: MyTimer.as
package {
  public class MyTimer {
    private var my_date:Date;
    function MyTimer() {
      resetTimer();
    }
    public function resetTimer():void {
      my_date = new Date();
    }
    public function getElapsedTime():Object {
      var current_date:Date = new Date();
      var nElapsedTime:Number = current_date.time-my_date.time;
      var oElapsedTime:Object = translateToTimeObject(nElapsedTime);
      return oElapsedTime;
    }
    public static function translateToTimeObject(nTime:Number):Object {
      var oTime:Object = new Object();
      oTime.milliseconds = nTime%1000;
      nTime = Math.floor(nTime/1000);
      oTime.seconds = nTime%60;
      nTime = Math.floor(nTime/60);
      oTime.minutes = nTime%60;
      oTime.hours = Math.floor(nTime/60);
      return oTime;
    }
  }
}

追加したtranslateToTimeObject()メソッドには、static属性キーワードが指定してあります。

staticというキーワードは、Mathクラスのメソッドをご紹介するときに説明しました(02-01「関数・メソッドを使う」の「Math.floor()メソッドで小数点以下を切捨てる」)。たとえば、Math.floor()メソッドは、インスタンスを生成することなく、Mathクラスを直接参照して呼出しました。また、円周率のπもMathクラスの定数として、Math.PIと記述してアクセスします。

このように、インスタンスでなくクラスに直接プロパティを宣言したり、メソッドを定義したいときに、static属性を指定します。static属性を指定した静的なメンバー(プロパティやクラス)は、インスタンスを生成せずに、直接「クラス名.プロパティ名」または「クラス名.メソッド名()」でアクセスします。

したがって、静的メソッドtranslateToTimeObject()は、クラスを直接参照してMyTimer.translateToTimeObject()のかたちで呼出します。ただし、メソッドを定義したMyTimerクラス内では、クラスの参照は省略することができます。上記スクリプト08-004のgetElapsedTime()メソッドは、クラスの参照なしに静的メソッドtranslateToTimeObject()を呼出しています。

Word 08-001■static属性キーワード
クラス定義においてつぎのシンタックスで、静的なメンバーを指定します。メンバーには、変数(プロパティ)と定数およびメソッドが含まれます(定数については後述)。インスタンスに定義されるメンバーはインスタンスメンバー、静的なメンバーをクラスメンバーと呼ぶことがあります。

static var プロパティ名
static const 定数名
static funciton 関数名(引数:データ型):戻り値のデータ型 {}

静的な(クラス)メンバーにアクセスするには、クラスを直接参照します。クラスのインスタンスを生成する必要はなく、インスタンスを参照してアクセスすることはできません。クラス内から静的なメンバーにアクセスするときは、クラスの参照を省略することができます。

静的なメンバーはクラスに対して定義され、クラスがロードされるときに1度だけ初期化されます。個別のインスタンスには関連づけられませんので、静的なメソッド内からはインスタンスメンバーにアクセスできません。逆に、インスタンスがクラスを参照することは当然できますから、インスタンスメソッドから静的なプロパティを参照したり、静的メソッドを呼出すことは可能です。

translateToTimeObject()メソッドは、引数として与えられたミリ秒数を、時・分・秒・ミリ秒のプロパティをもったObjectインスタンスに変換して返します。この処理は、個々のTimerインスタンスに関わりません。そこで、いわばユーティリティ的なメソッドとして、クラスに(静的に)定義することにしたのです。

では、translateToTimeObject()メソッド単体の動作を確かめておきましょう。メソッドはpublic属性で指定してありますので、タイムラインから呼出すことができます。タイムラインには確認用に、Objectから時・分・秒・ミリ秒の各プロパティ値を取出して、文字列として返す関数getTimeString()を定義しておきます。つぎのフレームアクションで、静的メソッドtranslateToTimeObject()が、クラスMyTimerを直接参照して呼出されていることにご注目ください。

// タイムライン: メイン
// フレームアクション
var oTime:Object = MyTimer.translateToTimeObject(45296789);
trace(getTimeString(oTime));   // 出力: 12:34:56.789
function getTimeString(oTime:Object):String {
  var time_str:String = String(oTime.hours);
  time_str += ":"+String(oTime.minutes);
  time_str += ":"+String(oTime.seconds);
  time_str += "."+String(oTime.milliseconds);
  return time_str;
}

translateToTimeObject()メソッドに、数値45296789が引数として渡されます。戻り値のObjectをさらにタイムイランの関数getTimeString()に渡して、返された文字列はtrace()関数により[出力]パネルに表示します。"12:34:56.789"が[出力]されれば、正しく処理が行われていると考えられます(図08-011)。

図08-011■静的メソッドtranslateToTimeObject()をタイムラインから呼出す

タイムラインにはも、Objectから時・分・秒・ミリ秒の各プロパティ値を取出して、文字列として返す関数getTimeString()を定義した。

静的メソッドtranslateToTimeObject()の動作が確認できたら、今度はMyTimerインスタンスを作成して、インスタンスメソッドgetElapsedTime()の処理を確かめます。再びStageのInteractiveObject.clickイベントにリスナー関数を登録しましたので、ステージをクリックするたびにMyTimerインスタンスに対してgetElapsedTime()メソッドが呼出されます。戻り値のObjectインスタンスはタイムラインに定義した関数getTimeString()に渡して、返された「時:分:秒.ミリ秒」の形式の文字列を[出力]パネルに表示します(図08-012)。

// タイムライン: メイン
// フレームアクション
var myObject:MyTimer = new MyTimer();
stage.addEventListener(MouseEvent.CLICK, xTrace);
function xTrace(eventObject:MouseEvent):void {
  var oElapsedTime:Object = myObject.getElapsedTime();
  trace(getTimeString(oElapsedTime));
}
function getTimeString(oTime:Object):String {
  var time_str:String = String(oTime.hours);
  time_str += ":"+String(oTime.minutes);
  time_str += ":"+String(oTime.seconds);
  time_str += "."+String(oTime.milliseconds);
  return time_str;
}
図08-012■MyTimerインスタンスを作成してgetElapsedTime()メソッドの呼出し

ステージをクリックするたびに、「時:分:秒.ミリ秒」の形式の文字列が[出力]パネルに表示。

08-03 戻り値をクラス化する
前記スクリプト08-004で、メソッドgetElapsedTime()はObjectインスタンスを返すようにしました。ただ、Objectインスタンスに設定するプロパティには、データ型が指定できません。時・分・秒・ミリ秒のプロパティに文字列など、数値以外を代入してもチェックのしようはないのです。また、プロパティはいくらでも加えられますから、ブロパティ名を間違えたとしても、気づかない可能性があります。

プロパティを限定し、データ型も指定できるようにするには、クラスを定義すればよいです。そして、そのインスタンスを、getElapsedTime()メソッドから返します。クラス名はMyTimerInfoとしましょう。例によって、新たなクラスMyTimerInfoをどのように使うかから考えます。

時・分・秒・ミリ秒のプロパティを保持するクラスの定義
MyTimerクラスのgetElapsedTime()メソッドで、MyTimerInfoインスタンスを生成して返します。MyTimerInfoのコンストラクタメソッドには、経過時間を示す総ミリ秒数を引数として渡します。MyTimerInfoインスタンスには、時・分・秒・ミリ秒の各プロパティが設定される予定です。すると、静的メソッドMyTimer.translateToTimeObject()は不要になります。これらの修正を加えると、MyTimerクラスはつぎのスクリプト08-005のようになります。

スクリプト08-005■getElapsedTime()メソッドはMyTimerInfoインスタンスを返すよう修正

// ActionScript 3.0クラス定義ファイル: MyTimer.as
package {
  public class MyTimer {
    private var my_date:Date;
    function MyTimer() {
      resetTimer();
    }
    public function resetTimer():void {
      my_date = new Date();
    }
    public function getElapsedTime():MyTimerInfo {
      var current_date:Date = new Date();
      var nElapsedTime:Number = current_date.time-my_date.time;
      var elapsedTime:MyTimerInfo = new MyTimerInfo(nElapsedTime);
      return elapsedTime;
    }
    /* public static function translateToTimeObject(nTime:Number):Object {   // メソッド削除
    } */
  }
}

なお細かい点として、getElapsedTime()メソッド内でMyTimerInfoインスタンスを保持するローカル変数名は、Objectの接頭辞oは取去ってelapsedTimeとしました(教科書的な考慮ですので、変更しなくても動作には差支えありません)。

Tips 08-009■[検索して置換]
スクリプト中の特定の変数名などをまとめて変更したいときは、[編集]メニューの[検索して置換]を使うことができます(図08-013)。ただし、識別子を後から変えるのは、修正漏れや間違った変更などのミスを招きやすい操作です。元のスクリプトは別にコピーしておくなどして、慎重に作業しましょう。

図08-013■スクリプト中の変数名を[検索して置換]

スクリプトの修正は、基本的に[大文字と小文字を区別]する。

クラスMyTimerInfoの定義については、とくに新たな知識は要りません。これまでの復習問題として丁度よい内容でしょう。まず、時・分・秒・ミリ秒のプロパティhours、minutes、seconds、millisecondsは、すべてint型で宣言します。

つぎに、コンストラクタメソッドが受取った総ミリ秒数の数値から各プロパティ値を計算して設定するには、前述スクリプト08-004でMyTimerクラスに定義した静的メソッドtranslateToTimeObject()の処理が応用できます。ただし、値の設定先はObjectインスタンスでなく、MyTimerInfo自身のプロパティです。この処理はメソッドsetTime()として定義し、コンストラクタからミリ秒値を渡して呼出すことにします(スクリプト08-006)。

スクリプト08-006■時・分・秒・ミリ秒のプロパティを保持するクラスMyTimerInfo

// ActionScript 3.0クラス定義ファイル: MyTimerInfo.as
package {
  public class MyTimerInfo {
    public var milliseconds:int;
    public var seconds:int;
    public var minutes:int;
    public var hours:int;
    public function MyTimerInfo(nMilliseconds:Number=0) {
      setTime(nMilliseconds);
    }
    private function setTime(nTime:Number):void {
      milliseconds = nTime%1000;
      nTime = Math.floor(nTime/1000);
      seconds = nTime%60;
      nTime = Math.floor(nTime/60);
      minutes = nTime%60;
      hours = Math.floor(nTime/60);
    }
  }
}

クラスの動作を確かめる
MyTimerInfoクラスが定義できたら、まず単体の動作を確かめます。フレームアクションで以下のようにクラスMyTimerInfoのコンストラクタを呼出し、引数として整数45296789を与えます。生成されたMyTimerInfoインスタンスから各プロパティ値を取出して、「時:分:秒.ミリ秒」のかたちの文字列にするために、また関数getTimeString()を利用します。関数の受取る引数のデータ型は、MyTimerInfoに変更しました。

// タイムライン: メイン
// フレームアクション
var myTime:MyTimerInfo = new MyTimerInfo(45296789);
trace(getTimeString(myTime));
function getTimeString(timeInfo:MyTimerInfo):String {
  var time_str:String = String(timeInfo.hours);
  time_str += ":"+String(timeInfo.minutes);
  time_str += ":"+String(timeInfo.seconds);
  time_str += "."+String(timeInfo.milliseconds);
  return time_str;
}

[ムービープレビュー]を確かめると、MyTimerInfoクラスのコンストラクタに渡した整数値45296789が時・分・秒・ミリ秒の各プロパティ値に設定されて、インスタンスが返されます。フレームアクションに定義した関数getTimeString()は、そのMyTimerInfoインスタンスからプロパティ値を取出して、文字列"12:34:56.789"のかたちで返しますので、その値がtrace()関数により[出力]パネルに表示されます(図08-014)。

図08-014■MyTimerInfoインスタンスのプロパティ値を確認

コンストラクタに引数として整数45296789を渡すと、時・分・秒・ミリ秒のプロパティ値が"12:34:56.789"として[出力]。

つぎは、MyTimerクラスも含めて、動作結果を確認します。タイムラインに記述するテスト用のフレームアクションは、MyTimerInfoクラスを定義する前のスクリプト08-004の確認時と基本的に同じです。getElapsedTime()メソッドの戻り値を格納する変数elapsedTimeと、関数getTimeString()の引数myTimeにMyTimerInfoで型指定している点が異なるだけです(厳密には、これらの変数および引数の名前も変えています)。

// タイムライン: メイン
// フレームアクション
var myObject:MyTimer = new MyTimer();
stage.addEventListener(MouseEvent.CLICK, xTrace);
function xTrace(eventObject:MouseEvent):void {
  var elapsedTime:MyTimerInfo = myObject.getElapsedTime();
  trace(getTimeString(elapsedTime));
}
function getTimeString(myTime:MyTimerInfo):String {
  var time_str:String = String(myTime.hours);
  time_str += ":"+String(myTime.minutes);
  time_str += ":"+String(myTime.seconds);
  time_str += "."+String(myTime.milliseconds);
  return time_str;
}

ステージをクリックするたびに、Stageオブジェクトに対するInteractiveObject.clickイベントのリスナー関数からMyTimer.getElapsedTime()メソッドが呼出されます。メソッドが返すMyTimerInfoインスタンスは関数getTimeString()に渡されて、プロパティが取出され、戻り値となる「時・分・秒・ミリ秒」の文字列値が[出力]パネルに表示されます(図08-015)。

図08-015■MyTimer.getElapsedTime()メソッドの返すMyTimerInfoインスタンスが文字列に変換されて[出力]

InteractiveObject.clickイベントで、リスナー関数からMyTimer.getElapsedTime()メソッドが呼出される。戻り値のMyTimerInfoインスタンスは、getTimeString()関数により「時・分・秒・ミリ秒」の文字列に変換され、その値が[出力]される。

getElapsedTime()メソッドの戻り値はMyTimerInfoインスタンスですので、前掲スクリプト08-004で返されたObjectインスタンスとは異なり、クラスの外部からスクリプトでプロパティを加えることはできません。また、MyTimerInfoクラスのpublicプロパティはすべて数値のint型で指定されていますから、異なる型のデータを代入すれば[コンパイルエラー]になります。

たとえば、タイムラインのフレームアクションで、getElapsedTime()メソッドの戻り値のMyTimerInfoインスタンスに、クラスで宣言されていないプロパティtestを追加したり、publicプロパティsecondsに数値でなく文字列"test"を代入しようとするとエラーが生じます(図08-016)。

図08-016■MyTimerInfoインスタンスにプロパティを追加したり数値以外のデータを代入するとエラー

MyTimerInfoクラスに定義されていないプロパティを追加したり、プロパティに宣言された型以外のデータを代入すると、[コンパイルエラー]が発生。

他方で、MyTimerInfoの時・分・秒・ミリ秒の各プロパティには、整数であればどのような値でも設定できてしまいます。たとえば、プロパティsecondsに150を代入すれば、そのまま150が値として設定され、minutesに2分繰り上がることはありません。

// タイムライン: メイン
// フレームアクション
var myObject:MyTimer = new MyTimer();
var elapsedTime:MyTimerInfo = myObject.getElapsedTime();
elapsedTime.seconds = 150;
trace(getTimeString(elapsedTime));   // 出力: 0:0:150.0
function getTimeString(myTime:MyTimerInfo):String {
  var time_str:String = String(myTime.hours);
  time_str += ":"+String(myTime.minutes);
  time_str += ":"+String(myTime.seconds);
  time_str += "."+String(myTime.milliseconds);
  return time_str;
}

08-04 get/setアクセサメソッド
プロパティに対して単なるデータ型の指定を超えて、値の範囲を限定したり、他のプロパティの値と関連づけたりするには、メソッドを使うしかありません。しかし、値を設定するという操作は、プロパティの方が端的でわかりやすいといえます。ActionScript 3.0には、プロパティと同じアクセスの仕方で呼出せる特別なメソッドがあります。それがgetおよびsetアクセサメソッドです。

get/setアクセサメソッドの定義
getおよびsetアクセサメソッドは、メソッドにgetおよびset定義キーワードを添えて定義します。

【get/setアクセサメソッドの定義】
private var プロパティ:データ型;
public function get アクセサメソッド():データ型 {
  return プロパティ;
}
public function set アクセサメソッド(引数:データ型):void {
  プロパティ = 引数;
}

getとsetのアクセサメソッドは、ともに同じ名前をつけます。これらのアクセサメソッドには、プロパティと同じようにアクセスできます。アクセサメソッドにはpublic属性を指定しつつ、内部的に用いるプロパティはprivateとするのが基本です。

たとえば、クラスMyClassに以下のようなget/setアクセサメソッドrotationを定義してみます。setアクセサメソッドの処理により、privateプロパティ_rotationには、つねに0から360までの数値が設定されます(Maniac! 05-002「小数値や負数の剰余演算」参照)。

// ActionScript 3.0クラス定義ファイル: MyClass.as
package {
  public class MyClass {
    private var _rotation:Number;
    public function get rotation():Number {
      return _rotation;
    }
    public function set rotation(nDegree:Number):void {
      _rotation = (nDegree%360+360)%360;
    }
  }
}

getおよびsetアクセサメソッドを定義すると、プロパティと同じように代入演算子で値を設定することができます。参照の仕方も、プロパティとまったく同じです。フレームアクションで上記クラスMyClassのインスタンスを生成すると、以下のとおりrotationがプロパティであるかのように値を設定し、参照することができます。

// タイムライン: メイン
// フレームアクション
var myObject:MyClass = new MyClass();
myObject.rotation = 450;
trace(myObject.rotation);   // 出力: 90
myObject.rotation += 270;
trace(myObject.rotation);   // 出力: 0

setアクセサメソッドrotationが、数値を0から360までの値に変換して設定します。したがって、プロパティを参照すれば、つねにこの範囲の数値が返されます(図08-017)。

図08-017■get/setアクセサメソッドはプロパティのように設定・参照が可能

setアクセサメソッドが、値を0から360の値に変換している。

AS1&2 Note 08-002■ActionScript 2.0のget/setアクセサメソッド
getおよびsetアクセサメソッドは、ActionScript 2.0から実装されました。メソッドの定義方法は、ActionScript 3.0ととくに異なる点はありません。

ただし、ActionScript 2.0のget/setアクセサメソッドは、代入演算子を連続して用いた場合(Tips 07-007「複数の変数・プロパティへの値の代入」参照)に問題があります。たとえば、前記MyClassと同じ内容を、ActionScript 2.0で定義したとします。

// ActionScript 2.0クラス定義ファイル: MyClass.as
class MyClass {
  private var _rotation:Number;
  public function get rotation():Number {
    return _rotation;
  }
  public function set rotation(nDegree:Number):Void {
    _rotation = (nDegree%360+360)%360;
  }
}

インスタンスにアクセサメソッドで値を設定し、さらにその結果を別の変数に代入すると、最初の代入文の右辺値でなくgetアクセサメソッドの戻り値が変数値になります。

たとえば、以下のフレームアクションで変数nには450が代入されるべきところ、実際にはgetアクセサメソッドの戻り値である90が設定されます(詳しくは、FumioNonaka.com「黙示的なsetメソッドの代入式で戻り値がgetメソッドの値になる」<http://www.fumiononaka.com/TechNotes/Flash/FN0402006.html>参照)。

// タイムライン: メイン
// フレームアクション
var myObject:MyClass = new MyClass();
var n:Number = myObject.rotation = 450;
trace([n, myObject.rotation]);   // 出力: 90,90

MyTimerInfoクラスにget/setアクセサメソッドを定義する
それでは、MyTimerInfoクラスに、getおよびsetアクセサメソッドを定義してみます。

get/setアクセサメソッドには、必ず対応するプロパティが存在しなければならない訳ではありません。MyTimerInfoクラスのコンストラクタメソッドに渡される総ミリ秒数さえ保持していれば、時・分・秒・ミリ秒の各値はいつでも計算することができます。また、各値を設定する場合も、総ミリ秒数に変更を加えれば足ります。

そこで、MyTimerInfoクラスのプロパティとしては、コンストラクタに渡される総ミリ秒数だけをprivate属性でtotalMillisecondsとして宣言することにします(スクリプト08-007)。つまり、hours、minutes、seconds、millisecondsは、get/setアクセサメソッドとしてのみ定義し、インスタンスにアクセスする際の見かけ上のプロパティとして扱われることになります。

また、時・分・秒・ミリ秒の値を設定するとき、総ミリ秒数に変更を加えるメソッドはsetTime()として定義します。このメソッドはpublic属性で指定しますので、インスタンスに対して時分秒ミリ秒すべての値を設定したいとき、タイムラインなど外部からアクセスすることが可能です。

スクリプト08-007■クラスMyTimerInfoにget/setアクセサメソッドを定義

// ActionScript 3.0クラス定義ファイル: MyTimerInfo.as
package {
  public class MyTimerInfo {
    private var totalMilliseconds:Number;
    public function MyTimerInfo(nMilliseconds:Number=0) {
      totalMilliseconds = nMilliseconds;
    }
    public function get milliseconds():int {
      var nMilliseconds:int = totalMilliseconds%1000;
      return nMilliseconds;
    }
    public function get seconds():int {
      var nSeconds:int = Math.floor(totalMilliseconds/1000)%60;
      return nSeconds;
    }
    public function get minutes():int {
      var nMinutes:int = Math.floor(totalMilliseconds/1000/60)%60;
      return nMinutes;
    }
    public function get hours():int {
      var nHours:int = Math.floor(totalMilliseconds/1000/60/60);
      return nHours;
    }
    public function set milliseconds(nMilliseconds:int):void {
      setTime(hours, minutes, seconds, nMilliseconds);
    }
    public function set seconds(nSeconds:int):void {
      setTime(hours, minutes, nSeconds, milliseconds);
    }
    public function set minutes(nMinutes:int):void {
      setTime(hours, nMinutes, seconds, milliseconds);
    }
    public function set hours(nHours:int):void {
      setTime(nHours, minutes, seconds, milliseconds);
    }
    public function setTime(nHours:int=0, nMinutes:int=0, nSeconds:int=0, nMilliseconds:int=0):void {
      nMinutes = nHours*60+nMinutes;
      nSeconds = nMinutes*60+nSeconds;
      nMilliseconds = nSeconds*1000+nMilliseconds;
      totalMilliseconds = nMilliseconds;
    }
  }
}

getアクセサメソッドは、その単位に繰り上がるのに必要なミリ秒数(分であれば1000×60ミリ秒)で割った商の整数部分を、ひとつ上の単位に繰り上がるのに必要な数(60分で1時間)で割った余りを取出します。

setアクセサメソッドは、setTime()メソッドを呼出して、総ミリ秒数を設定します。setTime()メソッドの引数に渡す時・分・秒・ミリ秒の値は、アクセサメソッドが受取った値以外は、getアクセサメソッドで取得します。

setTime()メソッドは、時・分・秒・ミリ秒の大きい単位から順に小さい単位に換算して、最終的に総ミリ秒値を求めて設定します。引数が指定されない場合のデフォルト値は、すべて0を指定しました。

以下のフレームアクションで、動作がどう変わったかを確かめることができます。hours、minutes、seconds、millisecondsはget/setアクセサメソッドとして定義しましたので、プロパティと同じようにアクセスして、値を設定・取得します。

// タイムライン: メイン
// フレームアクション
var myObject:MyTimer = new MyTimer();
var elapsedTime:MyTimerInfo = myObject.getElapsedTime();
elapsedTime.seconds = 150;
trace(getTimeString(elapsedTime));   // 出力: 0:2:30.0
elapsedTime.seconds -= 50;
trace(getTimeString(elapsedTime));   // 出力: 0:1:40.0
function getTimeString(myTime:MyTimerInfo):String {
  var time_str:String = String(myTime.hours);
  time_str += ":"+String(myTime.minutes);
  time_str += ":"+String(myTime.seconds);
  time_str += "."+String(myTime.milliseconds);
  return time_str;
}

[ムービープレビュー]を見ると、secondsに設定した150はminutesに値が繰り上がり、2分30秒(0:2:30.0)として扱われ、さらに50を差引くと1分40秒(0:1:40.0)になります(図08-018)。

図08-018■MyTimerInfoインスタンスのget/setアクセサメソッドにアクセスする

minutesに設定した150は2分30秒として扱われ、そこから50を差引くと1分40秒になる。

修正を加えたMyTimerInfoクラス(スクリプト08-007)は、時・分・秒・ミリ秒それぞれについてgetとsetのアクセサメソッドをひと組ずつ定義しましたので、メソッドの数は増えました。しかし、private属性の内部プロパティを総ミリ秒数のtotalMillisecondsひとつにしたために、処理内容はシンプルになっています。将来、たとえば時間の足し算や引き算のメソッドを拡張する必要が生じた場合も、対応はしやすいでしょう。

他方で、時・分・秒・ミリ秒の値を頻繁に設定・取得する処理が発生するような場合には、個々の値をそれぞれプロパティとして保持しておいた方が有利なこともありえます。get/setアクセサメソッドは、このように内部的な処理を外部からのアクセスとは切離して設計できるのが利点です。内部的なデザインを変更しても、外部からの見え方はシンプルなプロパティというかたちのまま変わりません。


外見は優しく、でも中身はミステリアス。


08-05 インスタンスの文字列表現をカスタマイズする ー toString()メソッド
インスタンスは文字列として表示すべき場合があります。trace()関数の引数に指定されて、[出力]パネルに表示されるときがそのひとつです。その際の表示の仕方を、インスタンスの文字列表現と呼びます(Tips 03-002「trace()関数ではインスタンスの文字列表現が[出力]される」)。

文字列表現が求められると、インスタンスに対してtoString()メソッドが呼出されます(Maniac! 03-002「trace()関数はインスタンスのtoString()メソッドを呼出す」参照)。クラスにメソッドが定義されていなければ、スーパークラスから継承されたtoString()が呼出されます。スーパークラスを継承の順にたどり、いずれにも定義がないときは、最終的にObject.toString()メソッドが呼出されます。Objectはすべてのクラスのルーツとなるスーパークラスだからです(06-06「Objectインスタンスの配列を使う」の「Objectはすべてのクラスのスーパークラス」参照)。

インスタンスの文字列表現をカスタマイズしたい場合には、クラスにtoString()メソッドをつぎのように再定義します。

【toString()メソッドの再定義】
public function toString():String {
  // 必要な処理
  return カスタマイズした文字列;
}

注意は2点です。第1に、文字列表現を返すのですから、もちろん戻り値は文字列Stringです。第2に、外部からtrace()関数を通してアクセスされたりしますので、public属性で指定する必要があります。

それではまず、クラスMyTimerにtoString()メソッドを定義してみましょう。文字列表現としては、privateプロパティmy_dateの日時情報をDateインスタンスと同じフォーマットで返すことにします(スクリプト08-008)。

スクリプト08-008■クラスMyTimerにtoString()メソッドを定義

// ActionScript 3.0クラス定義ファイル: MyTimer.as
package {
  public class MyTimer {
    private var my_date:Date;
    function MyTimer() {
      resetTimer();
    }
    public function resetTimer():void {
      my_date = new Date();
    }
    public function getElapsedTime():MyTimerInfo {
      var current_date:Date = new Date();
      var nElapsedTime:Number = current_date.time-my_date.time;
      var eapsedTime:MyTimerInfo = new MyTimerInfo(nElapsedTime);
      return eapsedTime;
    }
    public function toString():String {
      return my_date.toString();
    }
  }
}

クラスMyTimerに再定義したメソッドtoString()を試すには、以下のようなフレームアクションを記述すればよいでしょう。[ムービープレビュー]を見れば、MyTimerインスタンスを作成した日時の情報がDateインスタンスのフォーマットで[出力]されます(図08-019)。

// タイムライン: メイン
// フレームアクション
var myObject:MyTimer = new MyTimer();
trace(myObject);
図08-019■MyTimerクラスに定義したtoString()メソッドがインスタンス作成時の日時情報を返す

フレームアクションでMyTimerインスタンスをtrace()関数に引数として渡せば、作成時の日時が[出力]される。

Maniac! 08-002■オーバーライドとoverride属性キーワード
継承したスーパークラスのメソッドをサブクラスで再定義することは、「オーバーライド」(override)と呼ばれます。ActionScript 3.0では、オーバーライドするサブクラスのメソッドには、override属性を指定しなければなりません。

しかし、例外としてtoString()メソッドには、このoverride属性を指定しません(ヘルプ[ActionScript 3.0 コンポーネントリファレンスガイド] > [Object] > [toString()メソッド]参照)。メソッドtoString()overrideキーワードを用いると、[コンパイルエラー]になります。

つぎに、クラスMyTimerInfoにも、toString()メソッドを再定義しましょう。返す文字列表現は、privateプロパティtotalMillisecondsに設定された総ミリ秒数を、「時:分:秒.ミリ秒」のフォーマットとします(図08-009)。

スクリプト08-009■クラスMyTimerInfoにtoString()メソッドを定義

// ActionScript 3.0クラス定義ファイル: MyTimerInfo.as
package {
  public class MyTimerInfo {
    private var totalMilliseconds:Number;
    public function MyTimerInfo(nMilliseconds:Number=0) {
      totalMilliseconds = nMilliseconds;
    }
    public function get milliseconds():int {
      var nMilliseconds:int = totalMilliseconds%1000;
      return nMilliseconds;
    }
    public function get seconds():int {
      var nSeconds:int = Math.floor(totalMilliseconds/1000)%60;
      return nSeconds;
    }
    public function get minutes():int {
      var nMinutes:int = Math.floor(totalMilliseconds/1000/60)%60;
      return nMinutes;
    }
    public function get hours():int {
      var nHours:int = Math.floor(totalMilliseconds/1000/60/60);
      return nHours;
    }
    public function set milliseconds(nMilliseconds:int):void {
      setTime(hours, minutes, seconds, nMilliseconds);
    }
    public function set seconds(nSeconds:int):void {
      setTime(hours, minutes, nSeconds, milliseconds);
    }
    public function set minutes(nMinutes:int):void {
      setTime(hours, nMinutes, seconds, milliseconds);
    }
    public function set hours(nHours:int):void {
      setTime(nHours, minutes, seconds, milliseconds);
    }
    public function setTime(nHours:int=0, nMinutes:int=0, nSeconds:int=0, nMilliseconds:int=0):void {
      nMinutes = nHours*60+nMinutes;
      nSeconds = nMinutes*60+nSeconds;
      nMilliseconds = nSeconds*1000+nMilliseconds;
      totalMilliseconds = nMilliseconds;
    }
    public function toString():String {
      var time_str:String = String(hours)+":"+String(minutes)+":"+String(seconds)+"."+String(milliseconds);
      return time_str;
    }
  }
}

以下のフレームアクションを[ムービープレビュー]で試すと、ステージをクリックするたびに、経過時間が「時:分:秒.ミリ秒」のかたちで[出力]パネルに表示されます(図08-020)。リスナー関数xTraceに記述されたtrace()関数には引数としてMyTimerInfoインスタンスが直接渡されており、インスタンスから各プロパティ値を取出して文字列に変換する関数(getTimeString())がタイムラインに定義されていないことにご注目ください。

// タイムライン: メイン
// フレームアクション
var myObject:MyTimer = new MyTimer();
trace(myObject);
stage.addEventListener(MouseEvent.CLICK, xTrace);
function xTrace(eventObject:MouseEvent):void {
  var oElapsedTime:Object = myObject.getElapsedTime();
  trace(oElapsedTime);
}
図08-020■MyTimerInfoクラスに定義したtoString()メソッドが「時:分:秒.ミリ秒」の文字列を返す

リスナー関数xTraceに記述されたtrace()関数には、引数にMyTimerInfoインスタンスが直接渡されている。MyTimerInfoクラスに再定義されたtoString()メソッドにより、文字列表現として「時:分:秒.ミリ秒」のフォーマットで値が返される。

Column 08 メソッドもプロパティ
クラスはインスタンスのひな形となります(03-02「クラスとインスタンス」参照)。オブジェクト(インスタンス)を定義するものがクラスだといってもよいでしょう。そして、JavaやC++などのオブジェクト指向プログラミング言語においては、オブジェクトはデータを保持する「プロパティ」と、その操作手順を定めた「メソッド」で構成されるものと捉えられています。

オブジェクトとプロパティ
しかし、ECMAScript(Column 01「ECMAとECMAScript」参照)は厳密には、オブジェクトはプロパティの集まりだとしています。そして、プロパティにはデータ(変数)だけでなくメソッド(関数)も含まれます。つまり、ECMAScriptでは、メソッドもプロパティなのです。

Maniac! 08-003■ECMAScriptにおけるクラスとオブジェクト
「Proposed ECMAScript 4th Edition - Language Overview」(2007年10月23日。<http://www.ecmascript.org/>)p.9は、クラスについてつぎのように定めます。

クラスはオブジェクトを、そのプロパティ(フィールド)を示すことによって記述する。それらは定められたプロパティや固定値を明示し、変数や定数、およびメソッドを含む(筆者訳。"A class describes an object by presenting those properties (fields) of the object that are always present (the fixed properties or fixtures), including variables, constants, and methods.")。

また、「Standard ECMA-262: ECMAScript Language Specification」(第3版1999年12月。邦訳は<http://www2u.biglobe.ne.jp/~oz-07ams/prog/ecma262r3/>)の4.3.3「オブジェクト (Object)」も、つぎのように規定しています。

オブジェクトはObject型の構成要素である。序列のないプロパティの集合体で、それぞれのプロパティがプリミティブ値やオブジェクト、関数を含む。オブジェクトのプロパティに格納された関数はメソッドと呼ばれる。

[ヘルプ]のActionScript 3.0の解説には、クラスに定義されたインスタンスプロパティをインスタンス変数と表記している箇所が少なくありません([ActionScript 3.0のプログラミング] > [ActionScriptのオブジェクト指向プログラミング] > [クラス] > [クラス定義]や[変数]など)。また、メソッドまでも含んだプロパティという語と区別するために、インスタンス変数という用語を明示的に使っているActionScript 3.0の解説書も存在します(Colin Moock『Essential ActionScript 3.0』p.42)。

しかし、「ActionScript 3.0コンポーネントリファレンスガイド」では、クラスに定義された変数を「プロパティ」、関数を「メソッド」と記載しています。そして、他のオブジェクト指向プログラミング言語との細かな違いを議論する場合を除けば、あえてプロパティについて異なる定義を用いる必要性もあまりありません([ActionScript 3.0のプログラミング] > [ActionScript言語とシンタックス] > [言語の概要]およびFumioNonaka.com「ActionScript 3.0: 言語の概要」<http://www.fumiononaka.com/TechNotes/Flash/FN0802001.html>参照)。

したがって、本書では「ActionScript 3.0コンポーネントリファレンスガイド」や他のオブジェクト指向プログラミング言語と同じように、クラスに宣言された変数をプロパティと呼び、クラスに定義された関数であるメソッドとは区別することにしています。そして、あえてメソッドも含む意味で用いるときには、「広義のプロパティ」と表現することにします。

Maniac! 08-004■「ActionScript 3 Language Specification」
Adobeサイトには「ActionScript 3 Language Specification」(<http://livedocs.adobe.com/specs/actionscript/3/>)というドキュメントがあります。その中では、ECMAScriptの仕様に沿って用語が定義されています。

このドキュメントによれば、「オブジェクトはプロパティの集まり」(4.13「Object」)です。そして「プロパティは名前を備え、値またはメソッドをもつ」(4.14「Property」)ものとされます。

Function型の変数と関数の代入
広義のプロパティに関数が含まれるということは、変数に関数(function )が納められることを意味します。実際、変数のデータ型にはFunction型が存在します。そして、Function型を指定した変数には、関数が代入できます。

関数はその名前、つまり識別子により参照されます。タイムラインのフレームアクションに定義した関数は、その名前としてつけた識別子で、タイムラインにメモリされます。したがって、変数に関数を格納するには、関数の識別子を代入すればよいのです。

// タイムライン: メイン
// フレームアクション
var myFunction:Function = test;
function test():void {
   trace(this);
}
myFunction();

関数を代入した変数は、関数の識別子と同じように扱うことができます。つまり、変数に続けて演算子の括弧()を記述すれば、格納されている関数が呼出せます。上記フレームアクションを[ムービープレビュー]すると、[出力]パネルにはつぎのように表示されます(図08-021)。

[object MainTimeline]
図08-021■変数に代入された関数を呼出す

関数の代入された変数に続けて、演算子の括弧()を記述すれば関数が呼出せる。

関数には名前をつけなくても、文法上は問題なく定義できます。ただし、識別子がなければ、そのままではメモリされません。ですから、名前のない関数は、定義すると同時に、どこかに格納して保持する必要があります。メモリの代表は変数です。名前のない関数を直接変数に代入すれば、変数を参照して関数が呼出せます。なお、名前のない関数は、「匿名関数」あるいは「関数リテラル」と呼ばれることもあります(リテラルについては、Word 03-003「リテラル」参照)。


名前のない関数は、つくったその場でしまわないとダメ。

以下のフレームアクションは、名前のない関数を定義すると同時に変数myFunctionに代入しています。したがって、前掲フレームアクションと同じように、myFunction()という記述で、変数に格納された関数の処理を呼出すことができます(図08-022)。

// タイムライン: メイン
// フレームアクション
var myFunction:Function = function ():void {
  trace(this);
};
myFunction();
図08-022■名前のない関数を変数に代入して呼出す

名前のない関数も、定義と同時に変数に代入すれば、変数を参照して呼出せる。

関数をFunciton型データとして変数に格納する場合のもっとも大きな注意点は、代入する前には呼出せないということです(表08-002[II]。図08-023)。冷静に考えれば当たり前です。変数に数値を設定する場合でも、代入する前に変数を参照したら、値は入っていません。値があることを前提とした処理は、当然意図した結果にはならないでしょう。

しかし、識別子をつけて「function定義した関数は、スクリプトのステートメントが処理される前に、予めメモリに読込まれ」ました(Column 04「変数宣言と関数定義の初期化時期」)。ですから、記述のうえではfunction定義の前に呼出すことが可能です(表08-002[III])。この場合と混同しないように気をつける必要があるのです。

表08-002■関数の定義と呼出しの順序
[I]変数代入後に呼出し
OK
[II]変数代入前に呼出し
NG
[III]関数定義前に呼出し
OK
var myFunction:Function =
function ():void {
  trace(this);
};
myFunction();
myFunction();
var myFunction:Function =
function ():void {
  trace(this);
};
myFunction();
function myFunction():void {
  trace(this);
};

図08-023■関数を変数に代入する前には呼出せない

エラーコード#1006は、「存在しない関数を呼び出そうとした場合に発生」(ヘルプ[ActionScript 3.0コンポーネントリファレンスガイド] > [ランタイムエラー]参照)。

変数に代入した関数を利用する
変数に関数を代入して処理するという必要性は、あまり多くはないでしょう。参考までに、簡単なサンプルをご紹介します。

以下のフレームアクション(スクリプト08-010)は、設定したMovieClipインスタンスに対するアニメーションを、クリックするたびに切替えます。スクリプトの処理の流れは、つぎのとおりです。

[1] アニメーションの処理を行うふたつの関数は、それぞれ変数xRotateとxFollowに代入します。前者は、前掲スクリプト07-006と同じ内容で、マウスポインタの方向にMovieClipインスタンスを回転させます。後者は、前掲スクリプト04-004と同じ内容で、MovieClipインスタンスにマウスポインタの座標を追わせます。もうひとつFunction型で宣言した変数xAnimateを用意し、変数xRotateとxFollowのいずれかの参照を代入します。

[2] MovieClipインスタンスに対するイベントリスナーは、ふたつ設定します。まず、インスタンスをクリックした場合のInteractiveObject.clickイベントに、リスナー関数xSetAnimationを設定します。関数xSetAnimation()は呼出されるたびに、マウスポインタに対する回転(xRotate)と追随(xFollow)のふたつのアニメーションを切替えます。つぎに、インスタンスのDisplayObject.enterFrameイベントに、リスナー関数xEnterFrameを設定します。関数xEnterFrame()は、設定されたアニメーションを毎フレーム実行します。

スクリプト08-010■変数に代入する関数の切替えによりアニメーションを変化させる

// MovieClip: アニメーションさせるインスタンス
// [0] 初期値設定
var nRadianToDegree:Number = 180/Math.PI;
var nDeceleration:Number = 0.2;
var bFlag:Boolean = false;
// [1] アニメーションの関数ふたつを変数に設定
var xRotate:Function = function ():void {
  var nRadian:Number = Math.atan2(mouseY, mouseX)*nRadianToDegree;
  rotation += nRadian*nDeceleration;
};
var xFollow:Function = function ():void {
  x += (parent.mouseX-x)*nDeceleration;
  y += (parent.mouseY-y)*nDeceleration;
};
var xAnimate:Function = xRotate;
// [2] イベントリスナー設定
addEventListener(MouseEvent.CLICK, xSetAnimation);
addEventListener(Event.ENTER_FRAME, xEnterFrame);
// [3] アニメーションの処理
function xEnterFrame(eventObject:Event):void {
  xAnimate();
}
function xSetAnimation(eventObject:MouseEvent):void {
  if (bFlag = !bFlag) {
    xAnimate = xFollow;
  } else {
    xAnimate = xRotate;
  }
}

[3] 上記フレームアクションを設定したMovieClipインスタンスのアニメーションは、ふたつのリスナー関数が処理します。リスナー関数xEnterFrame()は、単純にxAnimate()を呼出します。Function型変数のxAnimateには、回転のxRotateか追随のxFollowのいずれかの関数の参照が代入されています。したがって、DisplayObject.enterFrameイベントが呼出される毎フレーム、設定されたアニメーションの処理を繰返します。

InteractiveObject.clickイベントにのリスナー関数xSetAnimation()は、インスタンスがクリックされるたびに、変数xAnimateに設定する関数の参照を切替えます。[0]の初期値設定で宣言された変数bFlagには、Boolen型データのtruefalseかが設定されます(初期値はfalse)。if条件で変数には、論理否定演算子!によりtruefalseが逆転して代入されます。したがって、関数xSetAnimation()は呼出されるたびに、ifステートメントとelseステートメントとを交互に処理します。つまり、Function型変数のxAnimateに代入されるアニメーションの関数が毎回切替わることになります。

Word 08-002■論理否定演算子!
つぎのシンタックスで、式のブール(論理)値としての評価を反転します。

!式

「式」には、前述(Word 03-004「」)のとおり、単一の変数や単純な数値も含まれます。式はまずブール(論理)値として評価され、その結果の値のtruefalseを反転したBoolean型の値が返されます。

式がブール(論理)値以外の値を返す場合、それをブール(論理)値として評価した結果については、前掲表05-001をご参照ください。


Tips 08-010■if条件に指定した代入式
前述(Tips 05-003「if条件の評価」)のとおり、if条件に代入式を指定することも、文法として問題はありません。この場合、まず代入が行われた後、代入式の返す値が条件として扱われます。代入式は代入した右辺値を返しますので、結局代入値がif条件として評価されることになります。

[ムービープレビュー]を見ると、まずMovieClipインスタンスはマウスポインタの方向に回転します。つぎに、インスタンスをクリックすると、マウスポインタの座標を追いかけるようになります。そして、インスタンスをクリックするたびに、回転と追随のアニメーションが切替わります(図08-024)。

図08-024■インスタンスのクリックごとに変数に設定したアニメーションの関数を切替える

インスタンスをクリックするたびに、マウスポインタの方向に向かった回転と、座標への追随のアニメーションが切替わる。

Tips 08-011■フラグ
前記スクリプト08-010においてBoolean型変数bFlagの値は、MovieClipインスタンスのアニメーションがそのとき回転(xRotate)と追随(xFollow)のどちらの処理になっているかを示します。値がfalseなら回転で、trueであれば追随の状態です。

条件により処理を分岐させるため、現在の状態を変数に保持しておく場合、その変数の役割を「フラグ」と呼びます。スクリプト08-010のように状態がふたつ存在するときはブール(論理)値を使うことが多く、3つ以上あれば数値や文字列が用いられる場合もあります。

フラグ(flag)は旗を意味し、目印の役割を果たすことからこの名がつけられたと想像されます。第1回日本アカデミー賞作品賞に輝いた山田洋次監督の『幸福の黄色いハンカチ』は、刑務所帰りの主人公がヒロインに書いた、こんな手紙がクライマックスにつながります。「もし、まだ待っててくれるなら、黄色いハンカチをぶらさげてくれ……」このハンカチこそが、まさに目印(フラグ)です。映画のタイトルが示すとおり、主人公は青空にはためく無数の黄色いハンカチに迎えられます。


フラグは目印。

[*筆者用参考] Wikipedia「フラグ (コンピュータ)」、JavaA2Z「フラグ」、goo映画「幸福の黄色いハンカチ」、「幸せの黄色いハンカチ」。

もっとも、イベントリスナーは、いつでも登録したり削除したりすることができます。したがって、Function型の変数を使わなくても、イベントリスナーを切替えれば、アニメーションを変化させることができます(スクリプト08-011)。

スクリプト08-011■リスナー関数の切替えによりアニメーションを変化させる

// MovieClip: アニメーションさせるインスタンス
// [0] 初期値設定
var nRadianToDegree:Number = 180/Math.PI;
var nDeceleration:Number = 0.2;
var bFlag:Boolean = false;
// [1] イベントリスナー設定
addEventListener(MouseEvent.CLICK, xSetAnimation);
addEventListener(Event.ENTER_FRAME, xRotate);
// [2] アニメーションの関数定義
function xRotate(eventObject:Event):void {
  var nRadian:Number = Math.atan2(mouseY, mouseX)*nRadianToDegree;
  rotation += nRadian*nDeceleration;
}
function xFollow(eventObject:Event):void {
  x += (parent.mouseX-x)*nDeceleration;
  y += (parent.mouseY-y)*nDeceleration;
}
// [3] アニメーションの切替え
function xSetAnimation(eventObject:MouseEvent):void {
  if (bFlag = !bFlag) {
    removeEventListener(Event.ENTER_FRAME, xRotate);
    addEventListener(Event.ENTER_FRAME, xFollow);
  } else {
    removeEventListener(Event.ENTER_FRAME, xFollow);
    addEventListener(Event.ENTER_FRAME, xRotate);
  }
}

イベントリスナーの登録を抹消するには、EventDispatcher.removeEventListener()メソッドを使います(Word 08-003)。ふたつの引数の指定は、EventDispatcher.addEventListener()メソッドと同じです。

Word 08-003■EventDispatcher.removeEventListener()メソッド
つぎのシンタックスで、インスタンスからリスナー関数を取除きます。

インスタンス.removeEventListener(イベント:String, リスナー関数:Function):void

EventDispatcherあるいはそのサブクラスのインスタンスの指定したイベントに対して登録されたリスナー関数の参照を削除します。引数に渡したリスナー関数が登録されていなければ、とくに効果はありません。

イベント ー イベントの種類を文字列で指定します。Eventまたはそのサブクラスに定義されたイベント定数を用いることが推奨されます(イベント定数については、02-07「イベントのことをもう少し知ろう」参照)。

リスナー関数 ー 登録を取除くべき関数です。指定イベントが発生したときに呼出されるようEventDispatcher.addEventListener()メソッドで登録された関数を指定します。登録された関数の参照は、本メソッドにより削除されます。

なお、本メソドにはオプションの(省略可能な)第3引数があります。その内容については、[ActionScript 3.0コンポーネントリファレンスガイド] > [EventDispatcher.removeEventListener()メソッド]をお読みください。

[Prev/Next]


作成者: 野中文雄
更新日: 2008年6月6日 参照先の不備を補足。
更新日: 2008年6月5日 08-02「プロパティとメソッドを定義する」の「プロパティとメソッドのアクセス制御」の項でサンプルスクリプト1箇所修正。
更新日: 2008年4月27日 Tips 08-011に加筆。
作成日: 2008年3月8日


Copyright © 2001-2008 Fumio Nonaka.  All rights reserved.