前回で関数の実装はまともになったので、クラスを拡張する方向に検討をしてみる。
とりあえずmapBetween関数をtraitにして分離。(20〜24行目)
前回までは、型パラメタAを下限とする型パラメタTを関数の引数の型としていたが、traitにしたことでまたうまく関数の型推論ができなくなってかえって使いにくいため、Aそのものを引数の型に変更した。匿名関数が手軽に定義できる以上、JavaのComparatorのような問題は起きないと思って良いのかもしれない。
使う際にはSeqのサブクラスをnewする時にwith ExtSeq[A]でtraitをミックスインする(3行目)ことで、mapBetween関数を呼び出すことができるようになる。(4〜6行目)
ミックスインできるのはインスタンス化するときなので、生成されたインスタンスを返すRichIntクラスのtoメソッドは使えない。Rangeクラスのコンストラクタはfrom, to, stepの3つの引数を必ず要求するため、ちょっと使いにくい。
そこで、Seqのサブクラスのインスタンスを trait ExtSeqをミックスインしたクラスのインスタンスに変換する、暗黙の型変換を行う関数を定義する。(14〜17行目)
この暗黙の型変換は、mapBetweenオブジェクトのスコープ内で有効なため、Rangeクラスのインスタンスを普通に生成(8行目)しても、そのインスタンスに対してmapBetween関数を呼び出そうとすることで、コンパイル時に暗黙的にseqToExtSeq関数の呼び出しが加えられる。それによって、あたかもRangeクラスに元々mapBetween関数が定義されていたかのように見える。(9〜12行目)
このseqToExtSeq関数を定義する際に注意することは、返り値をExtSeqにしなければならないのを誤ってSeqのインスタンスを返すように実装(例えば++=の代わりに++を呼んでしまった等)しても、コンパイルが通ってしまうことだ。そうした場合、実行時にseqToExtSeq関数が呼び出されると、Seqのインスタンスを本来の返り値の型であるExtSeqに暗黙的に変換しようとして、自分を再帰的に呼び出してしまい、無限ループに陥る。しかもタチの悪いことに、末尾最適化されてしまってスタックがオーバフローしないため、ただダンマリになってしまってしばらく何が起きたのかわからなかった。実装時はimplicit修飾子を付けないで実装して、後から修飾子を付けたほうが間違いが起こりにくいかも。
ところで、immutableなパッケージでミックスインできるクラスが見つけられなかったためやむを得ずArrayBufferを使ってしまったが、このままでは暗黙の型変換をした後のインスタンスがmutableになってしまう。ココは改善する必要があるのだが、とりあえずノーアイディア。
2012年2月27日月曜日
2012年2月24日金曜日
mapBetween関数Scala版 その2
前回のコードをrpscalaの方々に見て頂いたところ、「それ、slidingでできるよ!」と教えていただいたので修正。
sliding関数は引数で指定された個数を1つのブロックとして、sliding windowを提供する。例えばSeq(1,2,3,4)に対してsliding(2)を呼び出すと、Seq(1,2),Seq(2,3),Seq(3,4)と返すIteratorが返される。sliding(3)ならSeq(1,2,3),Seq(2,3,4)となる。
これはmapBetween関数でやりたいことほぼそのものなので、結果としてえらいシンプルになってしまった。
前回適用する関数に対して型推論が効かないと愚痴ったが、これはカリー化することで対応できることを教えてもらった。カリー化前だと型パラメタAとTを同時に推論しなければならず、それが上手くいかない原因だった模様。カリー化することで先に型Aが決定するため、型Tが上手く推論できるようになるらしい。嘘かも。型推論はもうちょっとまともに勉強したいなぁ。。。
sliding関数は引数で指定された個数を1つのブロックとして、sliding windowを提供する。例えばSeq(1,2,3,4)に対してsliding(2)を呼び出すと、Seq(1,2),Seq(2,3),Seq(3,4)と返すIteratorが返される。sliding(3)ならSeq(1,2,3),Seq(2,3,4)となる。
これはmapBetween関数でやりたいことほぼそのものなので、結果としてえらいシンプルになってしまった。
前回適用する関数に対して型推論が効かないと愚痴ったが、これはカリー化することで対応できることを教えてもらった。カリー化前だと型パラメタAとTを同時に推論しなければならず、それが上手くいかない原因だった模様。カリー化することで先に型Aが決定するため、型Tが上手く推論できるようになるらしい。嘘かも。型推論はもうちょっとまともに勉強したいなぁ。。。
2012年1月27日金曜日
mapBetween関数Scala版
dankogai先生の投稿 algorithm - mapBetween - 配列の隣接する2項にそれぞれ演算を施した配列 にあるmapBetween関数をScalaで書いてみる。
クラスを拡張する形で書きたかったのだが、いっぺんにやろうとするときっとこんがらがるのでとりあえずSeqを引数で受ける形で実装。
とりあえず動くけど、果たして効率の良い実装になっているのか。。。
隣り合う2項に適用する関数のパラメタに、型を指定しなければコンパイルが通らない。第1引数Seqの型パラメタから推論して欲しいところだけどどうもそうはならないみたい。
foldLeftの左項のタプルの1つめの要素で、Seq[B]()の代わりにNilを指定したかったのだが、型の不一致でこれもまたコンパイルが通らない。まぁこちらはなんとなく仕方がない気がする。
クラスを拡張する形で書きたかったのだが、いっぺんにやろうとするときっとこんがらがるのでとりあえずSeqを引数で受ける形で実装。
とりあえず動くけど、果たして効率の良い実装になっているのか。。。
隣り合う2項に適用する関数のパラメタに、型を指定しなければコンパイルが通らない。第1引数Seqの型パラメタから推論して欲しいところだけどどうもそうはならないみたい。
foldLeftの左項のタプルの1つめの要素で、Seq[B]()の代わりにNilを指定したかったのだが、型の不一致でこれもまたコンパイルが通らない。まぁこちらはなんとなく仕方がない気がする。
2011年1月25日火曜日
topコマンドのログ解析(Python手習い) その2
その後OrderedDictというクラスがcollectionsモジュールにあることが判明したため、これを使ってもうちょっとマシなファイルハンドル操作を実装してみた。
ついでにファイルオープン時に致命的なバグがあったため、合わせて修正。
キモは51~63行あたり。
OrderedDictは名が示すとおり、(key, value)セットの追加順序を記憶する辞書型である。ただし、一度追加した(key, value)は、valueの更新が行われても順序を入れ替えない。今回はLRUを実装しなければならないため、既に追加されている(key, value)にアクセスした場合は必ず一度削除してから再度追加するようにしている。
さらに、もし管理しているファイルハンドルがMAX_FILE_HANDLEを超えそうになったら、一番使われていないファイルハンドルを取得しクローズ処理を行なっている(54行目)。popitemメソッドは、引数がtrueの場合はLIFO、falseの場合はFIFOとして動作する。
前回バグっていたのを直したのは56~60行目あたり。前回はopen関数の第2引数を常に'w'と指定していたため、ファイルハンドルが増えたために一旦クローズしてしまったファイルについて、再度書きこみを行おうとした場合にクローズ前までのデータを消してしまっていた。オープン時にファイルが存在する場合は追記('a')するように修正。
150MB程度のログファイルの処理に、僕の非常に非力なマシン(PentiumM 1.2GHz 752MB RAM)で約2分。生成されるファイル数は4,000あまりというところ。
時間があったら、次はもう少しモジュール化してみたい。
ついでにファイルオープン時に致命的なバグがあったため、合わせて修正。
from collections import OrderedDict
from datetime import datetime, timedelta
from functools import reduce
from os.path import exists
from re import match, search, split
from sys import argv
from shutil import rmtree
from os import makedirs
MAX_FILE_HANDLE = 500
WORK_DIR = 'work'
if len(argv) != 3:
print("\nUsage:\npython", argv[0], "logfile yyyy/mm/dd")
exit()
rmtree(WORK_DIR, True)
if not exists(WORK_DIR):
makedirs(WORK_DIR)
mydate = datetime.strptime(argv[2], '%Y/%m/%d')
mydateStr = mydate.strftime('%Y/%m/%d ')
try:
f = open(argv[1])
mem = open(WORK_DIR + "/mem.log", 'w')
mem.write("time,mem av,mem used,mem free,mem shard,mem buff,mem actv, mem in_d,swap av,swap used,swap free,swap cached\n")
processes = OrderedDict()
maxtime = ""
for line in f:
line = line.strip()
if match(r"^\d\d:\d\d:\d\d", line):
if line[0:2] == "00" and timestamp[0:2] != "00":
mydate = mydate + timedelta(1)
mydateStr = mydate.strftime('%Y/%m/%d ')
timestamp = line[0:8]
elif match(r"^Mem", line):
data = split(r"\s+", line)[1:9:2]
elif match(r"^\d+k", line):
data = data + split(r"\s+", line)[0:5:2]
elif match(r"^Swap:", line):
data = data + split(r"\s+", line)[1:8:2]
prefix = mydateStr + timestamp
result = reduce((lambda x,y: x + ',' + y.rstrip('k')), data, prefix)
mem.write(result + "\n")
elif search(r"java", line):
data = split(r"\s+", line)
pid = data[0]
process = processes.get(pid)
if process == None:
while len(processes) >= MAX_FILE_HANDLE:
processes.popitem(last=False)[1].close()
filename = WORK_DIR + "/Pid-" + pid + ".log"
if exists(filename):
process = open(filename, 'a')
else:
process = open(filename, 'w')
process.write("time,PID,USER,PRI,NI,SIZE,RSS,SHARE,STAT,%CPU,%MEM,TIME,CPU,COMMAND\n")
else:
del processes[pid]
processes[pid] = process
prefix = mydateStr + timestamp
result = reduce((lambda x,y: x + ',' + y), data, prefix)
process.write(result + "\n")
if data[10] > maxtime:
maxtime = data[10]
maxpid = pid
print("maxpid:", maxpid)
finally:
if f != None:
f.close()
if mem != None:
mem.close()
map(lambda x: x.close(), processes)
キモは51~63行あたり。
OrderedDictは名が示すとおり、(key, value)セットの追加順序を記憶する辞書型である。ただし、一度追加した(key, value)は、valueの更新が行われても順序を入れ替えない。今回はLRUを実装しなければならないため、既に追加されている(key, value)にアクセスした場合は必ず一度削除してから再度追加するようにしている。
さらに、もし管理しているファイルハンドルがMAX_FILE_HANDLEを超えそうになったら、一番使われていないファイルハンドルを取得しクローズ処理を行なっている(54行目)。popitemメソッドは、引数がtrueの場合はLIFO、falseの場合はFIFOとして動作する。
前回バグっていたのを直したのは56~60行目あたり。前回はopen関数の第2引数を常に'w'と指定していたため、ファイルハンドルが増えたために一旦クローズしてしまったファイルについて、再度書きこみを行おうとした場合にクローズ前までのデータを消してしまっていた。オープン時にファイルが存在する場合は追記('a')するように修正。
150MB程度のログファイルの処理に、僕の非常に非力なマシン(PentiumM 1.2GHz 752MB RAM)で約2分。生成されるファイル数は4,000あまりというところ。
時間があったら、次はもう少しモジュール化してみたい。
2011年1月24日月曜日
topコマンドのログ解析(Python手習い)
topコマンドのログをExcelでグラフ化できるようにCSV形式に変換するPythonスクリプトを書いてみた。とりあえず動いたが、色々知らないまま書いているのでたぶんもっと良い書き方があるはず。
JavaのPID毎に別ファイルに出力するようにしたかったのだが、Windows上では500個ちょっとオープンしたところで"Too Many Open Files"エラーが出たため、500を超えたら一旦すべてクローズするように暫定対処した。できれば参照の少ないファイルから先にクローズしていくようにしたい。Javaで言うところのLinkedHashMapみたいなものは無いのだろうか。
スクリプト言語としてはずいぶん昔にPerlを触った以来だが、やっつけで書きやすい割にPerlよりは可読性が高いように感じる。
JavaのPID毎に別ファイルに出力するようにしたかったのだが、Windows上では500個ちょっとオープンしたところで"Too Many Open Files"エラーが出たため、500を超えたら一旦すべてクローズするように暫定対処した。できれば参照の少ないファイルから先にクローズしていくようにしたい。Javaで言うところのLinkedHashMapみたいなものは無いのだろうか。
スクリプト言語としてはずいぶん昔にPerlを触った以来だが、やっつけで書きやすい割にPerlよりは可読性が高いように感じる。
import sys
import re
from datetime import datetime, timedelta
from functools import reduce
if len(sys.argv) != 3 :
print("\nUsage:\npython", sys.argv[0], "logfile yyyy/mm/dd")
exit()
mydate = datetime.strptime(sys.argv[2], '%Y/%m/%d')
mydateStr = mydate.strftime('%Y/%m/%d ')
try :
f = open(sys.argv[1])
mem = open("mem.log", 'w')
mem.write("time,mem av,mem used,mem free,mem shard,mem buff,mem actv, mem in_d,swap av,swap used,swap free,swap cached\n")
processes = {}
maxtime = ""
for line in f :
line = line.strip()
if re.match(r"^\d\d:\d\d:\d\d", line) :
if line[0:2] == "00" and timestamp[0:2] != "00" :
mydate = mydate + timedelta(1)
mydateStr = mydate.strftime('%Y/%m/%d ')
timestamp = line[0:8]
elif re.match(r"^Mem", line) :
data = re.split(r"\s+", line)[1:9:2]
elif re.match(r"^\d+k", line) :
data = data + re.split(r"\s+", line)[0:5:2]
elif re.match(r"^Swap:", line) :
data = data + re.split(r"\s+", line)[1:8:2]
prefix = mydateStr + timestamp
result = reduce((lambda x,y: x + ',' + y.rstrip('k')), data, prefix)
mem.write(result + "\n")
elif re.search(r"java", line) :
data = re.split(r"\s+", line)
if data[0] not in processes :
processes[data[0]] = open("Pid-" + data[0] + ".log", 'w')
processes[data[0]].write("time,PID,USER,PRI,NI,SIZE,RSS,SHARE,STAT,%CPU,%MEM,TIME,CPU,COMMAND\n")
prefix = mydateStr + timestamp
result = reduce((lambda x,y: x + ',' + y), data, prefix)
processes[data[0]].write(result + "\n")
if data[10] > maxtime :
maxtime = data[10]
maxpid = data[0]
if len(processes) > 500 :
map(lambda x: x.close(), processes)
processes = {}
print("maxpid:", maxpid)
finally :
if f != None :
f.close()
if mem != None :
mem.close()
map(lambda x: x.close(), processes)
2010年8月17日火曜日
コーディングイディオムの奇妙な慣習
先日仕事で緊急に、他人の作ったVisual Basicのコードを改修するタスクを受け持った。
恥ずかしながら、VBは人生初だったが、Visual Studio様とGoogle様のお力でなんとか凌いだ。確かに言語としての敷居は低い。出だしの瞬間的な生産性はかなり高めだ。静止摩擦係数が低いという感じか。静的に型宣言を要求されるため、Java使いには意外と馴染む。ただ、互換性維持のためなのだろうが、同じことを実現するための関数の系統がいくつもあって、どれを選択すべきなのかわからないことが多かった。MSDNを真面目に読めば書いてあるのかもしれないが。
元のコードは典型的な要リファクタリングコードだった。マーチンファウラー先生曰く「臭う」箇所があまりに多くて鼻が麻痺しそうだった。処理はいくつかの関数に分けられていたが基本的に一本道であり、同じ処理がコードのあちこちに何度も出てくる。やたらとグローバル変数が宣言されている。ローカル変数のスコープが無意味に広い。if文のネストが尋常じゃなく深い。そして何よりも、テストコードが一切存在しなかった、いわゆる「レガシーコード」だ。
いろいろ手をつけたかったが、最終的に断念した。期間があまりに短かった上、仕様が毎日どころか数時間毎に変更されていくためだ。修正箇所を極力限定的にし、テストの項目数を極小にしなければ、どう考えても納期に間に合わなかった。直したくて直したくてウズウズする気持ちを抑えて、最小限の修正で実装を終えた。
結果、バグを作りこんでしまった。テストが甘かったといえばそれまでなのだが、初めから自分が書いていれば発生するはずのない類のバグだった。今回はあまりに酷い例だったが、経験上意外とこういうコードを書く人が多く見られるため、自戒も込めてメモを残しておく。
1. ローカル変数の宣言箇所
これは本当に奇妙なのだが、変数の宣言をサブルーチンの先頭にまとめて行うコードを大変よく見かける。正直メリットを一切思いつかない。ひょっとすると、Cのようなガベージコレクタを持たない言語では、領域を確保した後に開放するのを忘れないようにするために、チェックする箇所を限定するという目的があるのかもしれない。しかしほとんどのローカル変数は対象外だし、Javaを含めてGCを持つ言語では百害あって一利もない。典型的な害は以下のような例だ。
for文の中でしか利用しないにも関わらず、先頭で予め宣言したために変数varがfor文の外のスコープを汚染している。もっと問題なのは、変数varの値がループの1つ前の処理に依存していることだ。上記の例の場合、forループの初回はif文の条件に合致した場合、varの値は"foo"になるが、合致しなかった場合は宣言時に代入したnullになる。2回目もif文の条件に合致した場合は同じく"foo"になるが、合致しなかった場合は初回の結果に依存する。
要するにfor文中の条件分岐のいずれかのルートで、varの代入を忘れると嘘の値になるという厄介なバグを作ることになる。今回僕が作りこんだバグはコレだ。正しく動作させるためには以下のようにする必要がある。
このような単純な例で見落とす可能性は低いが、このfor文の中の処理が長くなり、分岐が複雑多岐にわたるようになれば徐々に見落としが発生する可能性が増す。また、代入を忘れた場合もなんらかの値が入っているため、たまたま前ループで正しい値が代入されることでテストを通過してしまう可能性が高い。
このような見落としがそもそも発生しないようにすることは簡単で、以下のようにすれば良い。
変数varをfor文の中で宣言するだけだ。変数がforループ中、毎回宣言し直されるため、前回の値が誤って代入されることは絶対にない。昔はループのたびに変数宣言が行われることによるパフォーマンスの劣化を懸念するような意見があったが、ハードウェアのスペックが十分高くなった現在では、パフォーマンス劣化の危険性がバグを作り込む危険性に吊り合わない。そもそも、現在のJava VMでは上記のように都度宣言する記述を行っても、コンパイラの最適化によってforループの外で宣言する形に修正されるため、パフォーマンスの劣化自体が発生しない。そしてコンパイラが自動的に実行する最適化は、人が都度あれこれ気を使って行う最適化よりも概ね正確で効果的だ。変数は常に最小のスコープで、使うときに宣言すべきだ。
2. 無意味な変数初期化
1.で挙げた修正後のコードを、以下のように書く人もよく見かける。
こちらについては、必ずしもマズいとは言い切れ無い。特に上記のような単純な場合はコード量を減らす効果がある。しかし、僕はこのような書き方はあまり好きじゃない(そもそも上記のような単純な例なら、三項演算子を使えという話になってしまうがそれは置いておいて)。なぜなら上記の場合、if文の条件に合致するときに変数varの値は一旦"bar"によって初期化され、その後"foo"という値になる。この動作は非常に奇妙に感じる。もっと極端な例では以下のようなコードも見たことがある。
変数varを宣言した際に、「とりあえず」空文字で初期化しておくのだ。この空文字が実際に使われることは一度もない。空文字の代わりにnullでも同じことだ。宣言時に即初期化しろという強迫観念のようなものが存在するのだろうか。
このような記述は、コンパイラが持っている折角のチェック機構をひとつ潰すことになる。初期化を行わなかった場合、例えば以下のように記述すると、コンパイル時にエラーが発生する。
なぜなら、if文の条件に合致しなかった場合に変数varの初期化が行われないため、後でvarを参照しようとしたときに困ってしまうからだ。else文でvarに明示的に値を代入しない限り、コンパイルは通らない。
このような単純な例では恩恵が得られないように感じるかもしれないが、分岐が複雑になっていけばこれに救われる可能性が高くなってくる。ある分岐で、うっかりvarへの代入を忘れてしまったとき、とりあえずvarの初期化をしてしまっていると、そのとりあえずの値が採用されてしまい、コンパイル自体は通ってしまう。その分岐ルートをテストしなければ、間違いに気づかない。最悪、とりあえずの値が期待する値とたまたま一致していると、バグに気づかないことになる。宣言時に初期化を行わなければ、すべてのルートで代入が行われていない限りコンパイル自体が通らない。
結局、1と2両方を心がけることで、作りこんでしまったバグをコンパイラが見つけてくれる。これを利用しない手はない。もしコーディング規約等でこれらの奇妙な慣習が義務付けられているのであれば、規約自体の見直しを行う時が来たということになるだろう。
恥ずかしながら、VBは人生初だったが、Visual Studio様とGoogle様のお力でなんとか凌いだ。確かに言語としての敷居は低い。出だしの瞬間的な生産性はかなり高めだ。静止摩擦係数が低いという感じか。静的に型宣言を要求されるため、Java使いには意外と馴染む。ただ、互換性維持のためなのだろうが、同じことを実現するための関数の系統がいくつもあって、どれを選択すべきなのかわからないことが多かった。MSDNを真面目に読めば書いてあるのかもしれないが。
元のコードは典型的な要リファクタリングコードだった。マーチンファウラー先生曰く「臭う」箇所があまりに多くて鼻が麻痺しそうだった。処理はいくつかの関数に分けられていたが基本的に一本道であり、同じ処理がコードのあちこちに何度も出てくる。やたらとグローバル変数が宣言されている。ローカル変数のスコープが無意味に広い。if文のネストが尋常じゃなく深い。そして何よりも、テストコードが一切存在しなかった、いわゆる「レガシーコード」だ。
いろいろ手をつけたかったが、最終的に断念した。期間があまりに短かった上、仕様が毎日どころか数時間毎に変更されていくためだ。修正箇所を極力限定的にし、テストの項目数を極小にしなければ、どう考えても納期に間に合わなかった。直したくて直したくてウズウズする気持ちを抑えて、最小限の修正で実装を終えた。
結果、バグを作りこんでしまった。テストが甘かったといえばそれまでなのだが、初めから自分が書いていれば発生するはずのない類のバグだった。今回はあまりに酷い例だったが、経験上意外とこういうコードを書く人が多く見られるため、自戒も込めてメモを残しておく。
1. ローカル変数の宣言箇所
これは本当に奇妙なのだが、変数の宣言をサブルーチンの先頭にまとめて行うコードを大変よく見かける。正直メリットを一切思いつかない。ひょっとすると、Cのようなガベージコレクタを持たない言語では、領域を確保した後に開放するのを忘れないようにするために、チェックする箇所を限定するという目的があるのかもしれない。しかしほとんどのローカル変数は対象外だし、Javaを含めてGCを持つ言語では百害あって一利もない。典型的な害は以下のような例だ。
String var = null;
for (...) {
if (...) {
var = "foo";
}
System.out.println(var);
}for文の中でしか利用しないにも関わらず、先頭で予め宣言したために変数varがfor文の外のスコープを汚染している。もっと問題なのは、変数varの値がループの1つ前の処理に依存していることだ。上記の例の場合、forループの初回はif文の条件に合致した場合、varの値は"foo"になるが、合致しなかった場合は宣言時に代入したnullになる。2回目もif文の条件に合致した場合は同じく"foo"になるが、合致しなかった場合は初回の結果に依存する。
要するにfor文中の条件分岐のいずれかのルートで、varの代入を忘れると嘘の値になるという厄介なバグを作ることになる。今回僕が作りこんだバグはコレだ。正しく動作させるためには以下のようにする必要がある。
String var;
for (...) {
if (...) {
var = "foo";
} else {
var = "bar";
}
System.out.println(var);
}このような単純な例で見落とす可能性は低いが、このfor文の中の処理が長くなり、分岐が複雑多岐にわたるようになれば徐々に見落としが発生する可能性が増す。また、代入を忘れた場合もなんらかの値が入っているため、たまたま前ループで正しい値が代入されることでテストを通過してしまう可能性が高い。
このような見落としがそもそも発生しないようにすることは簡単で、以下のようにすれば良い。
for (...) {
String var;
if (...) {
var = "foo";
} else {
var = "bar";
}
System.out.println(var);
}変数varをfor文の中で宣言するだけだ。変数がforループ中、毎回宣言し直されるため、前回の値が誤って代入されることは絶対にない。昔はループのたびに変数宣言が行われることによるパフォーマンスの劣化を懸念するような意見があったが、ハードウェアのスペックが十分高くなった現在では、パフォーマンス劣化の危険性がバグを作り込む危険性に吊り合わない。そもそも、現在のJava VMでは上記のように都度宣言する記述を行っても、コンパイラの最適化によってforループの外で宣言する形に修正されるため、パフォーマンスの劣化自体が発生しない。そしてコンパイラが自動的に実行する最適化は、人が都度あれこれ気を使って行う最適化よりも概ね正確で効果的だ。変数は常に最小のスコープで、使うときに宣言すべきだ。
2. 無意味な変数初期化
1.で挙げた修正後のコードを、以下のように書く人もよく見かける。
for (...) {
String var = "bar";
if (...) {
var = "foo";
}
System.out.println(var);
}こちらについては、必ずしもマズいとは言い切れ無い。特に上記のような単純な場合はコード量を減らす効果がある。しかし、僕はこのような書き方はあまり好きじゃない(そもそも上記のような単純な例なら、三項演算子を使えという話になってしまうがそれは置いておいて)。なぜなら上記の場合、if文の条件に合致するときに変数varの値は一旦"bar"によって初期化され、その後"foo"という値になる。この動作は非常に奇妙に感じる。もっと極端な例では以下のようなコードも見たことがある。
for (...) {
String var = "";
if (...) {
var = "foo";
} else {
var = "bar";
}
System.out.println(var);
}変数varを宣言した際に、「とりあえず」空文字で初期化しておくのだ。この空文字が実際に使われることは一度もない。空文字の代わりにnullでも同じことだ。宣言時に即初期化しろという強迫観念のようなものが存在するのだろうか。
このような記述は、コンパイラが持っている折角のチェック機構をひとつ潰すことになる。初期化を行わなかった場合、例えば以下のように記述すると、コンパイル時にエラーが発生する。
for (...) {
String var;
if (...) {
var = "foo";
}
System.out.println(var);
}なぜなら、if文の条件に合致しなかった場合に変数varの初期化が行われないため、後でvarを参照しようとしたときに困ってしまうからだ。else文でvarに明示的に値を代入しない限り、コンパイルは通らない。
このような単純な例では恩恵が得られないように感じるかもしれないが、分岐が複雑になっていけばこれに救われる可能性が高くなってくる。ある分岐で、うっかりvarへの代入を忘れてしまったとき、とりあえずvarの初期化をしてしまっていると、そのとりあえずの値が採用されてしまい、コンパイル自体は通ってしまう。その分岐ルートをテストしなければ、間違いに気づかない。最悪、とりあえずの値が期待する値とたまたま一致していると、バグに気づかないことになる。宣言時に初期化を行わなければ、すべてのルートで代入が行われていない限りコンパイル自体が通らない。
結局、1と2両方を心がけることで、作りこんでしまったバグをコンパイラが見つけてくれる。これを利用しない手はない。もしコーディング規約等でこれらの奇妙な慣習が義務付けられているのであれば、規約自体の見直しを行う時が来たということになるだろう。
登録:
投稿 (Atom)