11. 手続きと関数
手続きと関数はいわゆるサブルーチン (副プログラム) です。手続き (procedure) は実行時に値を返さないルーチンです。関数 (function) は実行時に値を返すルーチンです。
手続き・関数 =
手続き・関数ヘッダ ";" (ブロック | 指令) ";".
手続き・関数ヘッダ =
(手続きヘッダ | 関数ヘッダ).
Delphi だと正確には次のようになっています。
手続き・関数 =
手続き・関数ヘッダ ";" [ブロック ";"].
手続き・関数ヘッダ =
(手続きヘッダ | 関数ヘッダ) [";" 指令指定].
指令指定 =
[指令] [ヒント指令].
手続き・関数はプログラムブロックの手続き・関数宣言部に書かれます。
11.1. 手続き (Procedures)
これまでは標準手続きを "使って" きましたが、この節では手続きの作り方を述べます。
手続き宣言はプログラム部分を定義してそれに名前を関連付ける働きがあります。宣言した手続きは手続き呼び出し文によって実行されます。手続きの宣言は program と同じ形をしていますが、プログラムヘッダの代わりに手続きヘッダで始まります。
手続きヘッダ =
procedure 識別子 [仮パラメータリスト].
手続きの例は次のようになります。
program ProcedureTest(output);
var
A: Integer; {*4}
B: Integer; {*4, *5}
procedure Test; {*1}
var {*2}
i: Integer; {*3}
B: Integer; {*3, *5}
begin
B := 0;
for i:=1 to 10 do
B := B + i;
A := A + B;
end; { Test }
begin
A := 1;
B := 2;
Test; { *6 }
Writeln(A);
Writeln(B);
Test; { *6 }
Writeln(A);
Writeln(B);
end.
実行結果は次の通りです。
56
2
111
2
上記のプログラムは簡単なものですが様々な事項が含まれています。
-
手続きヘッダ:
パラメータのない手続き Test() です。 -
ブロック:
手続きは名前の付いたブロックです。プログラムブロックは ProcedureTest で、その中に手続きブロック Test があります。手続きのブロックにはプログラムブロックと同じように宣言部があります。 -
ローカル変数 (局所変数):
手続き Test() の局所的な変数は i と B で、これらの変数は手続き Test() の有効範囲でしかアクセスできません。 -
グローバル変数 (大域変数):
変数 A と B がメインプログラム (主プログラム) ProcedureTest で宣言された大域的な変数です。これらの変数はプログラムの全域で参照可能で、手続き Test() の中で A に値を代入しています。 -
有効範囲 (スコープ):
変数 i はグローバル変数にもローカル変数にもありますが、それらは同じ変数ではありません。 -
手続き呼び出し文:
メインプログラムが手続き Test() を 2 回実行しています。このようにプログラム部分を手続きにすると、同じ処理を何度も書かずに済みます。
手続き呼び出し文の構文は次の通りです。
手続き呼び出し文 =
手続き名 (実パラメータリスト | 書き出しパラメータリスト).
※ 書き出しパラメータリスト
は Write() および Writeln() のための特殊な構文です (12.3. 節)。
See also:
11.1.1. パラメータリスト (パラメータ並び / Parameter list)
先のプログラムは手続き内でグローバル変数を直接読み書きしており、計算の回数も変更できなかったので、パラメータを持つように変更してみました。
program ProcedureTest2(output);
var
A: Integer;
B: Integer;
procedure Test(L: Integer; var V: Integer); { *1, *2, *4 }
var
i: Integer;
B: Integer;
begin
B := 0;
for i:=1 to L do
B := B + i;
V := V + B;
end; { Test }
begin
A := 1;
B := 2;
Test(10, A); { *3 }
Writeln(A);
Writeln(B);
Test(20, A); { *3 }
Writeln(A);
Writeln(B);
end.
1. 手続きヘッダ:
パラメータリストを持つ形式になっています。
2. 仮パラメータ (仮引数 / パラメータ / parameter / formal-parameter):
パラメータリストには仮パラメータの名前とその型を書きます。手続き Test() は仮パラメータとして L と V を持っています。
3. 実パラメータ (実引数 / 引数 / argument / actual-parameter):
手続き呼び出し文には実パラメータのパラメータリストを含んでいます。実パラメータと仮パラメータは同じ並びである必要があります。渡せるパラメータには値パラメータ、変数パラメータ、手続きパラメータ、関数パラメータ の 4 種類があります。Delphi にはこの他にも 定数パラメータ、out パラメータ等があります。
4. 値パラメータ (Value parameters) と変数パラメータ (Variable parameters):
パラメータ L は値パラメータであるので、式 (変数を含む) を実パラメータとして渡せます。予約語 var が付加されたパラメータ V は変数パラメータであり、手続き内から値を変更する事ができます。
仮パラメータリストの定義は次の通りです。各仮パラメータはセミコロン ;
で区切ります。他言語の関数プロトタイプ宣言では ,
を使うので、最初は戸惑うかもしれません。
仮パラメータリスト =
"(" 仮パラメータ {";" 仮パラメータ} ")".
仮パラメータ =
[var | CONST | OUT] パラメータ.
パラメータ =
識別子 {"," 識別子} [':' 型名].
同じ型を持つ仮パラメータはカンマで区切って指定する事ができます。例えば次の手続きヘッダは、
procedure TEST(a: Integer; b: Integer);
このように短く書く事ができます。変数宣言部と似た記述が可能です。
procedure TEST(a, b: Integer);
実パラメータリストの定義は次の通りです。各実パラメータはカンマ ,
で区切ります。
実パラメータリスト =
"(" 実パラメータ {"," 実パラメータ} ")".
実パラメータ =
{ [変数 | 式] }.
パラメータのない手続きの呼び出しでは (
)
を省略できます 1。
DoSomething();
DoSomething;
パラメータの種類は次の通りです。
パラメータの種類 | 予約語 | 実パラメータ (呼び出し時) |
入力 | 出力 |
---|---|---|---|---|
値パラメータ (値渡し or 参照渡し2) |
式 | 〇 | × | |
変数パラメータ (参照渡し) |
var | 変数 | 〇 | 〇 |
定数パラメータ (値渡し or 参照渡し2) |
const | 式 | 〇 | × |
定数パラメータ (参照渡し) |
[Ref] const | 式 | 〇 | × |
out パラメータ (参照渡し) |
out | 変数 | × | 〇 |
値パラメータ (予約語での修飾なし) は、手続きまたは関数の呼び出し時に渡す値に初期化されるローカル変数のような働きをします (値パラメータに代入可能です)。
program ParamTest(output);
var
i: Integer;
procedure Proc(v: Integer);
begin
v := v * 2; { ローカル変数のように使える }
writeln(v);
end;
begin
i := 3;
Writeln(i); { 3 }
proc(i); { 6 }
Writeln(i); { 3 }
end.
Pascal の値パラメータは値渡しですが、Delphi の値パラメータは値渡しとは限りません。
変数パラメータ (var) は参照渡し (変数渡し) を強制しますが、定数パラメータ (const) は値渡しを強制するものではありません。パラメータへの代入 (出力) ができない事を強制します。定数パラメータに代入しようとするとコンパイルエラーになります。
Delphi XE3 以降では、const に [Ref] コンパイラ属性を付けて定数パラメータに参照渡しを強制させる事ができます。コンパイラ属性の指定は const [Ref]、[Ref] const どちらでも構いません。
out パラメータはパラメータへの代入 (出力) のみを許可します。渡された時に実パラメータは初期化されてしまうので実パラメータの値を参照 (入力) する事はできません。out パラメータは COM (Component Object Model) 以外の用途で使われることはあまりありません。
多くのケースで out パラメータは var パラメータと同等の動作 (入力が可能) になるため var の使用が推奨されています。もちろん COM (あるいは Delphi for .NET) で使用する場合においてはこの限りではありません。
program ParamTest;
{$APPTYPE CONSOLE}
uses
System.SysUtils;
var
lv1, lv2, lv3, lv4, lv5: Integer;
procedure Test( v1: Integer;
var v2: Integer;
const v3: Integer;
[Ref] const v4: Integer;
out v5: Integer);
begin
Writeln(Format('v1: %.8x : %.8x ', [@lv1, @v1]));
Writeln(Format('v2: %.8x : %.8x var ', [@lv2, @v2]));
Writeln(Format('v3: %.8x : %.8x const ', [@lv3, @v3]));
Writeln(Format('v4: %.8x : %.8x [Ref] const', [@lv4, @v4]));
Writeln(Format('v5: %.8x : %.8x out ', [@lv5, @v5]));
end;
begin
Test(lv1, lv2, lv3, lv4, lv5);
end.
実行結果は次のようになります。
※サンプルコードを簡潔にするため、外部関数 Format() を使っています。
Delphi 10.1 Berlin 以降、すべてのプラットフォームで次のコンパイラ属性を付ける事が可能です。
- var または out パラメータに [Unsafe] コンパイラ属性を付けて [Unsafe] な属性の変数(またはメンバ)を渡す事を強制させる事ができます。
- var または out パラメータに [Weak] コンパイラ属性を付けて [Weak] な属性の変数(またはメンバ)を渡す事を強制させる事ができます。
- パラメータに [Volatile] コンパイラ属性を付けて最適化による破棄を行わないようにする事ができます。
但し、Delphi 10.4 Sydney で ARC が廃止されたため、現在 [Unsafe]
と [Weak]
についてはインターフェイス参照のためだけに利用可能です。
See also:
11.1.2. 整合配列パラメータ (Conformant array parameters)
配列型のパラメータをとるルーチンを宣言するとき、そのパラメータ宣言でインデックスの型を指定することはできません。次の宣言はエラーになります。
procedure DoSomething(A: array [1..10] of Integer);
type arr10 = array [1..10] of Integer; のように、あらかじめ配列型を定義しておけば、パラメータに指定することが可能になります。
procedure DoSomething(A: arr10);
それとは別の問題として、ここまでに紹介した文法では 任意の長さの配列をパラメータにとるルーチン
を作る事もできません。
上記の問題を解決するための仕組みが整合配列パラメータなのですが、Delphi ではこのパラメータ形式をサポートしていません。標準 Pascal においてもオプションです。
整合配列形式 =
パック状整合配列形式 | アンパック状整合配列形式.
パック状整合配列形式 =
packed array "[" 添字型仕様 "]" of 型名.
アンパック状整合配列形式 =
array "[" 添字型仕様 { ";" 添字型仕様} "]" of (型名 | 整合配列形式).
添字型仕様 =
識別子 ".." 識別子 ":" 順序型名
次の例は任意の長さの文字列 (文字配列) を受け取り、シーザー暗号で出力します。
program CaesarCipher(output);
type
Positive = 1..MaxInt;
var
Str1: packed array [1..3] of Char;
Str2: packed array [1..12] of Char;
procedure CCStr(Str: packed array [M..N: Positive] of Char);
var
i: Integer;
begin
for i := M to N do
Write(Chr(Ord(Str[i]) - 1));
Writeln;
end;
begin
Str1 := 'IBM';
CCStr(Str1);
Str2 := 'Hello,World.';
CCStr(Str2);
end.
実行結果は次のようになります (ideone.com で試せます)。
HAL
Gdkkn+Vnqkc-
標準 Pascal の規格で定義されている言語機能をすべて対応した上で、この整合配列パラメータに対応すれば 標準 Pascal 水準 1 (Standard Pascal Level 1)、対応しなければ 標準 Pascal 水準 0 (Standard Pascal Level 0) となります。
11.1.3. 再帰手続き (Recursive procedures)
ルーチン内でそのルーチン自身を呼び出すことを再帰呼出し (Recursive call)と言い、それを行う手続きを再帰手続きと呼びます。関数なら再帰関数です。
次のコードは階乗を求める再帰手続きです。
program RecursiveProcedureTest(output);
var
f: Integer;
procedure fact(n: Integer; var v: Integer);
begin
if (n > 0) then
begin
v := v * n;
fact(n - 1, v);
end;
end;
begin
f := 1;
fact(5, f);
Writeln(f);
end.
11.1.4. 手続きパラメータ (Procedural parameters)
Delphi ではこのパラメータ形式をサポートしていません。また、標準 Pascal では標準手続き を実手続きパラメータとして渡せません。
パラメータの種類 | 予約語 | 仮パラメータ (宣言時) |
実パラメータ (呼び出し時) |
---|---|---|---|
手続きパラメータ | 手続きヘッダ | 手続き |
program ProcParamTest(output);
var
A: Integer;
procedure Proc1(V: integer);
begin
A := A + (V * 3);
end; { Proc1 }
procedure Proc2(V: integer);
begin
A := A - (V * 5);
end; { Proc2 }
procedure Test(procedure Proc(V: integer));
begin
Proc(10);
end; { Test }
begin
A := 0;
Test(Proc1);
Writeln(A);
Test(Proc2);
Writeln(A);
end.
実行結果は次の通りです。
30
-20
(11.1.5.) オープン配列パラメータ (Open Array Parameters)
Delphi では、配列パラメータを実現するための別の解としてオープン配列パラメータが実装されています。
オープン配列パラメータを使用すると、基底型が同じでサイズ (要素数) だけが異なる配列をルーチンに渡す事ができます。
procedure DoSomething(A: array of Integer);
オープン配列パラメータの構文は動的配列の構文に似ていますが同じではありません。実パラメータとして動的配列だけでなく静的配列も渡せます。ルーチンの中では次のようにして渡された配列を処理できます。
procedure Clear(var A: array of Real);
var
I: Integer;
begin
for I := 0 to High(A) do
A[I] := 0;
end;
パラメータに配列そのものを渡しているわけではないため、SetLength() で動的配列の要素数を変更したりする事はできず、
procedure Clear(var A: array of Real);
var
I: Integer;
begin
SetLength(A, 10); // 変更できない
for I := 0 to High(A) do
A[I] := 0;
end;
var
ra: array of Real;
begin
SetLength(ra, 5); // 配列の要素数を 5 に
Clear(ra);
end.
二次元のオープン配列パラメータなんていうものもありません。
procedure Clear(var A: array of array of Real); // NG
オープン配列パラメータを使ったルーチンには、オープン配列コンストラクタも渡すことができます。
See also:
(11.1.6.) 型可変オープン配列パラメータ (Variant Open Array Parameters)
型可変オープン配列パラメータ (型なしの配列パラメータ) を使用すると、基底型すら異なる配列を渡す事ができます。
procedure DoSomething(A: array of const);
array of const は array of TVarRec と同じなので、TVarRec.VType を使って型ごとに処理を分岐させる事ができます。
function MakeStr(const Args: array of const): string;
var
I: Integer;
begin
Result := '';
for I := 0 to High(Args) do
with Args[I] do
case VType of
vtInteger: Result := Result + IntToStr(VInteger);
vtBoolean: Result := Result + BoolToStr(VBoolean);
vtChar: Result := Result + VChar;
vtExtended: Result := Result + FloatToStr(VExtended^);
vtString: Result := Result + VString^;
vtPChar: Result := Result + VPChar;
vtObject: Result := Result + VObject.ClassName;
vtClass: Result := Result + VClass.ClassName;
vtAnsiString: Result := Result + string(VAnsiString);
vtUnicodeString: Result := Result + string(VUnicodeString);
vtCurrency: Result := Result + CurrToStr(VCurrency^);
vtVariant: Result := Result + string(VVariant^);
vtInt64: Result := Result + IntToStr(VInt64^);
end;
end;
型可変オープン配列パラメータにもオープン配列コンストラクタを渡すことができます。
See also:
(11.1.7.) オープン配列コンストラクタ (Open Array Constructors)
オープン配列コンストラクタにより、関数および手続き呼び出し内で、直接、配列を構築することができます。それらは、オープン配列パラメータまたは、型可変オープン配列パラメータとしてのみ渡すことができます。
例えば次のような定義 (オープン配列パラメータ) があった場合、
procedure Add(A: array of Integer);
オープン配列コンストラクタを使って Add() 手続きを次の文で呼び出すことができます:
Add([5, 7, I, I + J]);
次のような定義 (型可変オープン配列パラメータ) なら、
procedure Add(A: array of const);
オープン配列コンストラクタを使って Add() 手続きを次の文で呼び出すことができます:
Add([5, 7, I, I + J]);
Add([100, 'A', 3.14]);
オープン配列コンストラクタを頻繁に使う最も有名なルーチンは System.SysUtils にある Format() 関数です。
uses
..., System.SysUtils;
s := Format('%s: %.4d', ['Hello', 123]);
See also:
(11.1.8.) 型なしパラメータ (Untyped Parameters)
パラメータは次のように定義されていました。
パラメータ =
識別子 {"," 識別子} [':' 型名].
よく見ると解るのですが、パラメータは :型名
を省略する事ができます。
具体的には値パラメータ以外のパラメータ (var / const / out) であれば、型を省略した 型なしパラメータ にする事ができます。これにより、任意の長さの配列をパラメータにとるルーチン
を作る事が可能になります。
例えば System.FillChar()
の最初のパラメータは型なしパラメータとなっています。
procedure FillChar(var X; Count: NativeInt; Value: Integer);
短い文字列 (ShortString) に限れば、型なしパラメータを使わずとも "任意の長さの配列パラメータ問題" を回避する方法があります。後述の (11.1.9.) オープン文字列パラメータ (Open String Parameters) を参照してください。
See also:
(11.1.9.) オープン文字列パラメータ (Open String Parameters)
Delphi のオープン文字列パラメータは、ANSI 版 Delphi かつ String が短い文字列 (ShortString) である ({$H-}
) 場合にのみ意味を持ちます。
次に示すコードのパラメータ S の string はString 型を示しているのではなく、オープン配列パラメータの文字列版である オープン文字列パラメータ を示しています。長さの異なる文字列変数をルーチンのパラメータとして渡す事ができます。
program OpenStringParameterTest(Output);
{$APPTYPE Console}
var
S1: string[10];
S2: string[20];
procedure AssignStr(var S: string); // Delphi 1 以前ではオープン文字列パラメータ
begin // Delphi 2 以降では長い文字列の変数 (var) パラメータ
S := '0123456789ABCDEF';
end; { AssignStr }
begin
AssignStr(S1); { '0123456789' }
AssignStr(S2); { '0123456789ABCDEF' }
end.
コンパイラ指令 {$P}
はパラメータの string をオープン文字列パラメータとみなすかどうかのスイッチです。デフォルトで{$P+}
ですが、{$P-}
だと string パラメータはただの変数パラメータとみなされます。
Delphi 1 では上記コードのコンパイルが通ったのですが、Delphi 2 以降では、コンパイルエラーになりました。S1 と S2 が 短い文字列 (ShortString) であるのに対し、S はオープン文字列パラメータではなく長い文字列 (string) の変数パラメータだからです。
この互換性の問題を解決するためには 2 つの方法があります。一つはコンパイラ指令 {$H-}
を使い、string 型を短い文字列 (ShortString) とみなすようにする方法です。影響範囲が広いですが、大量のコードを移植しなければならない時には有効でしょう。
もう一つは OpenString 識別子を使う方法です。OpenString は正確には型ではなく、オープン文字列パラメータを示す識別子で、次のコードでは、パラメータ S はオープン文字列パラメータとして認識されます (型としては ShortString)。
procedure AssignStr(var S: OpenString);
begin
S := '0123456789ABCDEF';
end; { AssignStr }
”String” という識別子を使っていないため、コンパイラ指令 {$P}
の影響も受けません。
コンパイラ指令 {$V}
というのもあります。このオプションは本質的に "安全でないバージョンのオープン文字列パラメータ" を提供します。{$P+}
が指定されている場合には、string パラメータはオープン文字列パラメータですので、{$V}
の効果はありません。
program OpenStringParameterTest2(Output);
{$APPTYPE Console}
procedure MyProc(var s: string); { ここの "string" の意味は?}
begin
s := 'abcdefghijk'; { 11 文字の代入 }
end; { MyProc }
var
ss: string[5]; { 短い文字列 (5 文字分) }
begin
MyProc(ss);
end.
上記コードと {$P}
および {$V}
コンパイラ指令の関係は次の通りです。
{$P} | {$V} | 説明 |
---|---|---|
{$P+} |
{$V+} or {$V-}
|
オープン文字列パラメータ。 S への割り当てが実パラメータで宣言された サイズを超えないことを保証する。 |
{$P-} |
{$V+} |
MyProc(ss) は型不一致の コンパイルエラーを生成する。 |
{$P-} |
{$V-} |
MyProc はコンパイラエラーを生成しないが、 プログラム内で上書きエラーになり、 システムがクラッシュする可能性がある。 |
String が短い文字列 だったのは Delphi 1 とそれ以前の Turbo Pascal ですから、これらのコンパイラ指令はもはや気にする必要がないと思います。
むしろ Unicode 版 Delphi になってずいぶん経ちますが、未だにコンパイラ指令 {$H}
・{$P}
・{$V}
がプロジェクトオプションに存在するのがとても不思議です。
See also:
(11.1.10.) デフォルトパラメータ (Default Parameters)
手続きや関数のヘッダー (仮パラメータ) で、デフォルトパラメータ値を指定できます。デフォルト値を指定できるのは、値パラメータと定数パラメータだけです。
たとえば、次のような宣言は
procedure FillArray(A: array of Integer; Value: Integer = 0);
次のいずれの呼び出しでも結果は同じになります。
FillArray(MyArray);
FillArray(MyArray, 0);
複数のパラメータを一度に宣言する場合には、デフォルト値は指定できません。 従って、次の宣言は無効です。
function MyFunction(X, Y: Real = 3.5): Real; // syntax error
デフォルト値を指定するパラメータは、パラメータ リストの末尾に置かなければなりません。 つまり、あるパラメータにデフォルト値を指定したら、それ以降のパラメータにはすべてデフォルト値を指定する必要があります。 従って、次の宣言は無効です。
procedure MyProcedure(I: Integer = 1; S: string); // syntax error
See also:
(11.1.11.) ルーチンのオーバーロード (Overloading Routines)
同じスコープ内で同じ名前のルーチンを複数宣言することができます。これは、オーバーロードと呼ばれます。オーバーロードされるルーチンは、overload 指令を付けて宣言する必要があり、パラメータ リストで区別できなければなりません。
function Divide(X, Y: Real): Real; overload;
begin
Result := X / Y;
end
function Divide(X, Y: Integer): Integer; overload;
begin
Result := X div Y;
end;
オーバーロードされたルーチンは、受け取るパラメータの数またはそれらのパラメータの型で区別できなければなりません。
See also:
(11.1.12.) Exit() 手続き
Delphi では Exit() 手続きを使って現在のルーチンから抜ける事ができます。メインプログラムで実行するとプログラムが終了します。
procedure foo(a: Integer);
begin
if (a < 0) and (a > 100) then
Exit;
...
end;
See also:
(11.1.13.) Halt() 手続き
Delphi では Halt() 手続きを使ってプログラムを 異常終了 させる事ができます。パラメータとして終了コードを渡す事ができます。正常終了する手順は次の通りです。
-
VCL アプリケーションの場合:
Application.Terminate() を呼び出します。 -
FMX アプリケーションの場合:
Application.Terminate() を呼び出します。 -
コンソールアプリケーションの場合:
メインプログラムで Exit() 手続きを呼び出します。
See also:
11.2. 関数 (Functions)
関数宣言はプログラム部分を定義してそれに名前を関連付ける働きがあります。宣言した関数は関数呼び出しによって実行されます。関数宣言は program とほぼ同じ形をしていますが、プログラムヘッダの代わりに関数ヘッダで始まります。
関数ヘッダ =
functon 識別子 [仮パラメータリスト] ":" 型名.
ほぼ手続きと同じですが、関数は値を返す (結果 / 戻り値) ため、関数呼び出しは、代入や演算子内の式として使用できます。
結果型 (Result Type)
結果の型 (結果型) は単純型またはポインタ型の識別子である必要があります。標準 Pascal では結果型に構造化型は使えません。Delphi だと一旦型を定義すれば結果型に構造化型が使えます。
program ResultTest;
type
Str10 = array [1..10] of Char;
TIDPass =
record
ID: Integer;
Pass: Str10;
end;
// NG
function Sub1: array [1..10] of Char;
begin
...
end;
// OK
function Sub1: Str10;
begin
...
end;
// NG
function Sub2: record ID: Integer; Pass: Str10 end;
begin
...
end;
// OK
function Sub2: TIDPass;
begin
...
end;
begin
end.
結果 (Result)
Pascal では関数が返す値を 結果 (result) と呼びます。他の言語では 戻り値 (return value) と呼ばれる事が多いですが、当記事では由緒正しく結果
と表記しています。
関数で結果を返すには、標準 Pascal だと関数名に対して値を代入します。関数名は、その関数の結果を保持する特殊な変数として使用できます。
function Add(A, B: integer): Integer;
begin
Add := A + B;
end;
Delphi では拡張構文 {$X}
がデフォルトで有効 ({$X+}
) になっているので、関数ブロックで定義済みの変数 Result を使って、関数の結果を保持することができます。Result 変数のスコープは関数ブロック内です。
function Add(A, B: integer): Integer;
begin
Result := A + B;
end;
Result 変数を使うメリットは主に 2 つあります。
- 関数名の変更 (リファクタリング) が容易になる。
- 結果への代入と再帰関数を混同しなくなる。
Delphi 2009 以降では Exit() 手続きで、関数の結果をパラメータで受け取れるようになりました。C 言語の return 文のような記述が可能になっています。
function Add(A, B: integer): Integer;
begin
Exit(A + B);
end;
See also:
関数呼び出し (Function Call)
4.2. 節 で説明した通り、標準 Pascal は結果が不要でも結果を利用しない関数は呼び出せません。つまり、標準 Pascal での関数は必ず式の中で使われるため、関数呼び出し文というのはありませんでした。Delphi では結果を破棄できるため、関数呼び出し文が成立します。
また、パラメータのない関数の呼び出しでは (
)
を省略できます 1。
v := DoSomething();
v := DoSomething;
See also:
再帰関数 (Recursive functions)
次のコードは階乗を求める再帰関数です。
program RecursiveFunctionTest(output);
var
f: Integer;
function fact(n: Integer): Integer;
begin
if (n = 0) then
fact := 1
else
fact :=
fact(n - 1) * n;
end;
begin
f := fact(5);
Writeln(f);
end.
関数名が代入文の左辺にある場合、コンパイラはそれを結果を記録するために使われているものと見なします。関数名がステートメントブロックのその他の場所にある場合、コンパイラはそれをその関数の再帰呼び出しと解釈します。
結果を返すのに Result 変数を使うと再帰呼び出しとの判別が容易になります。
program RecursiveFunctionTest(output);
var
f: Integer;
function fact(n: Integer): Integer;
begin
if (n = 0) then
Result := 1
else
Result :=
fact(n - 1) * n;
end;
begin
f := fact(5);
Writeln(f);
end.
11.2.1. 関数パラメータ (Functional parameters)
手続きパラメータと同様、Delphi ではこのパラメータ形式をサポートしていません。標準 Pascal では標準関数 を実関数パラメータとして渡せません。
パラメータの種類 | 予約語 | 仮パラメータ (宣言時) |
実パラメータ (呼び出し時) |
---|---|---|---|
関数パラメータ | 関数ヘッダ | 関数 |
次に挙げるコードは『J&W (初版・第 2 版)』にある関数パラメータの例です。
関数 zero()
の第 1 パラメータには、function sin(x: Real): Real;
のような関数が渡される事が想定されていますが、古い Pascal では関数パラメータは結果の型さえ合致していればよく、渡す関数のパラメータを記述する必要はありませんでした。
{ program 11.6
find zero of a function by bisection }
program bisect(input, output);
const eps = 1E-14;
var x, y: real;
function zero(function f: real; a, b: real): real;
var x, z: real; s: boolean;
begin
s := f(a) < 0;
repeat
x := (a + b) / 2.0;
z := f(x);
if (z < 0) = s then a := x else b := x;
until abs(a - b) < eps;
zero := x;
end; { zero }
begin
read(x, y); writeln(x, y, zero(sin, x, y));
read(x, y); writeln(x, y, zero(cos, x, y));
end. { main }
標準 Pascal では関数パラメータを関数ヘッダと同じ書式で書く必要があります。ただし、Pascal-P5 では次のコードはコンパイル可能ですが実行時エラーが発生します。
{ program 11.6
find zero of a function by bisection }
program bisect(input, output);
const eps = 1E-14;
var x, y: real;
function zero(function f(v: real): real; a, b: real): real;
var x, z: real; s: boolean;
begin
s := f(a) < 0;
repeat
x := (a + b) / 2.0; z := f(x);
if (z < 0) = s then a := x else b := x;
until abs(a - b) < eps;
zero := x;
end; { zero }
begin
read(x, y); writeln(x, y, zero(sin, x, y));
read(x, y); writeln(x, y, zero(cos, x, y));
end. { main }
何故実行時エラーが発生するかというと、sin()
や cos()
が標準関数だからです。標準 Pascal では標準関数を関数パラメータとして渡せなくなっています 3。次のように標準関数をラッピングしてやれば実行時エラーは発生しません。
{ program 11.6
find zero of a function by bisection }
program bisect(input, output);
const eps = 1E-14;
var x, y: real;
function sin2(v: real): real;
begin
sin2 := sin(v);
end; { sin2 }
function cos2(v: real): real;
begin
cos2 := cos(v);
end; { cos2 }
function zero(function f(v: real): real; a, b: real): real;
var x, z: real; s: boolean;
begin
s := f(a) < 0;
repeat
x := (a + b) / 2.0; z := f(x);
if (z < 0) = s then a := x else b := x;
until abs(a - b) < eps;
zero := x;
end; { zero }
begin
read(x, y); writeln(x, y, zero(sin2, x, y));
read(x, y); writeln(x, y, zero(cos2, x, y));
end. { main }
詳細は後述しますが、Delphi では手続き型 (Procedural Types) を用いて次のように書く事ができます。パラメータの型や結果型を厳密に合わせる必要があるため、Real 型を Extended 型に変更しています。Delphi の sin()
は function Sin(const X: Extended): Extended;
と定義されています。
{ program 11.6
find zero of a function by bisection }
program bisect(input, output);
type Tf = function (const v: Extended): Extended;
const eps = 1E-14;
var x, y: Extended;
function zero(f: Tf; a, b: Extended): Extended;
var x, z: Extended; s: boolean;
begin
s := f(a) < 0;
repeat
x := (a + b) / 2.0; z := f(x);
if (z < 0) = s then a := x else b := x;
until abs(a - b) < eps;
result := x;
end; { zero }
begin
read(x, y); writeln(x, y, zero(sin, x, y));
read(x, y); writeln(x, y, zero(cos, x, y));
end. { main }
このプログラムを実行して次のように入力すると、
-1 1 1 2〔Enter〕
このような結果が得られます。
-1.00000000000000E+0000 1.00000000000000E+0000 -7.10542735760100E-0015
1.00000000000000E+0000 2.00000000000000E+0000 1.57079632679490E+0000
See also:
11.2.2. 副作用 (Side effect)
『J&W』に掲載されていた、手続きや関数内からグローバル変数を操作すると思わぬ結果をもたらす、というコードのサンプルです。結果を予想するのがとても難しいです。
program SideEffect(Output);
{ プログラム 11.10 - 関数の副作用の例 }
var
A, Z: Integer;
function Sneaky(X: Integer): Integer;
begin
Z := Z - X { Z への副作用 };
Sneaky := Sqr(X);
end; { Sneaky }
begin
Z := 10; A := Sneaky(Z);
Writeln(Output, A, Z);
Z := 10; A := Sneaky(10); A := A * Sneaky(Z);
Writeln(Output, A, Z);
Z := 10; A := Sneaky(Z); A := A * Sneaky(10);
Writeln(Output, A, Z);
end. { SideEffect }
上記コードの実行結果は次の通りです。
100 0
0 0
10000 -10
Pascal においての 副作用 とは、手続き / 関数内で定義されていない変数または変数パラメータへの代入の事を指します。
11.3. 前方宣言 (Forward declaration)
手続きや関数は前方宣言があれば、その手続きや関数の宣言よりも前で使用できます。
procedure A(v: Integer);
begin
B(1); { 呼び出せない }
end;
procedure B(v: Integer);
begin
end;
上記の問題は forward 指令を使って前方宣言する事によって回避できます。
procedure B(v: Integer); forward;
procedure A(v: Integer);
begin
B(1); { 呼び出せる }
end;
procedure B; { 仮パラメータと結果型はここに書かない }
begin
end;
Delphi の場合、前方宣言はプログラムファイル (*.dpr) 内だけで使えます。
forward 指令は、ユニットの interface セクションでは無効です。interface セクション内の手続きと関数のヘッダーは、forward 宣言と同様の動作をするので、implementation セクションで定義宣言をする必要があります。
unit Unit1;
interface
// 関数のヘッダー宣言
procedure A(v: Integer);
procedure B(v: Integer);
implementation
procedure A(v: Integer);
begin
B(1); { 呼び出せる }
end;
procedure B(v: Integer);
begin
end;
end.
See also:
(11.4.) インライン展開 (Inline expansion)
手続きや関数は inline 指令を使う事でインライン化する事ができます。
procedure foo; inline;
インライン化とはコンパイラによる最適化手法の一つで、ルーチンを呼び出す側に呼び出されるルーチンのコードを展開する事を指します。
ルーチンの呼び出しに伴うオーバーヘッドを削減する事が狙いなので、大きなルーチンに対して使っても実行形式ファイルが肥大化するだけで効果はありません。繰り返し呼ばれる小さなルーチンで効果を発揮します。
コンパイラ指令 {$INLINE}
で、インライン化を制御する事が可能です。
値 | 定義側の意味 | 呼び出し側の意味 |
---|---|---|
{$INLINE ON} (デフォルト) |
ルーチンが inline 指令によってタグ付けされている場合には、インライン化される。 | ルーチンは可能であればインラインに展開される。 |
{$INLINE AUTO} |
{$INLINE ON} と同じ動作に加え、inline でマークされていないコードサイズが 32 バイト以下のルーチンもインライン化される。 |
ルーチンのインライン化に影響を及ぼさない。 |
{$INLINE OFF} |
ルーチンに inline のタグが付いていてもインライン化されない。 | ルーチンはインラインに展開されない。 |
See also:
(11.5.) 手続き型 (Procedural Types)
手続き型は手続きまたは関数 (ルーチン) を代入できる型です。パラメータと結果の型が同じルーチンを代入できます。他言語では関数ポインタ (Function Pointer) とも呼ばれる機能です。
例えば、System.SysUtils では汎用的な手続き型 TProcedure が宣言されています。
{ Generic procedure pointer }
TProcedure = procedure;
TProcedure 型の変数にはパラメータのない手続きを代入できます。
program Project1;
{$APPTYPE CONSOLE}
procedure A;
begin
Write('A');
end;
procedure B;
begin
Write('B');
end;
procedure C;
begin
Write('C');
end;
type
TProcedure = procedure; // パラメータのない手続き
var
i: Integer;
Procs: array [0..2] of TProcedure;
begin
Procs[0] := A;
Procs[1] := B;
Procs[2] := C;
for i:=Low(Procs) to High(Procs) do
Procs[i];
Writeln;
end.
Delphi には手続き型に類似する型として
- メソッドポインタ型 (of object)
- メソッド参照型 (reference to)
がありますが、どちらもクラスに関連するものなので割愛します。
See also:
- 手続き型 (DocWiki)
- System.SysUtils.TProcedure (DocWiki)
- クラスとオブジェクト(Delphi)(DocWiki)
- Delphi での無名メソッド (DocWiki)
(11.5.1.) 手続き型定数とグローバル手続き型変数の初期化
Delphi で手続き型定数を宣言するには、定数の宣言される手続き型と互換性のあるルーチンの名前を指定します。
program ProceduralConstantsTest1;
{$APPTYPE CONSOLE}
// 手続き型の定義
type
TFunction = function(X, Y: Integer): Integer;
var
i: Integer;
function Calc(X, Y: Integer): Integer;
begin
end; { Calc }
// グローバル手続き型定数
const
MyFunction1: TFunction = Calc;
// グローバル手続き型変数と初期化
var
MyFunction2: TFunction = Calc;
procedure Sub;
//var
// ローカル手続き型変数は初期化できない
// MyFunction3: TFunction = Calc;
begin
end; { Sub }
begin
i := MyFunction1(5, 7);
i := MyFunction2(5, 7);
end.
手続き型を定義せずに直接宣言する事もできます。
program ProceduralConstantsTest2;
{$APPTYPE CONSOLE}
var
i: Integer;
function Calc(X, Y: Integer): Integer;
begin
end; { Calc }
const
MyFunction1: function(X, Y: Integer): Integer = Calc;
// グローバル手続き変数と初期化
var
MyFunction2: function(X, Y: Integer): Integer = Calc;
procedure Sub;
//var
// ローカル手続き変数は初期化できない
// MyFunction3: function(X, Y: Integer): Integer = Calc;
begin
end; { Sub }
begin
i := MyFunction1(5, 7);
i := MyFunction2(5, 7);
end.
手続き型変数を使うと次のような事ができます。
program ProceduralTypesFizzBuzz;
{$APPTYPE CONSOLE}
procedure FizzBuzz(X: Integer);
begin
Writeln('FizzBuzz');
end; { FizzBuzz }
procedure Fizz(X: Integer);
begin
Writeln('Fizz');
end; { Fizz }
procedure Buzz(X: Integer);
begin
Writeln('Buzz');
end; { Buzz }
procedure Num(X: Integer);
begin
Writeln(X);
end; { Num }
var
i: Integer;
FB: procedure(X: Integer);
begin
for i:=1 to 100 do
begin
if ((i mod 3) + (i mod 5)) = 0 then
FB := FizzBuzz
else if (i mod 3) = 0 then
FB := Fizz
else if (i mod 5) = 0 then
FB := Buzz
else
FB := Num;
FB(i);
end;
end.
See also:
索引
[ ← 10. ポインタ型 ] [ ↑ 目次へ ] [ → 12. テキストファイルの入出力 ]