目次

キーワード

概要

プログラミング言語での値の受け渡しの方法には 値渡し(pass by value)と参照渡し(pass by reference)という2つの方法があります。

C# では、値の受け渡しは基本的に値渡しになります。 しかし、refout といったキーワードを使うことで参照渡しにすることが出来ます。

ポイント
  • 値渡し: メソッド内で引数の値を書きかえても、呼び出し元には影響しない。

  • 参照渡し(ref): メソッド内での値の書き換えの影響が呼び出し元に伝搬する。

  • out: 特殊な参照渡し。戻り値以外にも値を返したいとき(複数の値を返したいとか)に使う。

値の受け渡し

値の受け渡しが発生する場所は何カ所かあります。例えば以下のような場所です。

  • 変数から変数
  • 変数から引数
  • 戻り値から変数
var x = 1;
var y = x; // x から y に値を渡す
static void VariableToParameter()
{
    var x = 1;
    F(x); // 変数 x から、F の引数 x に値を渡す
}

static void F(int x)
{
}
static void ReturnToVariable()
{
    var x = F(); // F の戻り値から変数 x に値を渡す
}

static int F() => 1;

受け渡しの方法には、以降で説明する値渡し参照渡しという2種類の受け渡し方法があります。

C#では、通常(特に何もつけないと)、値渡しになります。 一方、以下のようにして、参照渡しを使うこともできます。

  • C# 6以前では、引数の受け渡しの際にrefもしくはoutという修飾子を付けることで参照渡しができます
  • C# 7以降では、変数間の受け渡しや戻り値でもref修飾子を付けることで参照渡しができます

ちなみに、C#には受け渡しの値渡しと参照渡しの他に、型の区分として値型と参照型というものもあります。結果的に、「値型の値渡し」、「値型の参照渡し」、「参照型の値渡し」、「参照型の参照渡し」というような組み合わせもできるので注意が必要です。

値渡し

しばらく、C# 6以前でも使える「引数の受け渡し」で説明して行きましょう。

引数の値渡し(call by value)とは、メソッドを呼び出す際に値のコピーを渡すことを言います。 C# では普通にメソッドを定義すると、その引数は値渡しになります。 例えば、以下のようなプログラムがあったとします。

using System;
class ByValueTest
{
  static void Main()
  {
    int a = 100;
    Console.Write("{0} → ", a);
    Test(a);
    Console.Write("{0}\n", a);
  }

  static void Test(int a)
  {
    a = 10; // メソッド内で値を書き換える。
  }
}

Test メソッドの変数 a には Main メソッドの a のコピーが渡されています。 したがって、図1のように、 Test 内で変数 a を書き換えても Main 内の a の値は変わりません。 そのため、このプログラムの実行結果は以下のようになります。

100 → 100

値型の値渡し
値型の値渡し

同様に、参照型の変数を値渡しする場合、図2, 3に示すように、参照情報をコピーして渡すことになります。

参照型の値渡し(参照情報の書き換え)
参照型の値渡し(参照情報の書き換え)

参照型の値渡し(参照先の書き換え)
参照型の値渡し(参照先の書き換え)

参照渡し

引数の参照渡し(call by reference)とは、メソッドを呼び出す際に値の参照情報を渡すことを言います。 C# では、以下の例のように、メソッドの引数に ref キーワードを付けることでその変数は参照渡しになります。

using System;
class ByReferenceTest
{
  static void Main()
  {
    int a = 100;
    Console.Write("{0} → ", a);
    Test(ref a);
    Console.Write("{0}\n", a);
  }

  static void Test(ref int a)
  {
    a = 10; // メソッド内で値を書き換える。
  }
}

Test メソッドの変数 aMain メソッドの a に対する参照になっています。 したがって、図4のように、 Test 内で変数 a を書き換えた場合、 Main 内の a の値も同時に書き換わります。 そのため、このプログラムの実行結果は以下のようになります。

100 → 10

値型の参照渡し
値型の参照渡し

同様に、参照型の変数を値渡しする場合、図5に示すように、参照情報をさらに参照することになります。

参照型の参照渡し
参照型の参照渡し

ここで1つ注意しなければいけないのは、 メソッドの呼び出し側にも ref キーワードをつける必要があるということです。 参照渡しを行うと、メソッドの中で値が書き換えられる可能性があります。 (というよりも、書き換える必要があるから参照渡しにする。) 引数が参照渡しであることを知らずにメソッドを呼び出してしまうと、 プログラマの意図しないところで値が書き換わってしまう可能性があり、 これはバグの原因になります。 そのため、呼び出し側でも明示的に ref キーワードを付けなければならいないという制約をつけることによって、 知らないうちに参照渡しのメソッドを呼び出してしまう危険性をなくしています。

サンプル
using System;

class ByRefferanceTest
{
  static void Main()
  {
    int[] array = new int[]{4, 6, 1, 8, 2, 9, 3, 5, 7};
    BubbleSort(array);
    foreach(int a in array)
    {
      Console.Write("{0,3}", a);
    }
  }

  /// <summary>
  /// バブルソートを使って配列を整列する
  /// </summary>
  static void BubbleSort(int[] array)
  {
    for(int i=0; i<array.Length-1; ++i)
      for(int j=array.Length-1; j>i; --j)
        if(array[j-1] > array[j])
          Swap(ref array[j-1], ref array[j]);
  }

  /// <summary>
  /// a と b の値を入れ替える
  /// </summary>
  static void Swap(ref int a, ref int b)
  {
    int tmp = a;
    a = b;
    b = tmp;
  }
}
  1  2  3  4  5  6  7  8  9

出力引数

参照渡しを使うと、メソッド内からメソッド外にある変数を書き換えることができます。 これを、メソッドの戻り値代わりに使うこともできます。 特に、複数の戻り値を返す場合に有効な手段です。 ただ、ref修飾子を使った参照引数では、戻り値として使うには以下のようないくつかの問題があります。

using System;

class Program
{
    static void Main()
    {
        int a = 0; // この 0 という値には意味はないけど、必須
        int b = 0; // 同上
        MultipleReturns(ref a, ref b); // a, b を
        Console.Write("{0}\n", a);
    }

    static void MultipleReturns(ref int a, ref int b)
    {
        a = 10; // a を初期化
        // 本当は b も初期化してやらないといけないけど、忘れててバグってる
    }
}

(C# 6以前では、複数の戻り値を返す唯一の手段でした。C# 7以降ではタプル型というものを使って複数の戻り値を返すことができるようになっています。)

問題を要約すると以下の2点です

  • 呼び出し元で、特に意味のない値で変数を初期化しておかなければならない
    • メソッドの中で必ず上書きする想定なので、無駄な初期化になる
  • メソッドの中で代入を忘れてしまってもコンパイル エラーにならない

そこで、戻り値として使いたい場合(メソッド内で変数を初期化する予定である場合)、 以下のように out 修飾子を用いて、出力用の参照引数であることを明示してやります。

using System;
class ByValueTest
{
  static void Main()
  {
    int a;
    Test(out a); // out を使った場合、変数を初期化しなくてもいい
    Console.Write("{0}\n", a);
  }

  static void Test(out int a)
  {
    a = 10; // out を使った場合、メソッド内で必ず値を代入しなければならない
  }
}
10

out キーワードを用いて宣言された引数は参照渡しになります。 ref キーワードとの違いは、上述のとおり、

  • メソッド呼び出し前に初期化する必要がなくなる
  • メソッド内で必ず値を割り当てなければいけない

の2点です。

サンプル

メソッドで複数の値を返したい場合、 戻り値では1つしか値を返せないので出力変数を使います。

using System;

class OutTest
{
  /// <summary>
  /// コンソールから係数を入力して2次方程式の根を計算し、出力する。
  /// </summary>
  static void Main()
  {
    string line = Console.ReadLine();
    string[] token = line.Split(' ');
    double a = double.Parse(token[0]);
    double b = double.Parse(token[1]);
    double c = double.Parse(token[2]);
    Console.Write("{0}x^2 + {1}x + {2} = 0\n", a, b, c);

    double x, y;
    int type;
    CalcRoot(a, b, c, out type, out x, out y);
    if(type == 0)      Console.Write("x = {0}, {1}\n", x, y);
    else if(type == 1) Console.Write("x = {0} ±i {1}\n", x, y);
    else               Console.Write("x = {0}\n", x);
  }

  /// <summary>
  /// 2次方程式 ax^2 + bx + c = 0 の根を求める
  /// </summary>
  /// <param name="a">2次の係数</param>
  /// <param name="b">1次の係数</param>
  /// <param name="c">定数項</param>
  /// <param name="type">根のタイプ。0:実数根2つ、-1:重根1つ、1:虚数根</param>
  /// <param name="x">根1(虚数根の場合、根の実部)</param>
  /// <param name="y">根2(虚数根の場合、根の虚部)</param>
  static void CalcRoot(
    double a, double b, double c,
    out int type, out double x, out double y)
  {
    b /= 2;
    double d = b * b - a * c;

    if(d < 0)
    {
      type = 1;
      x = -b / a;
      y = Math.Sqrt(-d) / a;
      return;
    }
    
    if(d > 0)
    {
      type = 0;
      double t1 = -b;
      double t2 = Math.Sqrt(d);
      x = (t1 + t2) / a;
      y = (t1 - t2) / a;
      return;
    }

    type = -1;
    x = -b / a;
    y = x;
  }
}

出力変数宣言

Ver. 7

C# 7で、出力引数を受け取るのと同時に式中で変数を宣言できるようになりました。 これを出力変数宣言(out variable declaration。あるいは、略して out-var)と呼びます。

以前は、出力引数で値を受け取るためには、メソッドなどの呼び出しよりも前に変数を宣言しておく必要がありました。 例えば以下のようになります。

static int? ParseOrDefault(string s)
{
    int x;
    return int.TryParse(s, out x) ? x : default(int?);
}

これに対して、C# 7では、以下のような書き方ができるようになります。 式の中で変数 x を宣言しつつ、出力引数の値を受け取っています。

static int? ParseOrDefault(string s)
{
    return int.TryParse(s, out int x) ? x : default(int?);
}

ちなみに、varを使った型推論もできます。

static int? ParseOrDefault(string s)
{
    return int.TryParse(s, out var x) ? x : default(int?);
}

この例では、C# 6以前の書き方では、変数宣言ステートメントが必須で、式1つにまとめることができませんでした。 一方、C# 7以降の書き方ならば1つの式で済んでいます。 C# 6で導入された => を使った形式でメソッドを書くことができます。

static int? ParseOrDefault(string s) => int.TryParse(s, out var x) ? x : default(int?);

出力変数宣言で作った変数のスコープは、概ね、その式を囲っているブロック内になります。 つまり、式の直前に変数を宣言したのと同じスコープになります。

using System;

struct Point
{
    public int X { get; set; }
    public int Y { get; set; }

    public void GetCoordinate(out int x, out int y)
    {
        x = X;
        y = Y;
    }
}

class Program
{
    static void Main()
    {
        // x, y のスコープはこのブロック内
        // この辺りで x, y という名前の変数は作れない

        var p = new Point { X = 1, Y = 2 };
        p.GetCoordinate(out var x, out var y);

        // 以下のような書き方をしたのと同じ
        // int x, y;
        // p.GetCoordinate(out x, out y);

        // この行から下で x, y を使える

        Console.WriteLine($"{x}, {y}");
    }
}

正確にいうともう少し複雑なルールになっていますが、詳細については「式の中で変数宣言」を参照してください。

更新履歴

ブログ