Top Of Page スコープ パーシスタンス 変数をまとめる 目的別変数 グローバル Bottom

変数の使い方

前の章では、変数名についてまとめてきました。
もしかしたら名前付けさえうまくいけば安心、というような印象を持たれたかもしれません。
それはある意味真理でもありますが、名前付けはいいプログラミングのスタート地点に過ぎません。
的確な名前をつけた変数をどのように使用するか。
それもまた、非常に重要なことなのです。

スコープ

ある変数があったとします。そして、その変数が見える範囲のことを、その変数のスコープといいます。
foo () {
	int i;
	...
	if (i == 0) {
		int j;
		...
	}
}
この場合、iのスコープはfoo()の中全体、jのスコープはifの中となります。
端的に言えば、変数を使ってもコンパイルエラーがでない範囲が、その変数のスコープになります。

変数を使用するときはこのスコープを意識し、スコープを狭くするように心がけましょう。
プログラムを調べるときは、どの変数がどう使われているかという点をキーポイントに調査することがよくあります。
その時に、調査する範囲がすくなくて済むということは非常にありがたいことです。
また、本来使用するべき範囲にスコープを限定することにより、不用意な変数使用によるバグを減らすことができます。

int foo1 () {
	int i;
	...
	if (i == 0) {
		int j;
		.../* j を使う */
	}

	return j;
}
int foo2 () {
	int i;
	int j;
	...
	if (i == 0) {
		.../* j を使う */
	}

	return j;
}
foo1とfoo2を比べてみてください。本来は return i とするはずだったとしましょう。そうすると、これはどちらも間違ったコードになりますね。
return i とするところを、直前の if の中で j を使ったものだからそのまま勢いで j をリターンするようにしてしまったというところでしょうか。
しかし、この2つには大きな違いがあります。それは、foo1はコンパイルエラーが出ますが、foo2はコンパイルエラーが出ないことです。
foo1でコンパイルエラーが出れば、ほとんどのプログラマはそのエラーを調べさえすれば、たちどころに原因を見つけ修正することができるでしょう。しかし、foo2はコンパイルエラーが起きません。プログラムを動かして初めて不可解な動作に気がつくことになります。
もちろん、コンパイルエラーで直したほうが効率がいいですよね。
こんなふうに、スコープをうまく利用して、プログラム開発の効率を上げるように意識しましょう。

パーシスタンス

ある変数の(内部の)データが有効な期間をパーシスタンス(寿命または永続性)といいます。
この概念はスコープと似ていますが、違うものです。
パーシスタンスには、一般に次の範囲が知られています。
  1. あるブロック、または、あるルーチンの中(通常の変数はこれになります)
  2. 明示的に破棄するまでパーシスタンスを持つ(C言語の malloc/free など)
  3. プログラムの始まりから終わりまで(グローバル変数や Cの静的変数など)
  4. プログラムの始まる前から、終わっても永遠に(ファイルやDBに値を保存した場合)

パーシスタンスに関しての問題のほとんどは、「変数のパーシスタンスを実際よりも長く勘違いする」という原因によります。
パーシスタンスの外側で変数にアクセスすると、たとえそれがスコープ内であっても(その場合コンパイルエラーはでません)、それは不具合を引き起こします。 こういった不具合は原因を突き止めにくく、発生すると非常に嫌な思いをします。
このため、特に上記の2. 3. 4. の変数を扱うとき(この場合スコープとパーシスタンスは必ずしも一致しません)は、変数の値が生きているかどうかに注意して扱いましょう。
また、変数を使う直前で初期化をする習慣をつければ、(特に長い関数の場合)こういった不具合をある程度避けることができます。

使う変数をまとめる

一般に、他人が書いたプログラムのソースコードを調査するときは、データを中心に追いかけることが多いことは前の項でも触れました。
要するに「この変数はどこでどう使ってるんだろう…ああ、なるほど…ふむ…」というように、処理を追いかけていくわけです。
その時に、調査する範囲は狭くすめばそれに越したことはありません。
void SumData1( ... )
{
	...
	GetOldData(OldData, &NumOldData);
	GetNewData(NewData, &NumNewData);
	OldDataTtl = Sum(OldData, NumOldData);
	NewDataTtl = Sum(NewData, NumNewData);
	PrintOldDataSum(OldData, OldDataTtl, NumOldData);
	PrintNewDataSum(NewData, NewDataTtl, NumNewData);
	SaveOldDataSum(OldDataTtl, NumOldData);
	SaveNewDataSum(NewDataTtl, NumNewData);
	...
}

void SumData2( ... )
{
	...
	GetOldData(OldData, &NumOldData);
	OldDataTtl = Sum(OldData, NumOldData);
	PrintOldDataSum(OldData, OldDataTtl, NumOldData);
	SaveOldDataSum(OldDataTtl, NumOldData);
	...
	GetNewData(NewData, &NumNewData);
	NewDataTtl = Sum(NewData, NumNewData);
	PrintNewDataSum(NewData, NewDataTtl, NumNewData);
	SaveNewDataSum(NewDataTtl, NumNewData);
	...
}
SumData1とSumData2を比較してみてください。見た感じは、SumData1のほうが処理がそろっているような気するかもしれません。
でも、SumData1は、わずが6行で8個の変数を使用しています。
対して、SumData2は、各ブロックで意識している変数は3個です。
後から調べるときは、SumData2のほうが労力が少なく済むことが多いのです。

目的に応じた変数

一つの変数を複数の目的のために使いまわすこともできます。これは、特にメモリが高価だった頃は日常的に使われていたテクニックです。
しかし、読みやすいプログラムを心がけるなら、それは避けるべきです。
特にTempなどという変数名を使用するテンポラリ変数が、この使い回しの対象になりやすいのですが、漠然としたTempではなく、「なんのテンポラリとして使うのか」ということを考えて変数名をつけ、その為にのみ使用するようにすればそういった混乱は無くなります。
この、テンポラリ変数を共有することによる決定的な欠点は、「同じ変数を関係ないところで使うことによって、本来関係ない部分が、まるで関係があるように見えてしまう」ということです。

foo1() {
	....
	/* 解を求める */
	Temp = SolutionPreCompute(x);
	solution1 = (-x + Temp) / x;
	solution2 = (-x - Temp) / x;
	.....
	/* 解を入れ替える */
	Temp = solution1;
	solution1 = solution2;
	solution2 = Temp;
	....
}

foo2() {
	....
	/* 解を求める */
	ComputeTemp = SolutionPreCompute(x);
	solution1 = (-x + ComputeTemp) / x;
	solution2 = (-x - ComputeTemp) / x;
	.....
	/* 解を入れ替える */
	SwapTemp  = solution1;
	solution1 = solution2;
	solution2 = SwapTemp;
	....
}
foo1では、解を求める個所と解を入れ替える個所にまるでなんらかの関連性があるように見えます。
foo2では、それらが完全に独立していることが見て判ります。
これは少し前までは「メモリの無駄」とされていました。しかし現在のメモリのコストは大変安くなってきており、見やすい(直しやすい)プログラムのために多少のパフォーマンスの犠牲は容認できるようになりました。
もちろん、メモリ消費を非常にシビアに考慮にいれる必要があるプログラムでは、あえてfoo1の形式を選択するかもしれません。判断に悩んだら、先輩やリーダーに相談してみるのもいいでしょう。

もう一つ、複数の目的のために使いまわしてしまうケースがあります。
こちらはテンポラリではなく、通常の変数をそのように(往々にして無意識に)使用してしまいますので、注意が必要です。
それはハイブリッド連結とよばれるもので、たとえば…

PageCntはページ数を示します。
ただし、PageCntが-1のときは、印刷のエラーを表します。

CustCode は顧客コードです。
ただし、負の場合は、絶対値の顧客コードが料金滞納であることを示します。

こんなふうに変数を使ってしまうことがあります。
「ただし」がついたら要注意です。
一つの変数が複数の目的に使われると、その変数の本質が薄れ、最悪の場合その変数を使うすべての箇所でifなどによる判断が必要になり、非常にコードが見難くなるんです。 何気なく使用する前に、「ただし」がつかないかどうか確認してみてください。

グローバル変数

グローバル変数(モジュール変数ではない)は、プログラムのどこからでもアクセスできます。便利ですが、リスクも大きいことを忘れないで下さい。
また、グローバル変数を使うと、「(自分が)その時だけ書きやすく、(他人が)読みにくい」コードになる傾向があります。今回のテーマが、「よいプログラミング=読みやすい、追いやすい、変更しやすい、バグの出にくい、テストしやすいプログラミング」であることを考えると、グローバル変数は敵視せざるを得ません(^^;

グローバル変数を使用しないことが「きつい」なら、その人は多分、「情報の隠蔽」や、「モジュール化」の力を活用していないと思われます。

「モジュール化」「情報隠蔽」だけが真理というわけではないので、変な宗教に帰依する必要はありません。
しかし、この2つによってもたらされるメリットを知れば、「聖モジュール教」や「オブジェクト至高教」に帰依しなくとも、グローバルな変数の使用を控えるようになるでしょう。
グローバル変数による問題は、以下のことが上げられます:

グローバル変数を使用すると、ルーチンやモジュールの独立性が著しく損なわれます。そうすると、他のプロジェクトへの利用などは実質的に不可能になります。

大規模なプログラミングでは、「複雑さ」を管理しないと成功はまずありえません。
この知的な管理を実現するには、「プログラムを(論理的に)分割し、一度に考える範囲をせばめる」という方法しか私には思い付きません。

しかし絶対にグローバル変数がいけないというわけでもありません。絶対にいけないものは、現代のプログラミング言語からは、取り除かれているでしょう。それが実装されているということは、それを使う正当な理由があるということです。
グローバル変数の使用を正当化する理由は、以下のようなものです:

本来グローバルであるべきものは、グローバル変数に保存するのが一番スマートな回答になることが多くあります。
また、プログラム全体で使う定数も、グローバル変数で保存するというのはうなずける方法論です。
最後の「通過パラメータ」というのは、
foo(p1, p2)
{
	....
	.... /* p2をつかう。p1を使わない。 */
	hee(p1);
}
このような時のp1を通過パラメータといいます。
この意味のないパラメータをfooから減らすために、グローバル変数を使うことがあります。
そしてこういうときのp1は、「本来グローバルであるべきもの」であることが往々にしてあるのです。

グローバル変数を使う際に

こんな風に、必ずしも「絶対に使用不可」ではないグローバル変数ですが、やはり使わないでスマートにすむのでしたら使わないに越したことはありません。
グローバル変数を使用するのは、いずれも、グローバル変数によるソフトウェア工学的なデメリットである、ルーチン・モジュール独立性の致命的な低下と、メリットである局所的な複雑さの低減を考慮し、局所的な複雑さの低減がプログラム全体にとって有益であると判断したときなのです。
そこで、グローバル変数を使う際には、以下の点を考慮してください。
構造体をグローバルにすると「巨大な(たくさんの)データをグローバルにしてしまっている」という意識が薄れます。これは非常に危険な意識といえますので、できれば構造体をグローバルにしなくてもよいプログラミングを心がけましょう。

はじめに ルーチン モジュール 変数名の力 変数使用法 制御構造 レイアウト コメント テスト デバッグ 謝辞 Top of Site

Copyright (c) 2000 Takao Tamura