関数とスコープ


小さなルーチン

私たちはこれまで、プログラムを実行するソースコードを書くとき、必ず main() 関数を定義してきました。 D 言語も C 言語同様に、最も小さいプログラムの実行ルーチンは関数なのです。 ただし、C 言語では関数の宣言と定義が分かれていたり、ヘッダファイルとソースファイルに分割し、ファイルスコープなどを気にかける必要がありました。 幸い、D 言語にはこのような煩わしさはなくなり、関数がどこで定義されているかは問題ではありません。

関数は、何らかの再利用可能な処理をまとめたコードの塊です。 数学的な計算を行うなど、面倒な処理は何度も描かずに関数にまとめることによって、コード量を少なくし、生産性を高めます。 特に、完成度の高い関数を使いまわすことは、バグを回避することができます。 関数を定義するには次のように記述します。

戻り値型 関数名( 仮引数リスト ) { 本体 }

これまで、printf() 関数などを使ってきたように、一度作成した関数は式文から呼び出すことで本体の処理を実行することができます。 関数名は他の関数名と衝突してはならず、一意に認識可能でなければなりません。 戻り値とは、この関数が返す型、仮引数リストは、この関数が受け取る引数の方と変数名を指定します。 値を受け取らず、値を返さない関数で、関数名を Popotan とする場合は、次のように定義することになるでしょう。

void Popotan() { 本体 }

他の関数の文から Popotan() を呼び出すことで、プログラムの制御はこの関数に移行します。 関数の処理が終了すれば、関数を呼び出した元の位置に復帰するため、最終的には main() 関数に戻ってきます。

import std.stream;

int main() {
	stdout.writeLine("main 関数開始");
	Popotan();
	stdout.writeLine("main 関数終了");

	return 0;
}

void Popotan() {
	stdout.writeLine("不思議な呪文〜♪ぽぽたん〜");
}

このプログラムを実行すると、出力される文字列の順番から、プログラムが "main 関数開始"→"ぽぽたん"→"main 関数終了" という順番で実行されたことを確認することができます。 main() 関数内で Popotan() 関数を呼び出したため、制御が main() 関数から Popotan() 関数に移動したのです。 C 言語とは異なり、関数の宣言位置は問題ではないことに注目してください。


変数のスコープ

D 言語の変数の範囲の考え方は、大部分で C 言語と同じです。 main() 関数以外の関数を使うようになり、広域的なプログラミングになってくると、気になるのは変数の管理です。 変数には可視範囲が存在し、ある場所からは参照することができるが、別の場所からは変数の識別子が隠されたりするのです。 そうしなければ、変数にアクセスする必要のない場所から、プログラマが誤って突然関係のない変数にアクセスしてくるかもしれません。 このような、変数の氾濫による管理ミスを防ぐための変数の可視範囲をスコープと呼びます。

特に、関数の中に存在する変数をローカル変数と呼びます。 ローカル変数のスコープは宣言された関数の内部でのみ有効で、外部からのアクセスは受け付けません。 例えば、Snow() 関数内の変数 meiko に Akumu() 関数から参照するということはできないのです。 また、関数から制御が戻ると、関数内のローカル変数が保存しているデータは破棄されます。

変数は、関数と同じグローバルな領域で宣言することも可能です。 これをグローバル変数と呼び、C 言語のグローバル変数と概念的に同じです。 グローバル変数のスコープはファイル全体で、ファイル内のすべての関数からアクセスすることができます。 また、ローカル変数と異なり、グローバル変数の寿命は半永久的です

import std.stream;

int value = 10;

int main() {
	int value = 100;

	stdout.printf("value = %d\n" , value);
	ShowValue();
	return 0;
}

void ShowValue() {
	stdout.printf("ShowValue() : value = %d\n" , value);
}

このプログラムでは、グローバル変数 value と、main() 関数内にローカル変数 value が宣言されています。 それぞれの変数はスコープが異なるため、識別子の衝突は問題ありません。

変数にアクセスしようとすると、変数にアクセスした式のスコープに最も近いスコープの変数が参照されます。 すなわち、main() 関数内で value にアクセスするとローカル変数を表し、ShowValue() 関数内の vlaue はグローバル変数を表しています。

このプログラムで、main() 関数からグローバル変数 value にアクセスしたい場合は原始式の . 演算子を用いて次のように記述します

. 変数名

通常の変数へのアクセスは、最も深い内部のスコープから順に外のスコープを検索しますが、. 演算子が指定されている場合は最も外側のスコープから変数を検索します。

import std.stream;

int value = 10;

int main() {
	int value = 100;

	stdout.printf("value = %d\n" , .value);
	return 0;
}

このプログラムを実行すると、printf() 関数の引数に指定している .value がグローバル変数を参照していることを確認することができます。


関数の引数と戻り値

D 言語の関数も、C 言語同様に、関数を呼び出すときにデータを渡したり、関数終了時にデータを戻り値から受け取ることができます。 戻り値を返す関数は、戻り値型を適切に宣言し、型に互換性のある値を return 文を用いて返します。 引数を受け取る場合は、仮引数リストに受け取る値の型と変数名を次のように宣言しなければなりません。

型 変数 , 型 変数 , ...

例えば、数値と浮動小数点数を引数から受け取り、戻り値として bit 型の値を返す関数 Popotan は次のように定義することができます。

bit Popotan(int value1 , double value2) {
	...
	return n;
}

この宣言では引数として受け取る変数の名前は value1、value2 としていますが、この名前は任意でかまいません。 関数内では、受け取った引数の値をこの変数から得ることができます。

ところが、関数は性質上、呼び出し元に返す値は 1 津で泣ければならないという制限があります。 この制限は、C 言語ではポインタから呼び出しもとの変数に間接参照することで解決することができました。 次のようなプログラムです。

void SetPoint(int* x , int* y) {
	*x = 10;
	*y = 100;
}

このプログラムは、ローカル変数にデータを格納しているのではなく、ポインタを通じて呼び出し元が提供した数値へ値を代入してるのです。 数値は、ポインタが参照しているメモリ位置に格納されるため、関数を越えてデータを代入することができているのです。

しかし、危険なポインタを使わずにプログラムを行う D 言語では、ポインタを使ってデータを通信する方法は確実ではありません。 そこで、引数に対して引数の性質を表すパラメータを指定する方法が採用されています。 この手法は Microsoft の C# 言語などでも利用されています。

デフォルトの引数は、呼び出し元から関数に対してデータを提供する in パラメータが指定されています。明示的に指定しても問題はありません。 これに対し、ポインタから間接参照するような、呼び出しもとの変数にデータを出力することが目的の引数には out パラメータを指定しなければなりません。 さらに、データを入出力するような場合は inout パラメータを利用します。 これらのパラメータは、引数の型の手前のトークンとして指定なければなりません

import std.stream;

int main() {
	int outValue;
	exciseTax(1572 , outValue);
	stdout.printf("1572 = %d\n" , outValue);

	return 0;
}

void  exciseTax(in int value1 , out int value2) {
	value2 = value1 * 0.05;
}

このプログラムの exciseTax() 関数は、入力用の引数 value1 に指定された値の 5 パーセント消費税を算出して、出力用の引数 value2 に返すというものです。 in パラメータが指定された入力用の引数に値を代入しても、入力用の引数はローカル変数なので他の関数に影響は与えません。 しかし、出力用の引数は呼び出し元が指定した変数を参照しているため、呼び出し元の変数に値を返すことができるのです。

当然ですが、コンパイラは引数の意味をパラメータによって理解しているため、out または inout が指定されている引数にリテラルを指定することはできません。 これらの引数には、必ず互換性のある型の変数を指定しなければならないのです。


内部関数

D 言語では、関数の内部でローカル関数を宣言する、内部関数宣言を認めています。 内部関数の宣言は通常の関数と同じですが、関数の中で宣言されるという点で異なります。 内部関数は、関数を宣言してる関数以外からは見ることができないため、関数のスコープがローカル変数と同じ扱いになります。

次の関数宣言 Komugi() は Momoi() 関数の内部で宣言されている内部関数であると解釈されます。

void Momoi() {
	void Komugi() { 関数本体 ... }
	Komugi();
}
void main() {
	Komugi();	//エラー(他の関数から内部関数は見えない)
}

内部関数は、比較的複雑で長いコードを実行する関数の内部で、一時的な計算処理などをマクロ関数化する目的に利用することができるでしょう。 もちろん、内部関数の内部関数を定義することも可能です。 内部関数の内部関数から、親関数のローカル変数や内部関数にアクセスすることは可能です。

void Under17() {
	int x;
	void Momoi() {
		int y;
		void Komugi() {
			x = 10;	//OK
			...
		}
		...
	}
	y = 10;	//エラー
}

このように、内部関数を入れ子にすることは可能ですが、あまり重要ではありません。 通常、マクロ的な利用方法以外に内部関数を利用することはありません。

import std.stream;

int main() {
	Kurohune();
	Kaikoku();
	return 0;
}
void Kurohune() {
	void RandL() {
		stdout.writeLine("R と L の発音の区別もできないくせにぃ〜");
	}

	void Kaikoku() {
		stdout.writeLine("Oh 開国してください〜");
		RandL();
	}

	Kaikoku();
}

void Kaikoku() {
	stdout.writeLine("いいじゃない開国ぅ〜");
}

このプログラムを実行すると、内部関数が実行されていることを確認することができます。 Kurohune() 関数の内部で定義されている内部関数 RandL() が、同じく内部関数 Kaikoku() から呼び出されていますが、これは問題ありません。 内部関数から、親関数で宣言されている識別子は、参照することができるのです。 ただし、逆に関数内部の識別しに関数の外からアクセスすることはできません。

ところで、このプログラムではグローバルな関数 Kaikoku() と Kurohune() 関数の内部関数 Kaikoku() の名前が衝突しています。 しかし、グローバルな領域から Kurohune() 関数の内部関数は隠蔽されているため、この衝突は問題ありません。 main() 関数の内部で呼び出している Kaikoku() 関数はグローバルな Kaikoku() 関数となります。 一方、Kurohune() 関数の内部で呼び出している Kaikoku() 関数は、内部関数です。 識別子が衝突している場合、同一スコープ内の識別子が常に優先されるためです。



前のページへ戻る次のページへ