不要なメンバー変数・メンバー関数を減らすコツ
どうもtaroです.
ちょっとした小ネタなのですが、変数のスコープを上手く利用して、不必要なメンバー変数やメンバー関数、クラスを減らす方法というのをご紹介いたします。
LoaderやURLLoaderを使う時、非常に良く出くわす問題として、一回しか使わないような変数をprivateなメンバー変数としてしまうことがありますが、僕は前から何だかこの書き方に少し疑問を持っていました。
private var _ldr:URLLoader; private var _data:Object; public function load():void { _ldr = new URLLoader(); _ldr.addEventListener(Event.COMPLETE, onLoaded); _ldr.load(new URLRequest(API_URL)); } private function onLoaded(e:Event):void { _data = JSON.decode(_ldr.data); }
例えば、このコードにおける_ldrやonLoaded等の存在意義です。特に問題は同じクラス内にloadedみたいなメソッドやロードするためのldr見たいな変数が複数存在するときに深刻化します。
実は、クロージャーを上手く使えばこのような一時変数のメンバー変数化が避けられます。
private var _data:Object; public function load():void { var ldr:URLLoader = new URLLoader(); ldr.addEventListener(Event.COMPLETE, function (e:Event):void { _data = JSON.decode(ldr.data); }); ldr.load(new URLRequest(API_URL)); }
e.targetを使うという話もありますが、そもそもonLoadedというメソッドが不要な気がします。また、イベント・ハンドラ内で複数回e.targetを書いたり、それを避けるために
var ldr:URLLoader = e.target as URLLoader;
なんて、コードを書くのもあまりスマートでない気がします。無名関数を使った書き方が慣れないという方は、ローカル関数を使って書くと、そこまで違和感を感じないかもしれません。
private var _data:Object; public function load():void { var ldr:URLLoader = new URLLoader(); ldr.addEventListener(Event.COMPLETE, onLoaded); ldr.load(new URLRequest(API_URL)); function onLoaded(e:Event):void { _data = JSON.decode(ldr.data); } }
このような書き方をすると、非同期な処理も一つのメソッド内に全部書ききることができ、一連の処理とその処理に使う変数が全て一つのメソッド内で完結するので見通しがよくなります。また、非常にオブジェクト指向的な書き方とも言えるでしょう。
注意点0
変数やメソッドを一つのメソッドの中に閉じ込めて書くときの注意点として、そのメソッドを抜けてしまうと参照が無くなってしまうことです。以下のコードは、期待通りに動きません。
private var _data:Object; public function load():void { var ldr:URLLoader = new URLLoader(); ldr.addEventListener(Event.COMPLETE, onLoaded, false, 0, true); ldr.load(new URLRequest(API_URL)); function onLoaded(e:Event):void { _data = JSON.decode(ldr.data); } }
イベントハンドラを使うとき、弱参照を使うのを習慣としていらっしゃる方も中にはいるかもしれませんが、関数の中にイベントハンドラを閉じ込めた場合、弱参照を用いてしまうと、関数のスコープを抜けた瞬間に参照が切れてしまい、イベント・リスナから外されてしまいます。
なので、明示的にイベントハンドラをremoveする必要が生じます。無名関数を使った場合のイベントハンドラの外し方については、コチラのエントリに説明がございます。
注意点1
コチラも有名なハマリどころですが、for文とクロージャーの併用時には、一時変数のスコープにお気をつけ下さい。
private function makeButtons():void { var button:Button; for (var i:int = 0; i < 5; ++i) { button = new Button; button.y = i * button.height + 20; button.label = "button " + i; button.addEventListener(MouseEvent.CLICK, function (e:MouseEvent):void { textField.text = "button " + i + " clicked!"; }); addChild(button); } }
さて上のコードは期待通りの動作をするでしょうか?
このコードではどのボタンを押しても"button 5 clicked!"となってしまいます。
これは、イベント・ハンドラが呼ばれたとき、変数iの参照を辿ると、makeButtonsの中の処理が全て終わったときのiの値となってしまうからです。これを回避する方法として、
private function makeButtons():void { var button:Button; for (var i:int = 0; i < 5; ++i) { button = new Button; button.y = i * button.height + 20; button.label = "button " + i; button.addEventListener(MouseEvent.CLICK, (function (j:int):Function { return function (e:MouseEvent):void { textField.text = "button " + j + " clicked!"; }; })(i)); addChild(button); } }
のように高階関数を1つ挟んで使う方法も考えられますが、少し冗長です。forEachはこのようなループの一時変数への参照の問題を上手く解決してくれます。
特に似たパーツへの処理がある場合は、個々のパーツへの参照よりも、それらの配列への参照がいたる所で使われる可能性が高いです。
buttonsは下のコードではローカル変数となっていますが、priivateなメンバ変数としても良いかもしれません。
private function makeButtons():void { var buttons:Array = []; for (var i:int = 0; i < 5; ++i) { buttons.push(new Button); } buttons.forEach(function (button:Button, index:int, arr:Array):void { button.y = index * button.height + 20; button.label = "button " + index; button.addEventListener(MouseEvent.CLICK, function (e:MouseEvent):void { textField.text = "button " + index + "clicked!"; }); addChild(button); }); }
無論、このボタンとindexを含むクラスを作ってしまうというのも一つの解法ですが、クロージャーの使用の一つのメリットとしては、
- わざわざクラスにするほどでもない小さな処理の纏まりを同じファイルにかける。つまり、複数ファイルを見なくとも処理が一望できる。
- 一時的なメソッド、変数をprivateなものとしてクラスに紐付けなくて済む。
等ということが考えられます。ただ、余りにクロージャーが肥大化するのであれば、それをクラス化する方がよいでしょう。
ということで、物凄く概念的なお話でしたが、適切にクロージャーを使うことでよりオブジェクト指向的な書き方が出来ることもありますね、ということでした。それでは。