手続きと関数(Delphi)
手続きと関数:インデックス への移動
このトピックでは、以下の事項について説明します。
- 手続きと関数の宣言
- 呼び出し規約
- 前方宣言とインターフェイス宣言
- 外部ルーチンの宣言
- 手続きと関数のオーバーロード
- ローカル宣言とネスト ルーチン
手続きと関数
手続きと関数(これらをまとめてルーチンと呼ぶ)は、プログラムのほかの場所から呼び出すことができる自己完結的なステートメント ブロックです。関数は実行時に値を返すルーチンです。手続きは値を返さないルーチンです。
関数は値を返すため、関数呼び出しは、代入や演算子内の式として使用できます。以下に例を示します。
I := SomeFunction(X);
SomeFunction を呼び出して、その結果を I に代入します。関数呼び出しを、代入文の左辺に置くことはできません。
手続き呼び出しと、拡張構文が有効になっている場合({$X+})の関数呼び出しは、完全な文として使用できます。以下に例を示します。
DoSomething;
DoSomething ルーチンを呼び出します。DoSomething が関数の場合、その戻り値は破棄されます。
手続きと関数は、それ自身を再帰的に呼び出すことができます。
手続きと関数の宣言
手続きまたは関数を宣言する場合は、その名前、パラメータの数と型、および関数の場合は戻り値の型を指定します。この部分は、プロトタイプ、ヘディング、またはヘッダーと呼ばれる場合もあります。 次に、手続きまたは関数が呼び出されたときに実行するコード ブロックを記述します。この部分は、ルーチンの本体またはブロックと呼ばれる場合もあります。
手続き宣言
手続きの宣言は、次の形式を持ちます。
procedure procedureName(parameterList); directives;
localDeclarations;
begin
statements
end;
procedureName は有効な識別子、statements は、この手続きが呼び出されたときに実行される文のシーケンスです。(parameterList)、directives;、および localDeclarations; は省略可能です。
手続き宣言の例を以下に示します。
procedure NumString(N: Integer; var S: string);
var
V: Integer;
begin
V := Abs(N);
S := '';
repeat
S := Chr(V mod 10 + Ord('0')) + S;
V := V div 10;
until V = 0;
if N < 0 then S := '-' + S;
end;
この宣言が指定されると、以下のようにして NumString 手続きを呼び出すことができます。
NumString(17, MyString);
この手続き呼び出しによって、値 '17' が MyString(string 型の変数でなければならない)に代入されます。
手続きのステートメント ブロック内では、手続きの localDeclarations 部で宣言されている変数とその他の識別子を使用できます。 パラメータ リストにあるパラメータ名(上の例では N と S)を使用することもできます。パラメータ リストには、1 組のローカル変数が定義されています。したがって、これらのパラメータ名を localDeclarations セクションで宣言してはいけません。 最終的に、この手続き宣言のスコープ内にある任意の識別子を使用できます。
関数宣言
関数の宣言は、戻り値の型と戻り値を指定すること以外は、手続きの宣言と同様です。関数宣言は以下の形式を持ちます。
function functionName(parameterList): returnType; directives;
localDeclarations;
begin
statements
end;
functionName は有効な識別子、returnType は型の識別子、statements は、この関数が呼び出されたときに実行される文のシーケンスです。(parameterList)、directives;、および localDeclarations; は省略可能です。
関数のステートメント ブロックは、手続きに適用されるものと同じ規則によって管理されます。 ステートメント ブロック内では、関数の localDeclarations 部で宣言されている変数とその他の識別子、パラメータ リスト内のパラメータ名、および関数宣言のスコープ内にある任意の識別子を使用できます。 さらに、関数名は、その関数の戻り値を保持する特殊な変数としての役割を果たします。あらかじめ定義されている変数 Result も同じ役割をします。
拡張構文が有効になっている場合は({$X+})、Result は、すべての関数で暗黙のうちに宣言されています。この変数を再宣言してはいけません。
以下に例を示します。
function WF: Integer;
begin
WF := 17;
end;
WF という定数の関数を定義します。この関数は、パラメータを取らず、常に整数値 17 を返します。
この宣言は以下と等価です。
function WF: Integer;
begin
Result := 17;
end;
以下は、さらに複雑な関数宣言です。
function Max(A: array of Real; N: Integer): Real;
var
X: Real;
I: Integer;
begin
X := A[0];
for I := 1 to N - 1 do
if X < A[I] then X := A[I];
Max := X;
end;
宣言されている戻り値の型と一致する値であれば、1 つのステートメント ブロック内で、何度でも Result や関数名に値を代入できます。 関数の実行が終了すると、最後に Result または関数名に代入された値が、その関数の戻り値になります。 例:
function Power(X: Real; Y: Integer): Real;
var
I: Integer;
begin
Result := 1.0;
I := Y;
while I > 0 do
begin
if Odd(I) then Result := Result * X;
I := I div 2;
X := Sqr(X);
end;
end;
Result と関数名は、常に同じ値を表します。したがって、
function MyFunction: Integer;
begin
MyFunction := 5;
Result := Result * 2;
MyFunction := Result + 1;
end;
上の関数は、値 11 を返します。ただし、Result と関数名は、完全には交換可能ではありません。関数名が代入文の左辺にある場合、コンパイラはそれを(Result と同様に)戻り値を記録するために使われているものと見なします。関数名がステートメント ブロックのその他の場所にある場合、コンパイラはそれをその関数の再帰呼び出しと解釈します。一方、Result は、演算子、型キャスト、集合構成子、インデックス、その他のルーチンの呼び出しにおいて、変数として使用できます。
Result または関数名に値が代入されずにその関数が終了した場合、その関数の戻り値は未定義になります。
呼び出し規約
手続きまたは関数を宣言する際は、register、pascal、cdecl、stdcall、safecall、winapi のいずれかの指定を使って、呼び出し規約を指定できます。例えば、
function MyFunction(X, Y: Real): Real; cdecl;
呼び出し規約によって、パラメータがルーチンに渡される順序が決まります。また、呼び出し規約は、スタックからのパラメータの削除、レジスタを使用したパラメータの受け渡し、エラーや例外の処理にも影響を及ぼします。デフォルトの呼び出し規約は register です。
- register 規約と pascal 規約の場合、評価順序は定義されていません。
- cdecl、stdcall、safecall の各規約の場合は、右から左にパラメータが渡されます。
- cdecl を除くすべての規約の場合は、手続きまたは関数が、戻るときにスタックからパラメータを削除します。cdecl 規約の場合は、呼び出しが戻ったときに呼び出し側がスタックからパラメータを削除します。
- register 規約では最大 3 個の CPU レジスタを使用してパラメータが渡されるのに対して、その他の規約ではすべてのパラメータがスタックを介して渡されます。
- safecall 規約では、例外 "ファイアウォール" を実装しています。Win32 では、これにプロセス間の COM エラー通知が実装されています。
- winapi は、実際には呼び出し規約ではありません。winapi は、デフォルト プラットフォームの呼び出し規約を使用して定義します。たとえば、Win32 では winapi は stdcall と同等です。
呼び出し規約を以下の表にまとめます。
呼び出し規約 :
指令 | パラメータの順序 | クリーンアップの担当 | レジスタ経由のパラメータ渡し |
---|---|---|---|
register |
未定義 |
ルーチン |
○ |
pascal |
未定義 |
ルーチン |
× |
cdecl |
右から左 |
呼び出し側 |
× |
stdcall |
右から左 |
ルーチン |
× |
safecall |
右から左 |
ルーチン |
× |
デフォルトの register 規約は、通常、スタック フレームの作成を回避できるので、最も効率的です (発行済みのプロパティへのアクセス メソッドでは、register を使用しなければなりません)。cdecl 規約は、C または C++ で記述された共有ライブラリ内の関数を呼び出す場合に役立ちます。一方、外部コードを呼び出す場合は、一般に、stdcall または safecall をお勧めします。Win32 では、オペレーティング システム API は stdcall または safecall です。その他のオペレーティング システムでは、一般に cdecl を使用しています (stdcall は cdecl よりも効率的です)。
safecall 規約は、デュアルインターフェイスのメソッドを宣言する場合に使用する必要があります。pascal 規約は、下位互換性のために維持されています。
near、far、および export の各指令は、16 ビットの Windows プログラミングの呼び出し規約を表します。これらは、Win32 では無効で、下位互換性のためだけに維持されています。
前方宣言とインターフェイス宣言
forward 指令は、手続きまたは関数の宣言内にある、ローカル変数宣言や文を含むブロックに置き換わります。以下に例を示します。
function Calculate(X, Y: Integer): Real; forward;
Calculate という関数を宣言します。forward 宣言の後のどこかで、ブロックを含む定義宣言としてこのルーチンを再宣言しなければなりません。Calculate の定義宣言は、以下のようになります。
function Calculate;
... { declarations }
begin
... { statement block }
end;
通常、定義宣言では、ルーチンのパラメータ リストや戻り値型を繰り返す必要はありません。ただし、それらを繰り返した場合は、forward 宣言のものと完全に一致していなければなりません(ただし、デフォルトのパラメータは省略できます)。 forward 宣言で、オーバーロードされた手続きまたは関数を指定した場合は、定義宣言でもパラメータ リストを繰り返す必要があります。
forward 宣言とその定義宣言は、同じ type 宣言セクション内になければなりません。 つまり、前方宣言と定義宣言の間に、新しいセクション(var セクション、const セクションなど)を追加することはできません。 定義宣言は、external 宣言または assembler 宣言にすることもできます。ただし、別の forward 宣言にすることはできません。
forward 宣言の目的は、手続きや関数の識別子のスコープを、ソースコードの前方の位置まで拡大することです。これによって、ほかの手続きや関数は、forward 宣言されたルーチンを、それが実際に定義される前に呼び出すことができます。forward 宣言は、コードを柔軟に構成できるだけでなく、相互に再帰呼び出しをする場合にも必要になります。
forward 指令は、ユニットの interface セクションでは無効です。interface セクション内の手続きと関数のヘッダーは、forward 宣言と同様の動作をするので、implementation セクションで定義宣言をする必要があります。interface セクションで宣言されたルーチンは、そのユニットのどこからでも利用できます。また、インターフェイスが宣言されているユニットを使用するほかのユニットやプログラムからも利用できます。
外部宣言
external 宣言は、手続きまたは関数の宣言内のブロックに置き換わって、プログラムとは別にコンパイルされたルーチンを呼び出すことができるようにします。外部ルーチンは、オブジェクト ファイルや動的に読み込み可能なライブラリから得ることができます。
パラメータ数が可変の C の関数をインポートする場合は、varargs 指令を使用します。以下に例を示します。
function printf(Format: PChar): Integer; cdecl; varargs;
varargs 指令は、外部ルーチンに対してのみ有効です。また、cdecl 呼び出し規約に対してのみ有効です。
オブジェクト ファイルへのリンク
別にコンパイルされたオブジェクト ファイルのルーチンを呼び出すには、まず、$L(または $LINK)コンパイラ指令を使用して、そのオブジェクト ファイルをアプリケーションにリンクします。以下に例を示します。
{$L BLOCK.OBJ}
BLOCK.OBJ をプログラムまたはユニットにリンクします。次に、呼び出したい関数や手続きを次のように宣言します。
procedure MoveWord(var Source, Dest; Count: Integer); external;
procedure FillWord(var Dest; Data: Integer; Count: Integer); external;
これで、BLOCK.OBJ から MoveWord と FillWord を呼び出すことができます。
Win32 プラットフォームでは、上のような宣言は、アセンブリ言語で記述された外部ルーチンにアクセスするためによく使われます。アセンブリ言語のルーチンを Delphi のソース コードに直接記述することもできます。
ライブラリからの関数のインポート
動的読み込み可能ライブラリ(.DLL)からルーチンをインポートするには、次の形式の指令
external stringConstant;
を通常の手続きまたは関数のヘッダーの最後に付加します。stringConstant
は、一重引用符で囲んだライブラリ ファイルの名前です。32 ビット Windows の場合の例を次に示します。
function SomeFunction(S: string): string; external 'strlib.dll';
SomeFunction
という関数を strlib.dll
からインポートします。
内部および外部リンカの利用
Delphi では外部について 2 つの解釈があり、コンパイラが外部リンカを使用するかによって変わります:
1. Delphi によってサポートされるプラットフォームは、次の 2 つのグループに分かれます:
- コンパイラが自分の内部リンカを使用するもの。
- コンパイラが外部リンカを使用するもの
内部リンカを使用 |
WIN32、WIN64、OSX、IOS シミュレータ |
外部リンカ を使用 |
iOS デバイス、Android、Linux、macOS64 |
2. Delphi が内部リンカを使用するプラットフォームでは、external <stringconstant>
は、関数/プロシージャが DLL、dylib、共有オブジェクト内にあることを指示しています。
これらのプラットフォームでは、Delphi はシンボルが .dll/.dylib/.so
からインポートすることを理解しています。リンク時に検証は実行されません。 Delphi は、イメージをシンボル/ライブラリへの参照で生成します。 シンボルがそのライブラリに実際にない場合には、実行時に見つけます。
3. Delphi が外部リンカを使用するプラットフォーム、たとえば、 iOSDevice32 プラットフォームをターゲットとするプラットフォームなどでは、識別子は external <stringconstant>
で指定され、外部リンカに渡されます。
Delphi コンパイラは、<name>
を ld.exe に渡します。 もしライブラリを見つけられない場合、次のエラーを表示します: Error: E2597 ld: file not found: <name>
。
オブジェクト ファイルから関数をインポートする(外部リンカのみ)
外部リンカを使用する際、外部指令でオブジェクト ファイルを指定することにより、$L(または $LINK)コンパイラ指令の指定を省略することができます。 例:
procedure FunctionName; cdecl; external object 'ObjectFile.o' name '_FunctionName';
フレームワークから関数をインポートする
ルーチンを外部フレームワークからインポートすることができます。 例:
Function foo: Integer; external framework 'framework name>'
これは iOS32、iOS64、macOS64 でのみ適用可能です。
ルーチンを別の名前でインポートする
ライブラリ内の名前とは異なる名前で、ルーチンをインポートすることもできます。それには、次のように external 指令の中に元の名前を指定します。
external stringConstant1 name stringConstant2;
ここでの stringConstant1
はライブラリ ファイルの名前で、stringConstant2
はルーチンの元の名前です。
次の宣言は、user32.dll
user32.dll(Windows API に含まれる)から関数をインポートします。
function MessageBox(HWnd: Integer; Text, Caption: PChar; Flags: Integer): Integer; stdcall; external 'user32.dll' name 'MessageBoxA';
関数の元の名前は MessageBoxA
ですが、これが MessageBox
としてインポートされます。
インポート宣言では、ルーチン名のスペルと大文字小文字の区別を完全に一致させてください。 インポートしたルーチンを後で呼び出すときは、名前の大文字小文字は区別されません。
ルーチンをインデックスでインポートする
名前の代わりに、インポートしたいルーチンを識別する番号を使うこともできます。
external stringConstant index integerConstant;
ここでの integerConstant
は、エクスポート テーブルでのルーチンのインデックスです。
ライブラリの遅延読み込み
実際に関数が必要になるときまで、その関数を含むライブラリの読み込みを先送りするには、次のようにインポートする関数に delayed
指令を追加します。
function ExternalMethod(const SomeString: PChar): Integer; stdcall; external 'cstyle.dll' delayed;
delayed
を使用することで、アプリケーション起動時にはインポートする関数を含むライブラリは読み込まれませんが、その関数を最初に呼び出すときには読み込みが完了するようにできます。 このトピックの詳細は、「ライブラリとパッケージ - 遅延読み込み」を参照してください。
ライブラリの依存関係を指定する
対象となるルーチンが入っているライブラリが、他のライブラリに依存している場合、それらの依存関係を指定するには、dependency
指令を使用します。
dependency
指令を使用するには、dependency
キーワードに続き、カンマ区切りの文字列のリストを追加します。各文字列には、対象となる外部ライブラリと依存関係にあるライブラリの名前が入ります:
external <library> dependency <dependency1>, <dependency2>, …
次の宣言は、libmidas.a
が標準 C++ ライブラリに依存していることを示しています。
function DllGetDataSnapClassObject(const [REF] CLSID, [REF] IID: TGUID; var Obj): HResult; cdecl; external 'libmidas.a' dependency 'stdc++';
手続きと関数のオーバーロード
同じスコープ内で同じ名前のルーチンを複数宣言することができます。これは、オーバーロードと呼ばれます。オーバーロードされるルーチンは、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;
これらの宣言では、型の異なるパラメータを取る 2 つの関数が、どちらも Divide という名前で作成されます。Divide が呼び出されると、コンパイラは、その呼び出しで渡された実際のパラメータを調べることにより、どちらの関数を呼び出すかを決定します。たとえば、Divide(6.0, 3.0) では、引数が実数値なので、1 番目の Divide 関数が呼び出されます。
ルーチンの宣言とは型が一致しないものの、1 つ以上の宣言のパラメータと代入互換性があるパラメータであれば、オーバーロードされたルーチンに渡すことができます。 このようなことは、1 つのルーチンがさまざまな整数型や実数型でオーバーロードされている場合に最もよく起こります。次に例を示します。
procedure Store(X: Longint); overload;
procedure Store(X: Shortint); overload;
このような場合、あいまいさがなければ、コンパイラは、呼び出しで渡された実際のパラメータに適応する最小範囲の型のパラメータを持つルーチンを呼び出します (実数値を持つ定数式は常に Extended 型になることを覚えておいてください)。
オーバーロードされたルーチンは、受け取るパラメータの数またはそれらのパラメータの型で区別できなければなりません。したがって、次のような宣言の組み合わせはコンパイル エラーになります。
function Cap(S: string): string; overload;
...
procedure Cap(var Str: string); overload;
...
しかし、次のような宣言は
function Func(X: Real; Y: Integer): Real; overload;
...
function Func(X: Integer; Y: Real): Real; overload;
...
有効です。
オーバーロードされたルーチンが 前方宣言またはインターフェイス宣言で宣言されている場合は、定義宣言でもそのルーチンのパラメータ リストを繰り返す必要があります。
コンパイラでは、AnsiString/PAnsiChar、UnicodeString/PChar、WideString/PWideChar の型のパラメータが同じパラメータ位置に含まれているオーバーロード関数を区別することができます。このようなオーバーロード状況で渡された文字列定数や文字列リテラルは、ネイティブの文字列型または文字型(UnicodeString/PChar)に変換されます。
procedure test(const A: AnsiString); overload;
procedure test(const W: WideString); overload;
procedure test(const U: UnicodeString); overload;
procedure test(const PW: PWideChar); overload;
var
a: AnsiString;
b: WideString;
c: UnicodeString;
d: PWideChar;
e: string;
begin
a := 'a';
b := 'b';
c := 'c';
d := 'd';
e := 'e';
test(a); // calls AnsiString version
test(b); // calls WideString version
test(c); // calls UnicodeString version
test(d); // calls PWideChar version
test(e); // calls UnicodeString version
test('abc'); // calls UnicodeString version
test(AnsiString ('abc')); // calls AnsiString version
test(WideString('abc')); // calls WideString version
test(PWideChar('PWideChar')); // calls PWideChar version
end;
オーバーロード関数の宣言で、バリアントをパラメータとして使用することもできます。バリアントは、どのような単純型よりも一般的であると見なされます。バリアント的な一致よりも型の完全な一致が常に優先されます。このようなオーバーロード状況でバリアントが渡されて、バリアントをそのパラメータ位置で受け取るオーバーロードが存在する場合は、バリアント型と完全に一致していると見なされます。
これが原因で、浮動小数型に関して、ちょっとした副作用が生じることがあります。浮動小数型は、サイズによって一致するかどうかが判断されます。オーバーロード呼び出しに渡された浮動小数変数と完全に一致するものはないが、バリアント パラメータがある場合は、そのバリアントが、よりサイズの小さい浮動小数型よりも優先されます。
以下に例を示します。
procedure foo(i: integer); overload;
procedure foo(d: double); overload;
procedure foo(v: variant); overload;
var
v: variant;
begin
foo(1); // integer version
foo(v); // variant version
foo(1.2); // variant version (float literals -> extended precision)
end;
この例では、double 版ではなく、variant 版の foo が呼び出されます。これは、定数 1.2 が暗黙のうちに extended 型と見なされ、extended は double と完全には一致しないからです。extended も variant と完全には一致しませんが、variant の方が一般的な型と見なされます(これに対して、double の方が extended より小さい型になります)。
foo(Double(1.2));
この型キャストは無効です。代わりに、型付き定数を使用する必要があります。
const d: double = 1.2;
begin
foo(d);
end;
上記のコードは正しく動作し、double 版が呼び出されます。
const s: single = 1.2;
begin
foo(s);
end;
上記のコードでも double 版の foo が呼び出されます。single の方が、variant よりも double に近いと見なされます。
一連のオーバーロード ルーチンを宣言する場合、浮動小数点型からバリアント型への昇格を避けるには、variant 版と一緒に、各浮動小数点型(single、double、extended)版のオーバロード関数を宣言するのが最良です。
オーバーロード ルーチンでデフォルト パラメータを使用する場合は、あいまいさのあるパラメータ シグネチャが入らないように注意してください。
ルーチンを呼び出すときに、ルーチン名を修飾することにより、オーバーロードの潜在的な副作用を抑えることができます。 たとえば、Unit1.MyProcedure(X, Y) と記述すると、Unit1 内で宣言されているルーチンだけを呼び出すことができます。この呼び出しの名前およびパラメータ リストに一致するルーチンが Unit1 にない場合は、エラーになります。
ローカル宣言
関数または手続きの本体は、通常、そのルーチンのステートメント ブロック内で使われるローカル変数の宣言で始まります。 これらの宣言に、定数、型、およびその他のルーチンを含めることもできます。 ローカルな識別子のスコープは、それが宣言されているルーチン内に限定されます。
ネスト ルーチン
関数や手続きのブロックのローカル宣言セクション内に、別の関数や手続きが含まれる場合もあります。たとえば、次の DoSomething という手続きの宣言には、ネストした手続きが含まれます。
procedure DoSomething(S: string);
var
X, Y: Integer;
procedure NestedProc(S: string);
begin
...
end;
begin
...
NestedProc(S);
...
end;
ネスト ルーチンのスコープは、それが宣言されている手続きまたは関数内に限定されます。この例では、NestedProc は DoSomething 内でのみ呼び出すことができます。
ネスト ルーチンの実際の例については、SysUtils ユニット内の DateTimeToString 手続き、ScanDate 関数、およびその他のルーチンを参照してください。