2014年10月7日火曜日

Maxscript その15 「変数のスコープ グローバル変数とローカル変数」

「リファレンス>変数>変数のスコープ」

maxscriptでは変数の定義が自動でされるので、好きな時に好きな変数を使える。しかし、好き勝手に変数が使えるかというと、そんな事はなく、多くの言語同様に変数には変数のスコープと呼ばれる変数の有効範囲が存在しているようだ。ここでは変数のスコープについて見ていってみようと思う。

maxscriptの変数のスコープには大きく分けて「グローバル変数」「ローカル変数」の2つの種類があるようだ。

グローバル変数はどこでも呼び出せるが、ローカル変数はある特定の構文内でしか使用する事が出来ないようだ。ブロック式を例に見たとき

(
  a = 10
  b = 30
  c = b - a
)

とした場合、ブロック式内で使用されているa、b、cはローカル変数となり、ブロック式内でしか有効ではない。

x = 20
y = 5.0
z = x/y
(
  a = z
  b = 30
  c = b - a
)
print c

とした場合、ブロック式の外で使われる変数xyz はグローバル変数として定義されるのでブロック式内でも使用出来るようだ。ブロック式内で定義された変数cをブロックの外で呼び出すと、変数cはブロック内でしか有効ではないローカル変数であるため、print cの実行結果は

undefined

となる。(後ほど触れるが、このスクリプトは2回目以降実行した際のprint cの実行結果はundefinedとはならない)

グローバル変数は起動しているmaxが終了するまで値を保持するが、ローカル変数は有効な範囲内でのみ値を保持するようだ。なので、一度グローバル変数として定義されれば、その変数は起動しているmax内に限り、別のスクリプトでも使用する事が出来るようだ。(リスナーで呼び出す事も可能)

一方、ローカル変数の有効範囲は、関数、ループ式、ブロック式の開始から終了までのようだ。

また、ローカル変数には段階的に有効範囲があるようだ。

とした場合、レベル1のブロックで宣言されているローカル変数abcはレベル1のブロック内である、レベル2の中でも有効だが、レベル2で宣言されたローカル変数uvwはレベル2のブロック内でしか有効ではないのでレベル2以外で変数uvwを呼び出すと「undefined」となるようだ。
レベル毎インデントを揃える事は、このような事を明確化するのにもとても役に立つのが分かる。
---------------------------------------------------
maxscriptにおける変数の初期化について


変数のスコープは変数を初めて使用する際に定義される。値を代入しない際、値は「undefined値」となる。
変数の型は変数に値が代入される際に定義される

scriptリファレンスには次のようにある。

暗黙的な変数宣言
変数を明示的に宣言せず、その変数名が上のレベルのスコープに存在しない場合は、その変数を初めて使用するとき MAXScript によって変数が作成され、特別な値 undefined を保持するために変数が初期化されます。変数を使用する前に、変数を明示的に宣言したり値を初期化する必要はありません。前のステートメントで明示的に宣言されていない変数は、暗示宣言変数と呼ばれます。暗示的に宣言された変数のスコープは、変数が最初に使用されたときの MAXScript のスコープ コンテキストになります。

この「暗示的な変数宣言」の仕様は便利な一方で「変数のスコープによるエラー」やバグを起こす理由の1つでもあるように感じる。(pythonなどでは未定義の変数を呼び出すとエラーとなるため、この問題が避けられるがmaxscriptではエラーとはならないため「暗示的な変数宣言」が問題となる事がある)

明示的な変数の宣言(変数の初期化)について
構文 ( local | global ) <decl> { , <decl> }

リファレンスの見方について(これが本当に分かりづらい・・・)
| ・・・orを意味する。どっちからしい。
{}内 ・・・ 必要がある際に記述する事が出来る。実際記述する際に{}は書かない。
<> ・・・ 式や値など様々なものを意味する。
ここで、<decl> は次のように定義されます。
<ver_name> { = <expr>} <ver_name>は変数名、代入式でも可。

maxscriptの変数には「スコープ(有効範囲)」と「型(タイプ)」という2つの属性があるらしい。基本的にどちらも自動的(暗黙的)に定義してくれるようなので、あえて定義する必要は基本的にはないようだが、変数を明示的に定義するための「global」「local」という命令が準備されているようだ。

エディター(もしくはリスナー)で
x = 20

と実行した場合、変数xの属性は「global変数」に定義され、型は「integer」となるようだ。これを明示的に行う場合は

global x = 20

とするようだ。また、

global x

とした場合の変数xの属性は「global変数」となり、型は「undefined」となるようだ。「未定義が定義される」非常にややこしい・・・。
,(コロン)を用いることで複数まとめて宣言が出来るようだ

global x = 10, y = 5
もしくは
global x,y
x = 10
y = 5

ネットなどで落としたスクリプトの出だしが、文字の羅列だけで始まっていたりすることがある。あれは「暗黙的な宣言」を用いて「global」を省略し変数をglobal変数として定義しているようである。
<例>
renderlist
materiallist
objectlist

これは
global renderlist
global materiallist
global objectlist

と同義のようである。全てを明示的に記述すると

global renderlist = undefined
global materiallist = undefined
global objectlist = undefined

となる。

ローカル変数の宣言はlocalを用いる。例えばブロック式内で

(x1 = 10)

とした場合の変数x1はローカル変数となる。これを明示的に行う場合は

(local x1 = 10)

とするようだ。
グローバルな領域でローカル変数を定義しようと

local x = 10

などとすると、おそらくエラーになると思う。あるスクリプト内だけで使われるローカル変数を定義したい場合は
(
   x = 10
   y =  20
   z = x + y
   print z
)
など、スクリプト全体をブロックで囲んであげればよいようだ。

ローカルな領域で
(
    global x = 10
)
と定義した場合、変数xは以降グローバル変数となるようだ。

明示的な変数宣言の必要性について
maxscriptでは変数の定義、初期化が自動で行われるため、必ずしも明示的な変数宣言を行う必要はないように思う。一方で、「暗黙的な変数宣言」の仕様が理解できていないと、バグの原因になったりするなどスクリプトが正しく動作しない理由となってしまったりするようにも思う。明示的な変数宣言のメリットをいくつか考えてみたいと思う。
------------------------------------
1.最初に明示的に変数の宣言を行う事でコードに使用している変数名やスコープが分かりやすくなる。特に長いコードを書く際や他の人が見た際などに有効。readme的な役割にもなる。コメントアウトと組み合わせるとさらに有効。

global x,y,z --変数x,y,zをグローバル変数として定義。オブジェクトの位置情報
x = 10
y = 5
z = x + y
(
    local u,v,w --ブロック内で使用する変数u,v,wをローカル変数として定義
    u = x
    v = y
    w = u * 0.5 + v * 0.5
)
といったように、そのスクリプト内で扱われている変数名と属性が分かりやすくなる。
------------------------------------
2.「暗示的な変数宣言」によるバグの回避
maxscriptでよくある「初回実行時のみエラーとなるスクリプト」もしくは「maxを再起動したらスクリプトが正しく動かなくなった」等のバグの回避になる。

(
    x1 = 20
)
print x1

と初回実行した場合、print x1の実行結果は「undefined」となる。この場合の変数x1がどのように暗示的に定義されているか考えてみると・・・・まず、ブロック式が実行され、ブロック式内で初めて使用されるx1はそのブロック内で有効な「ローカル変数」となる。ブロック式終了時に変数x1は破棄される。
次に、ブロックの外でprint x1とし変数x1を呼び出す。変数x1は破棄されているので、ここで初めて使われる事として、再び変数の定義がされるがx1を呼び出しているのがグローバルな領域なので、変数x1は「global変数」「undefined値」として初期化されるようだ(自信はないが、振る舞い的にそうであると想像する)。

同じスクリプトを再び実行する。今度はx1は既に「グローバル変数」と定義されているので、20という値が代入され、ブロックが終了してもその値が保持される。結果、print x1 とした場合、リスナーには「undefined」ではなく「20」と表示され「初回の実行結果と異なる」という事が起きる。

このように動的な変数のスコープの変化を望まない場合、変数の宣言を明示的に行ってあげればよいようだ。

グローバル変数として使いたければ
global x1
(
    x1 = 20
)
print x1

とすれば変数x1ははじめにglobal変数と定義されるので、print x1の初回時の実行結果も「undefined」とはならずに「20」となる。もしくは

x1
(
    x1 = 20
)
print x1

としても同義。

x1を常にlocal変数として扱いたい場合は
(
    local x1 = 20
)
print x1
とすれば、変数x1は「ローカル変数」として振る舞うため、print x1の実行結果は常に「undefined」となる。ただし、変数の定義を明示的に行ったからと言って、「暗黙的な定義」が行われなくなる訳ではないようなので、この書き方の場合、最終的にはprint x1の部分で変数x1の属性は「グローバル変数」に定義されてスクリプトが終了するようだ。

なので、もし、常にx1をローカル変数として使いたい場合は
(
    local x1 = 20
    print x1
)
とprintもブロックの中に入れる必要があるようだ。明示的な宣言を行わない場合も「暗示的な定義」がグローバルな領域でされないように

(
    x1 = 20
    print x1
)

など、処理がされる場所に気を付けてみたり。この辺り、スクリプトの実行結果は同じもののスクリプトの内部処理が異なってくるので難しい部分でもある。

global x1
(
    local x1 = 20
)
print x1
等とすると、変数の属性を途中で変更する事も出来るようだが、変数の属性を途中で変えるような処理は色々と問題を起こす気がするのであまりお勧めはしない。(また、この場合だと最終的に変数x1はグローバル変数となると思う)

もし、既に定義して使われたglobal変数の値を初期化したい場合は

x1 = undefined

等、当たり障りのない値を代入してやれば初期化する事が出来る(と思う。ただ、本当の意味でglobal変数の初期化を行いたいのであれば、一度maxを終了する必要があると思われる。)

リスナーで変数を使う場合も注意したい。スクリプトが正しく動かず、変数の中身がどうなっているかリスナーで確認しようと
x1
としてエンターして「undefined」と表示され「やっぱり未定義だよなー。」となったとする。この場合、この瞬間に変数x1は「グローバル変数」として定義され、スクリプト内でx1を明示的に宣言していない場合、maxが終了されるまでその属性と値を持ち続ける事になる。もし、スクリプトが正しく動かない理由が変数のスコープによるものだった場合はこのような行為も問題となったりする事もある。
------------------------------------
3.特定の構文内では「暗黙的な変数宣言」が行われない場合がある?
maxscriptは仕様が厳格であるとは言いずらい印象がある。

<例:rollout構文>

rollout r_test "test"(
    w = 100
    button b01 "test" width:w
)

とした場合、ロールアウト構文内で定義される変数wは「暗黙的な変数宣言」が行われないのかエラーとなってしまう。(3dsmax2014現在、※シンタックスエラーになる。ロールアウト構文内では認められない式の書き方のようだ。)

rollout r_test "test"(
    local w = 100
    button b01 "test" width:w
)

と、変数の宣言、初期化をしてあげるとエラーは出さなくなる。(構文エラーにならない)

シンタックスエラー(Syntax error) = 構文エラー = プログラムとして文法が間違っている
 詳細は不明だが、maxscriptでは「評価」を行うと、スクリプトが実行される前に文法に問題ないかの構文チェックを行うようだ。問題がなければコンパイルが行われ処理が開始される。文法に問題があった場合Sytax errorを返し、スクリプトが実行される前に処理が中断されるようだ。文法の間違いなのでコードを目視する事で問題点を見つける事が出来るエラーとなる。
 maxscriptのエラーは、構文チェックのエラー、コンパイル時のエラー、実行時のエラーと段階的なエラーと種類があるようだ。
------------------------------------
と、色々あるようなので、変数の明示的な宣言は面倒でなければ出来るだけ行った方がよいと言えるのではないかと思ったりもする。

global変数とlocal変数の使い分けについて
global変数である必要がないものは出来るだけlocal変数を用い、global変数は出来るだけ使わないように努める。極端な例だとスクリプト全体をブロックで囲んでしまう等。
(
    a = 10
    b = 20
    c = a + b
    print c
)

理由としてグローバル変数は起動しているmaxが終了するまで値を保持する」があげられる。ここでいう「値を保持する」は「物理的にメモリを使用する」という事だと思われる。global変数は起動しているmax内どこでも使えて便利な一方で「maxが終了するまでメモリに情報を保持し続ける」という事だと思う。一方、local変数は特定のスコープ内(ブロック式内など)のみ有効なので、その処理が終わると値が破棄されるようだ。スクリプトなので、そこまでストイックになる必要もないとも思うが、基本的な考え方として、保持する必要がない値は処理が終わったらどんどん破棄されるように、出来るだけlocal変数を使用した方が3dsmaxに優しい気がしないでもない。(その仕組みに悩んで、コードを書く時間がかかってしまうと、スクリプト本来の手軽であるメリットがなくなってしまう事もあるので、ほどほどでいいとも思うが・・・)

また、変数名が被らないように気を付ける際も、ローカル変数はその有効範囲内でのみ考えればよいので、ローカル変数は変数名が衝突する問題の解決方法の1つでもあると思う。(ただし、グローバル変数と衝突しないようにする配慮は常に必要)
--------------------
tips:maxscriptのglobal変数には、global変数をシーンファイルに埋め込む「存続グローバル変数」というものもある。(一見便利そうだがグローバル変数がシーンファイルに保存されるため、扱う際は注意が必要になるようだ。特にmaxscriptの変数は値だけではなく、関数を代入したり色んな物を代入出来てしまうようなので・・・。仕様が不明確な内はあまり使わない方がいいかもしれない。)

「リファレンス>変数>永続グローバル変数」

tips:maxscriptのメモリの仕様については
「メモリの割り当てとガベージコレクション」などで触れているようだ。

0 件のコメント:

コメントを投稿