試験運用中なLinux備忘録・旧記事

はてなダイアリーで公開していた2007年5月-2015年3月の記事を保存しています。

ValaやC#の「ref」や「out」について

Vala言語と(影響を受けている)C#言語では、関数へ引数を渡すときに「ref」や「out」といったキーワードが手前に渡されることがある。

ref/outの効果

「ref」や「out」は関数を呼ぶ側で定義した変数を引数にして渡したときに、その内容を関数の中で操作したいときに指定する。これを指定しないと、引数として受け取った変数の値を関数の中で変更しても、その値はその関数の中だけで有効なもので、関数を呼び出す側の変数の値は変更されない。

「ref」と「out」の違い

言語仕様的な違い

「out」では関数を呼び出す側で用意する変数を宣言したときに値が未定義でも使用できるのに対して、「ref」では値が未定義の変数を渡せない。

用途上の違い

「ref」は既に値が入っていることを前提としてその値を関数の中で利用するというときに使える。
「out」については、関数の中で何かのエラーが起こったときに呼び出し側から渡された変数へ内容を書き込むというような形(関数を呼ぶ側で変数の値を初期化(代入)しても意味がない場合)で使える。

(関数側)
void func (out int error)
{
  ...
  if ( ... )
  {
    error = [エラー番号1];
  }
  ...
  if ( ... )
  {
    error = [エラー番号2];
  }
  ...
}

(呼び出す側)
  int error;
  func (out error);  // 値はこの中で書き込まれる・宣言時に値を代入しても意味がない
  print ("error=%d", error);

Vala言語の例

[任意]ファイル名: refouttest.vala

using GLib;

/*
 * 「ref」で渡す引数に値が代入されていない場合(コンパイルに失敗する):
 * valac -o refouttest refouttest.vala
 * 「ref」で渡す引数に値が代入されている場合:
 * valac -D B_IS_ASSIGNED -o refouttest refouttest.vala
 */

namespace RefOutTest
{
  class MainClass
  {
    static void func (int a, ref int b, out int c)
    {
      a = 1;  // 値が渡されているので呼び出した側の変数はそのまま
      b = 2;  // refは代入済みの変数への参照を受け取り、呼び出し側でも変更される
      c = 3;  // outは代入されていない状態でも受け取れる
      print ("func(): a=%d b=%d c=%d\n", a, b, c);
    }
    public static int main (string[] args)
    {
      int a = 0;
#if B_IS_ASSIGNED
      int b = 0;
#else
      int b;
#endif
      int c;
      /*
       * (ref付きで渡す)bが代入されていないと
       * 「use of possibly unassigned local variable `b'」となる
       */
      func (a, ref b, out c);
      print ("main(): a=%d b=%d c=%d\n", a, b, c);  // bとcだけ変更されている
      return 0;
    }
  }
}

「ref」では未定義の変数を渡せないということを示すため、-D B_IS_ASSIGNEDオプションを付けたときにのみbに初期値を設定するようにしている(付けないとコンパイルに失敗する)。
下は実行結果。

func(): a=1 b=2 c=3
main(): a=0 b=2 c=3

C#言語の例

[任意]ファイル名: RefOutTest.cs

using System;

/*
 * 「ref」で渡す引数に値が代入されていない場合(コンパイルに失敗する):
 * gmcs -out:RefOutTest.exe RefOutTest.cs
 * 「ref」で渡す引数に値が代入されている場合:
 * gmcs -d:B_IS_ASSIGNED -out:RefOutTest.exe RefOutTest.cs
 */

namespace RefOutTest
{
  class MainClass
  {
    static void Func (int a, ref int b, out int c)
    {
      a = 1;  // 値が渡されているので呼び出した側の変数はそのまま
      b = 2;  // refは代入済みの変数への参照を受け取り、呼び出し側でも変更される
      c = 3;  // outは代入されていない状態でも受け取れる
      Console.WriteLine ("Func(): a={0} b={1} c={2}", a, b, c);
    }
    public static int Main (string[] args)
    {
      int a = 0;
#if B_IS_ASSIGNED
      int b = 0;
#else
      int b;
#endif
      int c;
      /*
       * (ref付きで渡す)bが代入されていないと
       * CS0165「Use of unassigned local variable `b'」となる
       */
      Func (a, ref b, out c);
      Console.WriteLine ("Main(): a={0} b={1} c={2}", a, b, c);  // bとcだけ変更されている
      return 0;
    }
  }
}

こちらも「ref」では未定義の変数を渡せないということを示すため、-d:B_IS_ASSIGNEDオプションを付けたときにのみbに初期値を設定するようにしている(付けないとコンパイルに失敗する)。
下は実行結果。

Func(): a=1 b=2 c=3
Main(): a=0 b=2 c=3

関連: C言語の場合

C言語では関数を呼ぶときに変数のアドレス*1を渡して、関数側からは受け取ったポインタ変数*2が参照する変数の値を操作することになる。

  • 引数を「&[変数名]」として渡す
  • 関数内で「*[ポインタ変数名]」として参照値を読み書きする

値が未定義な変数のアドレスを渡すことは(ValaやC#の「out」指定と同様)可能。

#include <stdlib.h>
#include <stdio.h>

void
func (int a, int *b, int *c)  // bとcはポインタ変数として受け取っている
{
  a = 1;
  /* bとcには変数のアドレスが入っていて「*b」「*c」でその変数の値が操作できる */
  *b = 2;
  *c = 3;
  printf ("func(): a=%d b=%d c=%d\n", a, *b, *c);
}

int
main ()
{
  int a = 0;
  int b = 0;  // 値を代入済み
  int c;      // 値が未定義(アドレスを渡すことは可能)
  func (a, &b, &c);  // bとcはアドレス渡し
  printf ("main(): a=%d b=%d c=%d\n", a, b, c);
  return EXIT_SUCCESS;
}

下は実行結果。

func(): a=1 b=2 c=3
main(): a=0 b=2 c=3

なお、C#言語やVala言語でもポインタを用いる方法は使用可能だが、一般的には「ref」や「out」を使用するようだ。

関連記事:

使用したバージョン:

  • Vala 0.7.3
  • Mono 2.2

*1:メモリ上のどこに変数が確保されているかの情報

*2:アドレスを格納する変数で、int型データへのポインタは「int *」型のように「*」の付いた型の名前を使用する