リセット関連の罠(RGSS・RGSS2共通)

組み込みモジュール・クラスに対して普通にaliasを使った場合、 F12でリセットしたときに「stack too deep」みたいなエラーが出ます。

● 原因

おそらくリセット時の動作が[ 例外派生→スクリプトの再読み込み ] となっている為だと思います。組み込み系はそのまま、スクリプトだけが読み込まれるので 組み込み系に対しては2度目のaliasが実行されてしまい無限ループになってしまっているようです。

例)
元のクラス定義
# Script1
class Kumikomi
  def initialize
    p "元の処理"
  end
end
Script1に処理を追加する場合次のようにaliasを使う方法がよくとられます。
# Script2
class Kumikomi
  alias :_tuika_initialize :initialize
  def initialize
    _tuika_initialize
    p "追加処理"
  end
end
これは
class Kumikomi
  def _tuika_initialize
    p "元の処理"
  end
  def initialize
    _tuika_initialize
    p "追加処理"
  end
end
と書くのとほぼ同じ処理になるのですが、Script2を再度実行してしまうと以下のようになってしまいます
class Kumikomi
  def _tuika_initialize
    _tuika_initialize
    p "追加処理"
  end
  def initialize
    _tuika_initialize
    p "追加処理"
  end
end

「_tuika_initialize」内で自身を呼び出すようになり無限ループになってしまっています。 無限ループ→「stack too deep」となるわけです。

● 対処法1

スクリプトの最後(mainの直前)にグローバル変数を設定しそれで判別

# Script2_Fixed2
class Kumikomi
  unless $teikitou_script_loaded
    alias :_tuika_initialize :initialize
    def initialize
      _tuika_initialize
      p "追加処理"
    end
  end
end

# 後ろの方
$teikitou_script_loaded = true

リセットしてもグローバル変数はそのままなので一度最後まで読まれたかどうかの判断に使えます。

スクリプト素材等では素材の最後にグローバル変数を定義したりする事になると思いますが、 その場合はまず他と重ならないようなユニークな名前にする必要があります。

● 対処法2

"$@"を使う
リセットを行った場合エラー情報が"$@"に入るのでそれによって判別する方法
# Script2_Fixed2
class Kumikomi
  unless $@
    alias :_tuika_initialize :initialize
    def initialize
      _tuika_initialize
      p "追加処理"
    end
  end
end

"$@"に値が入っていたらリセット後なので二度目の"alias"を実行しないようにしています。

リセット以外で例外情報が入っていたら?とかどこかで"$@"がクリアされていたら?とかが気になるので 個人的には好きな方法ではありません。実際そんな状況はまずないと思いますが。

● 対処法3

"method_defined?"等を使って判別する
# Script2_Fixed3
class Kumikomi
  unless private_method_defined?(:_tuika_initialize)
    alias :_tuika_initialize :initialize
    def initialize
      _tuika_initialize
      p "追加処理"
    end
  end
end

aliasをによって定義される「_tuika_initialize」が定義されているかどうかで判別しています。

元のメソッドがpublicメソッドなら"method_defined?", privateメソッドなら"private_method_defined?" 使えばたぶんOKです。

"initialize"メソッドは何も指定していなくても自動でprivateになります

これらの対処法は対策が必要ではないaliasの部分に対して行ってもデメリットはありません。 むしろ無駄に2回目のaliasを実行しなくていいのでかすかにメリットがあるくらい。 とりあえずaliasを使う時には書いておく位でもいいと思います。

● その他

継承されたメソッドにも罠が!!

組み込み系以外でもスーパークラスから継承されたメソッドをオーバーライドせずにそのままaliasしている 場合はちゃんと対策する必要があります。

# Script4 (faled case)
class Parent
  def test
    p "Parent"
  end
end
class Child < Parent
  alias :_tuika_test :test
  def test
    _tuika_test
    p "Child"
  end
end

上の例では

  def test
    _tuika_test
    p "Child"
  end

の部分でオーバーライドされているので、リセット後の

  alias :_tuika_test :test

では"Parent"クラスの"test"メソッドではなく"Child"クラスで新たに定義された "test"メソッドが利用される事になります。 結果的に組み込み系への"alias"と同じような状況になります。

この場合も上記の対処法で対処できます。

読み込まれる順番

組み込み系
 Script1 → Script2 → (Reset) → Script2
その他
 Script1 → Script2 → (Reset) → Script1 → Script2

その他

リセット時にグローバル変数等はリセットされません。またrequireも読み込み情報がそのままなので 読み込まれません。※1 リセット後も読み込みたい場合はload等を使った方がいいかもしれません。※2


※1
「require」で読み込まれたファイルのパスはグローバル変数「$"」に格納されます。 リセットではグローバル変数は初期化されないので読み込み済みとみなされます。
※2
「require」や「load」で外部ファイルを読み込む方法はセキュリティ面で難があります。 外部ファイルを改変されることで暗号化アーカイブの中身が見られたりする可能性もあります。