鳥の巣箱

ネトゲしたり、機械いじったり、ソフト書いたり、山篭ったり、ギャンブルしたりする人

手続き、関数のパラメータについて

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」パラメータの違いは何ですか? にある回答から引用