手続き、関数のパラメータについて
Delphiではprocedure(手続き)とfunction(関数)があります。
これらルーチンにはパラメータをもたせることができます。
例えば
function plus_one(val:Integer):Integer; begin Result := val +1; end;
こんな関数ですが、ここでのパラメータはInteger型のvalです。
valに与えられた数値に1を足して返す関数ですね。
Delphiではパラメータに特定の予約語を付けることで、その振る舞いを変えることができます。
値パラメータ
Delphiではデフォルトでこの種類のパラメータになります。
program Project1; {$APPTYPE CONSOLE} {$R *.res} uses System.SysUtils; procedure plus_one(val : Integer); begin val := val + 1; Writeln('in procedure : ', val); end; var x : Integer; begin try x := 10; Writeln('main : ', x); plus_one(x); Writeln('main : ', x); while True do except on E: Exception do Writeln(E.ClassName, ': ', E.Message); end; end.
main : 10 in procedure : 11 main : 10
値パラメータでは、ルーチン内でパラメータに対して操作をしても、呼び出し元には影響しません。
ルーチンに入るとき、変数のコピーが作成されて渡されるからです。
変数パラメータ
パラメータに予約語varを付けることで変数パラメータにできます。
procedure plus_one(var val : Integer); begin val := val + 1; Writeln('in procedure : ', val); end;
先程のplus_oneプロシージャのパラメータにvarをつけました。
このときの実行結果は
main : 10 in procedure : 11 main : 11
となり、ルーチン内の操作が呼び出し元にも反映されています。
変数パラメータはポインタを渡すようなイメージです。
定数パラメータ
パラメータに予約語constを付けることで定数パラメータにできます。
定数パラメータは文字通り、パラメータを定数として扱うのでルーチン内での代入等操作ができません。
procedure plus_one(const val : Integer); begin val := val + 1; Writeln('in procedure : ', val); end;
先程のソースで単純にconstを指定するとエラーが発生します。
[dcc32 エラー] Project1.dpr(33): E2064 代入できない左辺値です
これは、定数に対して代入しようとしているからですね。
constを使って先ほどと同様の結果を得ようとする場合は
procedure plus_one(const val : Integer); begin Writeln('in procedure : ', val+1); end;
このように修正することで、値渡しのときと同様の動作をします。
main : 10 in procedure : 11 main : 10
定数パラメータの[Ref]属性
定数パラメータは、値渡しになるのか、参照渡しになるのかが固定されません。コンパイラによって適宜変わります。
ですが[Ref]を付けることで強制的に参照渡しにすることができます。
procedure plus_one(const [Ref] val : Integer); begin Writeln('in procedure : ', val+1); end;
このままでは、参照渡しにした効力を実感できないので少し手を加えます。
procedure plus_one(const [Ref] val : Integer); var cash : PInteger; begin cash := @val; cash^ := cash^ +1; Writeln('in procedure : ', val); end;
ルーチン内でパラメータのポインタを一度ローカル変数に代入し、それをキャストして計算結果を代入しています。
これを使うとどうなるかと言うと。。。
main : 10 in procedure : 11 main : 11
変数パラメータと同じ動作をします。
強制参照渡しにすることで、呼び出し元の変数のポインタを受け取れるのでこのようなことができるんですね。
いや、そんな面倒なことするくらいなら最初からvarで変数パラメータにすればいいじゃん。
と、言いたくなる気持ちはわかります。
今の例だと全くその通りなんですが、ある場合だと面白いことができるようになります。
[Ref]属性定数パラメータの応用
[Ref]属性の威力を見るために、以下のプログラムを用意しました。
program Project1; {$APPTYPE CONSOLE} {$R *.res} uses System.SysUtils; type TTestClass = class(TObject) private FValA : Integer; FValB : Integer; function GetVal:Integer; public constructor Create; property ValVar : Integer read FValA; property ValMethod : Integer read GetVal; end; constructor TTestClass.Create; begin FValA := 10; FValB := 10; end; function TTestClass.GetVal: Integer; begin Result := FValB; end; procedure plus_one(const [Ref] val : Integer); var cash : PInteger; begin cash := @val; cash^ := cash^ +1; Writeln('in procedure : ', val); end; var x : TTestClass; begin try x := TTestClass.Create; Writeln('====TTestClass.ValMethod===='); Writeln('main : ',x.ValMethod); plus_one(x.ValMethod); Writeln('main : ', x.ValMethod); Writeln('====TTestClass.ValVar===='); Writeln('main : ',x.ValVar); plus_one(x.ValVar); Writeln('main : ', x.ValVar); while True do except on E: Exception do Writeln(E.ClassName, ': ', E.Message); end; end.
TTestClassには読み取り専用プロパティのValVarとValMethodがあります。
ValVarはフィールド、ValMethodはメソッドにそれぞれアクセスします。
この場合の出力結果を見てみましょう。
====TTestClass.ValMethod==== main : 10 in procedure : 11 main : 10 ====TTestClass.ValVar==== main : 10 in procedure : 11 main : 11
なんと、ValMethodの方は値が変更されていませんが、ValVarの方は変更されいます。
つまり、const [Ref]を応用すると、プロパティのアクセスがフィールドである場合、読み取り専用であるにも関わらず値を書き換えられるのです。
いや、これ別にvarでもできるんじゃないの?と思いきや
変数パラメータとしてプロパティを渡そうとすると
[dcc32 エラー] Project1.dpr(49): E2197 変数パラメータに定数オブジェクトを渡すことはできません
このようにエラーが発生するので無理なんですね。
これが[Ref]属性の面白さです。
outパラメータ
予約語outを付けることでoutパラメータになります。
基本的にはvarパラメータと同様、参照渡しのような動作をします。
ですが、outパラメータは型によって動作が変わります。
例えば次の例を見てみましょう。
program Project1; {$APPTYPE CONSOLE} {$R *.res} uses System.SysUtils; procedure plus_one(out val : Integer); begin val := val +1; Writeln('in procedure : ', val); end; procedure add_str(out str : string); begin str := str + 'add string'; Writeln('in procedure : ', str); end; var x : Integer; s : string; begin try x := 10; s := 'Test'; Writeln('====Integer===='); Writeln('main : ',x); plus_one(x); Writeln('main : ', x); Writeln('====String===='); Writeln('main : ',s); add_str(s); Writeln('main : ', s); while True do except on E: Exception do Writeln(E.ClassName, ': ', E.Message); end; end.
====Integer==== main : 10 in procedure : 11 main : 11 ====String==== main : Test in procedure : add string main : add string
Integer型を使ったoutパラメータは、varと同じ動作をします。
ですが、string型を使った場合は今までとは違う動作をしています。
outパラメータは一部の型を使う場合、ルーチンに入るときに変数を初期化します。
string型の場合、初期化されるので最初に代入した「Test」が消されて、ルーチン内での「add string」だけになっています。参照渡しであることは変わらないので、呼び出しもとでも変数の中身が「add string」になっています。
パラメータが初期化されるかどうかは、型がマネージド型か、アンマネージド型かで決まります。*1
アンマネージド型の場合、varと同じ動作をします。
アンマネージド型の例はInteger、Floatなどの整数型、実数型やbool、列挙型などが該当します。
Integerはアンマネージドなので、上の例ではvarと同じ動作をしています。
レコード型でoutパラメータを指定する場合、フィールドの型によって動作が変わります。
program Project1; {$APPTYPE CONSOLE} {$R *.res} uses System.SysUtils; type TTestRecord = record val : Integer; str : string; end; procedure plus_one(out val : Integer); begin val := val +1; Writeln('in procedure : ', val); end; procedure add_str(out str : string); begin str := str + 'add string'; Writeln('in procedure : ', str); end; procedure add_record(out rec : TTestRecord); begin rec.val := rec.val + 1; rec.str := rec.str + 'add string'; Writeln('in procedure : ', rec.val, ',', rec.str); end; var x : TTestRecord; begin try x.val := 10; x.str := 'Test'; Writeln('====Integer===='); Writeln('main : ',x.val); plus_one(x.val); Writeln('main : ', x.val); Writeln('====String===='); Writeln('main : ',x.str); add_str(x.str); Writeln('main : ', x.str); x.val := 10; x.str := 'Test'; Writeln('====Record===='); Writeln('main : ',x.val,',',x.str); add_record(x); Writeln('main : ', x.val,',',x.str); while True do except on E: Exception do Writeln(E.ClassName, ': ', E.Message); end; end.
TTestRecordにはInteger型とstring型のフィールドがあります。
これを、それぞれoutのフィールドをパラメータのルーチンに渡した場合と、レコード型そのものを渡した場合でみてみます。
====Integer==== main : 10 in procedure : 11 main : 11 ====String==== main : Test in procedure : add string main : add string ====Record==== main : 10,Test in procedure : 11,add string main : 11,add string
はい。
見ての通りで、フィールドの型がマネージド型かアンマネージド型かで変わっています。
フィールドごとに渡そうが、レコードそのものを渡そうがその挙動に変わりはないってことです。
参考文献
*1:公式Wikiには明記されていません。
delphi — 「var」パラメータと「out」パラメータの違いは何ですか?
にある回答から引用
ディスカッション
コメント一覧
まだ、コメントがありません