Minecraftとタートルと僕

PCゲームMinecraftのMOD「ComputerCraft」の情報を集めたニッチなブログです。

こちらのページは更新が滞っており、情報が古くなりつつあります。新しいCC情報サイトをはじめましたので、もしよければご参照ください。今後ともよろしくお願い申し上げます。

「百億のマインクラフトと千億のタートル」(https://hevo2.hatenablog.com/)

sleep実装からイベントを学ぶ(補足)―ローカル変数・グローバル変数

【補足】変数名の先頭についている local ってなに?

そういえば、これまで何度も使っているのに説明をしていませんでした。
良い機会ですのでLuaにおける変数の取り扱いについて説明しましょう。

まず、最初に重要な項目だけ先に提示します。
他のプログラム言語を使ったことがあるなら、Luaにおける以下の3つのルールを覚えるだけで問題ありません。また用語については、「ローカル」→「プライベート」、「グローバル」→「パブリック」と置き換えて読んでもらってもかまいません。

(ルール1)Luaは、local宣言しない変数はすべてグローバル変数として扱います。
(ルール2)local宣言した変数のみローカル変数となります。
(ルール3)for i=1,10 do ~ endのような、for構文中のカウンタ用変数はローカル変数です。
(ルール4)関数定義(function)も変数と同じように、頭にlocalでローカル関数になります。

以降の説明は本当に基本的な概念ですので、グローバル変数って? スコープって何? という人だけ読んでください。

ルール1)local宣言なしはすべてグローバル変数

これまで変数を定義(以降、わかりやすさのために「使用宣言」と書きます)するときに、次のように行ってきたと思います。

x = 10 -- 10という数字を入れたグローバル変数xをこれから使いますよ!

このグローバル変数xは、使用宣言した場所より後ろであれば、プログラムのどこであろうと参照できます。

変数を参照できる範囲のことをスコープと呼ぶのですが、グローバル変数のスコープは「変数宣言以降の場所であればすべて」です。無茶苦茶広いですね。
とても便利で強力なのですが、使い方を間違えると影響範囲が大きいので大変な目にあいます。

「さっき他の関数で計算した数値をこっちでも流用したいな・・・・・・。えーい、グローバル変数に入れてこちらでも参照しよう」
行き当たりばったりなプログラミングをしているとありがちですねw でもこれをやりすぎるとプログラムの構造が滅茶苦茶になります。

ルール2)local宣言するとローカル変数

ローカル変数とは

localと頭につけることで、その変数はローカル変数として使用できるようになります。

local y = 10 -- 10という数字を入れたローカル変数yをこれから使いますよ!

ローカル変数はスコープが限定された、謙虚な変数です。
ちょっとここだけで使いたい、他の場所で使う予定はないし、というときに使う変数だと思ってそう間違いではありません。スコープが限定されているので、ローカル変数を使うときには常にスコープを意識しながらプログラムを書くことになります。
場合によっては、スコープの制限によりプログラム全体を再構築しなくてはならないことも。しかしそれにより、見通しの良い、全体的にモジュール化された、バグの少ないプログラムが作られるのです。

ローカル変数のスコープは「変数使用宣言をしたブロックの中だけ」です。
「for ~ end」「while ~ end」「function ~ end」などのブロック中でローカル変数を使用宣言すると、その宣言の直後の行からそのブロックの末端にある「end」の直前までそのローカル変数を参照することができます。
たとえば以下のプログラム例だと、ローカル変数xのスコープは関数hevo()の中だけなので、ブロックの外であるmain部分でこのxを参照しようとしても失敗します。そのためこのプログラムはエラーでとまります。
xはprint関数から見て存在しない変数なので、xの値は存在しないよ、nilだよ、とみなされます。
つまり、print(x)はprint(nil)を実行することになります。そして結果もnilを返すことになり、プログラム作成者の意図と異なった結果になってしまいます。

function hevo() --ブロック開始
 local x = 10      -- ローカル変数使用宣言
 ・・・・          -- いろいろな処理
end             --ブロック終了

-- #####
-- main
hevo()
print(x) -- 変数xが参照できない。print(nil)を実行する

なお仮に、以下のようにxの頭のlocalをはずしてグローバル変数として使用宣言すると、このプログラムは「とりあえず」動きます。
しかしこのような、「グローバル変数をサブ関数の中で宣言。そして実際に使用するのはそのサブ関数の外」という使い方はバグの元であり、まったくお勧めしません。
なぜなら、プログラムが大規模になるほど、xがどこで宣言され、どこで使われているか把握するのが難しくなるからです。
場合によっては、意図しない場所で間違えて上書きしたり参照してしまう可能性すらあります。

function hevo()
 x = 10  -- グローバル変数
end

-- #####
-- main
hevo()
print(x) 
ファイルの先頭でローカル変数宣言することについて

最も外側の(一番上の)ブロックを考えると、それはプログラムファイルの頭から終わりまでというブロックになります。言い換えると、ファイル中で、なんらかのブロックに囲まれていない地(じ)の部分がトップレベルのブロックになります。

一番外側(トップレベル)のブロックでローカル変数を使用宣言すると、それ以降に記述するブロックはすべてトップレベルブロックの内側(下位)となるので、どこであろうとそのローカル変数を参照できることになります。

そのため、「グローバル変数のスコープ」と「ファイルの先頭で宣言したローカル変数のスコープ」は、ほぼ*1同じです。以下のプログラムはその実例で、正常に動きます。

-- config
local x = 10  -- ファイルの先頭でローカル変数宣言
y = 10 -- グローバル変数宣言

-- functions
function hevo2()
 print(x)
 print(y)
end

-- main
hevo2()
  • これは、プログラム中からグローバル変数を完全に根絶やしにできることを意味します。
  • 意味しますが、別にグローバル変数の完全排除を推奨しているわけではないのでご注意ください。
  • あなたは、グローバル変数を使ってもいいし、使わなくてもいい。

ルール3)for構文中のカウンタ用変数はローカル変数

以下のfor文は、printを計3回繰り返すわけですが、1回ごとの実行において利用される「変数i」は、1回ごとに生成&廃棄される、個別のローカル変数であることにご注意ください。

for i=1,3 do
 print(i)
end

つまりこのfor文は、実質的に以下のプログラムとほぼ等価です。

do        --ブロック開始
  local i=1 -- ローカル変数の使用宣言
  print(i) -- ローカル変数の利用
end       --ブロックが終わるのでローカル変数を廃棄

do
  local i=2
  print(i)
end

do
  local i=3
  print(i)
end

ルール4)関数にもローカル・グローバルがある

以下のように、ブロックの中でローカル(プライベート)関数を定義することができます。

function hevo()
  local function myPrint(x)
    -- 何らかの処理
  end

  myPrint("abc")
  myPrint("12345")
end

隠蔽化は、オブジェクト指向プログラミングにおいては重要なテーマですね。
ローカル・グローバルを使い分けて、見通しの良いプログラムを普段から心がけると後々幸せになれます。
とはいえ小規模なプログラムにおいて、関数のローカル・グローバルは、変数のときほど神経質になる必要はありません。
問題になるのは大規模なプログラムを書くとき、あるいは他のプログラムから呼び出されるAPIを書くときです。このようなときには、ローカルとグローバルの区別を厳密に意識しましょう。
APIの書き方については、そのうちしっかり時間をかけて説明したいと思います。

結局、ローカル変数とグローバル変数をどう使い分ければいいの?

使い方の原則はあります。しかし決まりはありません。そのプログラマーのポリシーしだいです。

まず原則論

  • グローバル変数は強力で影響範囲が大きいので、使用は最小限にすること。
    • 最小限とは、「そのグローバル変数がどこで定義し何のために使うものなのかを完全に把握できる数だけ」使うことです。グローバル変数の個数が多すぎてこれは何のための変数だっけ?などと思うようなプログラムは、そもそも設計が間違えています。
    • プログラマーによっては、「グローバル変数はバグの元」と言って、ひとつも使わない場合もあります*2
    • たくさんのプログラマが参加する大規模プロジェクトでは、グローバル変数は完全に管理されており、上司の許可なしに勝手にグローバル変数を増やすと怒られます。
  • そのため、「localつけないと基本はグローバル変数」という仕様になっているLuaは大規模プログラムを書くのに向かないことがわかりますね。
  • まあLuaは、もともと組み込み用途で設計された言語だから当然といえば当然なのですが。

僕の変数使用ポリシー

  • グローバル変数は、プログラムのconfigパラメータや定数でしか使いません。
  • 定数は大文字で強調した変数名にしてあります。
  • 例外として、args={...}やmon=peripheral.wrap("top")のようなよくある定型文でグローバル変数を使うことがあります*3
  • 他はほとんどすべてローカル変数です。

※ひとつの独立したプログラムではなく、例としてソースコードの断片を掲載するときには、localを省略することが多々あります。

※そして何事も例外があるということで、手を抜いて短時間で作ったプログラムにはしれっとlocalつけ忘れがあるかもしれません。その場合はご容赦ください。

今回のまとめ

グローバル変数を使うときには注意してください! 頭にlocalをつけずに、てきとーに使いまくっていると後で泣きを見ることになります。

また使うにしても、定数とか設定用パラメータくらいに使用を留めておくのが無難です。

次回について

本編より補足のほうが長いよ!! というご指摘は真摯に受け止めさせていただきます。
これもひとえに、私の書きたいことを思いつくままに書くというこのサイトのポリシーによるもので、今後も改善する予定はございませんのでご了承ください。

次回以降も、テーマに沿いつつもそれに関連したことを思いつくまま、脱線しながら書き散らかしていくつもりです。

このサイトを開始してから数ヶ月、読みづらい私の文章にお付き合いいただきありがとうございました。今後もこの調子で続けていくつもりですので、よろしくお願いします。

それでは来年も良いお年を。

*1:ほぼの理由は、Luaが組み込み用途で作られた言語であることに由来します。Luaプログラムを呼び出す外側のプログラム(たとえばJavaなど)から、そのLuaプログラムのグローバル変数に直接アクセスできます。また、APIとして他のプログラムから呼び出す場合にも挙動に違いがありますが、ここでは説明を省略します。

*2:たとえばJava言語は、厳密な意味で、グローバル変数はありません。

*3:本当はこれもlocal宣言しておくべきなんですけどね。今後は極力気をつけます。