参照戻り値と参照ローカル変数

Ver. 7

C# 7から、戻り値とローカル変数でも参照渡しを使えるようになりました。 書き方はほぼ参照引数と同じです。 戻り値の型の前、値を渡す側、値を受ける側それぞれにref修飾子を付けます。

例として、配列のi番目の要素を参照で返してみましょう。以下のようになります。

using System;

class Program
{
    static void Main()
    {
        var x = new[] { -1, -1, -1, -1, -1 };

        for (int i = 0; i < x.Length; i++)
        {
            // 戻り値を書き換えてる
            // 実際書き換わってるのは参照先の配列 x
            Ref(x, i) = i;
        }

        // ↑のループで書き換えたので、結果は 0, 1, 2, 3, 4
        Console.WriteLine(string.Join(", ", x));
    }

    // 配列の i 番目の要素を参照
    static ref int Ref(int[] array, int i) => ref array[i];
}
0, 1, 2, 3, 4

また、ローカル変数に対しても、ref修飾子を付けることで参照渡しができます。

using System;

class Program
{
    static void Main()
    {
        var a = 10;

         ref var b = ref a; // 参照ローカル変数。宣言側にも、値を渡す側にも ref

        var c = b;         // これは普通に値渡し(コピー)。この時点の a の値 = 10 が入る
        ref var d = ref b; // さらに参照渡しで、結局 a を参照

        d = 1; // d = b = a を書き換え

        ref var e = ref Ref(ref c); // 参照戻り値越しに、c を参照
        var f = Ref(ref c);         // これは結局、値渡し(コピー)

        ++e;   // e = c を +1。元が10なので、11に
        f = 0; // f は普通に値渡しで作った新しい変数なので他に影響なし

        // 結果は 1, 1, 11, 1, 11, 0
        // a, b, d が同じ場所を参照してて 1
        // 同上、c, e が 11
        // f が 0
        Console.WriteLine(string.Join(", ", a, b, c, d, e, f));
    }

    // 引数を素通し
    static ref int Ref(ref int x) => ref x;
}
1, 1, 11, 1, 11, 0

refだらけになってしまいますが、渡す側、受け取る側の両側にref修飾子が必要なのは参照引数と同様です。 元の変数がどこか遠くの知らない場所で書き換えられるかもしれないというのはそれなりに危険なことなので、あえて面倒な構文になっています。

上記の例でも、参照引数を参照戻り値で返して、それをさらに参照ローカル変数で受け取るものもあります。 ここだけ抜き出すと以下のような感じです。

static void Main()
{
    var x = 10;
    ref var y = ref Ref(ref x);
    y = 0; // y は巡り巡って x を参照。x も 0 に

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

static ref int Ref(ref int x) => ref x;

これで、下図のような状態になっています。これくらい単純な例でも、結局どこが書き換わるのかそこそこわかりづらくなるので注意が必要です。

参照引数を参照戻り値で返して、参照ローカル変数で受ける

参照戻り値で返せるもの

参照渡しをするときに気を付けないといけないのは、参照をたどった先の大元が消えしまっている可能性があることです。 C#の参照渡しでは、そうならないように、参照できるものを制限しています。

(他のプログラミング言語では、参照渡しが必ずしも安全でなかったり(不正なメモリ操作につながる)、逆に参照渡しの機能を提供していないものもあります。 .NETも、ILのレベルでは安全でない参照もできたりします。 C#は、コンパイラーが厳しめにチェックして、安全でない参照ができないようにしています。)

  • 参照引数は常に安全です
    • なので、これはC# 1.0の頃から認められています
  • 参照戻り値の場合、返しても安全かどうかを判定して、安全でない可能性があるならコンパイル エラーになります
    • 参照引数は参照戻り値で返せます
    • 通常の引数やローカル変数は返せません
    • 参照ローカル変数などを挟んで、多段に参照している場合、コードをたどって大元が安全かどうかまで調べます

例えば、以下のようなコードは、強調表示しているところがコンパイル エラーになります。

// 参照引数は参照戻り値で返せる
private static ref int Success1(ref int x) => ref x;

// 値渡しの引数はダメ
private static ref int Error1(int x) => ref x;

// ローカル変数はダメ
private static ref int Error2()
{
    var x = int.Parse(Console.ReadLine());
    return ref x;
}

// 多段の場合も元をたどって出所を調べてくれる
private static ref int Success1(ref int x, ref int y)
{
    ref int r1 = ref x;
    ref int r2 = ref y;
    ref int r3 = ref Max(ref r1, ref r2);

    // r3 は出所をたどると引数の x か y の参照
    // x も y も参照引数なので大丈夫
    return ref r3;
}

private static ref int Error1(ref int x, int y)
{
    ref int r1 = ref x;
    ref int r2 = ref y;
    ref int r3 = ref Max(ref r1, ref r2);

    // y が値渡しなのでダメ
    return ref r3;
}

private static ref int Error2(ref int x)
{
    var y = int.Parse(Console.ReadLine());
    ref int r1 = ref x;
    ref int r2 = ref y;
    ref int r3 = ref Max(ref r1, ref r2);

    // y がローカル変数なのでダメ
    return ref r3;
}

C# 7では、コンパイラーが賢くなって、この「大元をたどって調べる」という仕事ができるようになったので、参照戻り値や参照ローカル変数が使えるようになったということです。

更新履歴

ブログ