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

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# 7.3からできるようになっています (逆に、C# 7.0~7.2 ではこの機能は使えません)。

参照戻り値で返せるもの

もし何の制限も掛かっていないなら、参照渡しでは参照をたどった先の大元が消えしまっている可能性があって危険です。 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では、コンパイラーが賢くなって、この「大元をたどって調べる」という仕事ができるようになったので、参照戻り値や参照ローカル変数が使えるようになったということです。 こういうコンパイラーの努力をエスケープ解析(escape analysis: 逃がしてはいけないものが漏れ出ていないかの解析)といいます。

ただし、C# 7でも、あくまでメソッド内で完結できる範囲でしか「たどって調べる」ということができません。 例えば、以下のようなコードはコンパイルできません。

// あまり意味のないメソッドなものの…
// 第1引数しか参照しない
static ref int X(ref int x, ref int y) => ref x;

static ref int Y(ref int x)
{
    int local = 1;

    // X の中身まで追えば、実のところ local は参照していないものの、そこまでは追えない
    // あくまで、「local を参照で渡してしまった以上、X の戻り値に local が含まれている可能性あり」と判定する
    // 結果的に、このコードはコンパイル エラーになる
    return ref X(ref x, ref local);
}

このコードは、もし仮に、XYの中で展開してしまえば、ローカル変数localの参照を戻り値として返さないということがわかるんですが、 コンパイラーはそこまでは追ってくれません。 (こういうXの中身次第で変わる挙動を認めてしまうと、Xの変更の影響がX利用側(この例の場合Y)に及び過ぎるため問題があります。 「追ってくれない」というより、意図的に「追わない」という面もあります。)

構造体のフィールドの参照(戻り値にできない)

C# コンパイラーが行う「参照戻り値に返して安全かどうか」の判定で、 1つ注意が必要な点があります。 構造体の場合、フィールドの参照を返せません。 (ただし、C# 7.2 では、ref引数拡張メソッドを救済策として使えます。)

例えば、以下のコードはコンパイル エラーになります。

struct Struct
{
    int _v;
    public ref int Value => ref _v; // ダメ
}

class Class
{
    int _v;
    public ref int Value => ref _v; // クラスの場合はOK
}

ちなみに、エラーになるのは構造体のフィールドの参照を直接返している場合だけです。 以下のように、フィールドを介していても、参照型の中の参照を返すことはできます。

struct ArrayOffset<T>
{
    T[] _array;
    int _offset;
    public ArrayOffset(T[] array, int offset) => (_array, _offset) = (array, offset);

    // フィールドの参照を直接返しているわけではなく、
    // 配列 T[] (参照型)の中の参照を返しているのでOK
    public ref T this[int i] => ref _array[i + _offset];
}

構造体内では、フィールドの読み書きのために、実はthisが参照扱いになっています。 そのせいで、「大元をたどって参照を返せるかどうかを調べる」という作業が難しく、 結局「構造体はフィールドの参照(thisが絡む参照)を返せない」という制限を掛けたそうです。

この仕様は、少し詳しい人であれば何か釈然としないものがあるかもしれません。 例えば以下のように、拡張メソッド的に(静的メソッドで)書けば似たようなことが実現できます。

struct Struct
{
    internal int _v;

    // ↓これはダメ(なのでコメントアウト)
    // public ref int V() => ref _v;
}

static class Extensions
{
    // Struct.V() と、実のところやっていることは同じ
    // (構造体内では、this は参照扱いになっている)
    // Struct.V() ではダメなのに、同じことを静的メソッドでやるとできる
    public static ref int V(ref Struct @this) => ref @this._v;
}

実のところ、「thisが参照扱いになっている」というのはこのコードと似たような状態で、 このコードが許されるのに通常のメソッドでは許されないというのは少し不思議です。

正確には、「以下の2つのうちどちらか片方を選ぶ必要があり、前者を選んだ」ということだそうです。

  • 構造体はフィールドの参照を返せない(C# 7で選んだ仕様)
  • 構造体の関数メンバーを呼ぶ際には、常にthis参照が引数として渡っている前提で安全性を調べる(選ばなかった仕様)

要するに、以下の例の、Okメソッドのようなものを認めるためには前者の仕様が必要です。

struct ArrayOffset<T>
{
    // 拡張メソッドから参照するために internal
    internal T[] _array;
    internal int _offset;
    public ArrayOffset(T[] array, int offset) => (_array, _offset) = (array, offset);

    // OK
    public ref T this[int i] => ref _array[i + _offset];
}

static class Extensions
{
    // ArrayOffset のインデクサーと同じことを静的メソッドで書く
    public static ref T Get<T>(ref ArrayOffset<T> @this, int i) => ref @this._array[i + @this._offset];
}

class Program
{
    static ref int Ok()
    {
        // a はローカル変数なので、こいつが絡む参照は戻り値にしてはいけない
        var a = new ArrayOffset<int>(new[] { 1, 2, 3 }, 1);

        // 構造体の関数メンバーはフィールドの参照を返さないという仕様なので、
        // この ref には a 絡みの参照は絶対にない
        return ref a[1];
    }

    static ref int Ng()
    {
        // 同上、a 絡みの参照は返せない
        var a = new ArrayOffset<int>(new[] { 1, 2, 3 }, 1);

        // a が参照引数にわたっている以上、Get の戻り値には a 絡みの参照が含まれる可能性がある
        // コンパイル エラーになる
        return ref Extensions.Get(ref a, 1);
    }
}

あと、以下のように、ジェネリクス絡みの問題を避けるためにもこの仕様を選ぶ必要があったそうです。

using System;

interface IReference
{
    ref int Value { get; }
}

class ReferenceClass : IReference
{
    int _value;
    public ref int Value => ref _value;
}

struct ReferenceStruct : IReference
{
    int _value;
    public ref int Value => ref _value; // 認められていない。もし認めると…
}

class Program
{
    static void Main()
    {
        ref var r = ref X<ReferenceClass>();
        r = 1;
        Console.WriteLine(1);
    }

    static ref int X<T>()
        where T : IReference, new()
    {
        var x = new T();
        return ref x.Value; // T が構造体だと、返してはいけないはずの参照が返る
    }
}

条件演算子での ref 利用

Ver. 7.2

C# 7.2から、条件演算子の2項目、3項目を参照にできるようになりました。 以下のような書き方ができます。

x > y ? ref x : ref y

これを、さらに参照ローカル変数や参照戻り値で受けたい場合には、条件演算子の前にも ref が必要です。

var x = 1;
var y = 2;

// 条件演算子自体は ref を返すものの、その前に ref を付けていない
// v の型は int になる
var v = x > y ? ref x : ref y; ;

v = 10; // 書き換えても x, y に影響なし
Console.WriteLine((x, y)); // (1, 2)

// 条件演算子の前にも ref を付ける
// v の型は ref int になる
ref var r = ref x > y ? ref x : ref y; ;

r = 10; // y が書き換わる
Console.WriteLine((x, y)); // (1, 10)

この「条件 ref」は、左辺にも使えます。 例えば以下のように、「条件付きで xy のどちらかを書き換える」みたいなことができます。

var x = 1;
var y = 2;

// y が書き換わる
(x > y ? ref x : ref y) = 10;

Console.WriteLine((x, y)); // (1, 10)

ただし、この例の通り、左辺に () が必要です。 (ref に限った話ではなく、単に演算子の優先度の問題です。 代入と条件演算子が並んでいる場合、右から順に結合するので、()がなければ代入が先に解釈されます。)

ref readonly

Ver. 7.2

in引数と併せてC# 7.2で、 参照戻り値と参照ローカル変数でも「参照渡しだけども読み取り専用」という渡し方ができるようになりました。 以下のように、ref readonlyで修飾します。

static ref readonly int Max(in int x, in int y)
{
    ref readonly var t = ref x;
    ref readonly var u = ref y;

    if (t < u) return ref u;
    else return ref t;
}

ref readonlyと書く必要があるのは型名の側だけで、受け渡しする側(上記コードで言うとref xref y)の方はrefだけ書きます。

ちなみに、引数のinと、ローカル変数・戻り値の ref readonly は全く同じ意味です。 提案当初は引数でもref readonlyと書かせる案もありましたが、out引数との対称性がきれいだったため、最終的にはinの方が採用されました。

ref再代入

Ver. 7.3

C# 7.3で、参照引数、参照ローカル変数のref再代入(ref reassignment)というものができるようになりました。 参照先の値の書き換えではなく、「どこを参照しているか」自体を書き換える機能です。

以下のように、参照ローカル変数への代入時に、右辺にrefを付けることでref再代入になります。

int x = 1;
int y = 2;

// x を参照。
ref var r = ref x;

// このとき、r に対する代入は x に反映される。
r = 10; // x が 10 になる。

// これが ref 再代入。
// r が y を参照するようになる。
r = ref y;

// 今度は、r に対する代入が y に反映される。
r = 20; // y が 20 になる。

Console.WriteLine((x, y)); // (10, 20)

ちなみに、参照引数に対しても使えます。

static void M1(ref int x, ref int y)
{
    x = ref y;
}

static void M2(in int x, ref int y)
{
    x = ref y;
    // y = ref x; ←逆は当然ダメ
}

static void M3(ref int x, out int y)
{
    y = 0; // 先に値を与えないとダメ
    x = ref y;
    y = ref x;
}

この機能の用途はそんなに広くはありませんが、 例えば、配列中のデータの探索などで、この機能を使うとシンプルに書けて速度的にも有利なことがあります。 以下の例は、intの配列中の最大値になっているところを参照戻り値で返す処理ですが、 都度インデックス アクセスするよりも、ref再代入を使ったコードの方が少しだけ有利です。

static ref int RefMaxOld(int[] array)
{
    if (array.Length == 0) throw new InvalidOperationException();

    // これまでこんな感じでインデックスで持って、
    var maxIndex = 0;

    for (int i = 1; i < array.Length; i++)
    {
        // 毎度毎度、配列のインデックス アクセスするようなコードを書くことがたまに。
        // array[maxIndex] で配列の中身を取り直すのがちょっともったいない。
        if (array[maxIndex] < array[i])
        {
            maxIndex = i;
        }
    }

    return ref array[maxIndex];
}

static ref int RefMax(int[] array)
{
    if (array.Length == 0) throw new InvalidOperationException();

    // それを、こんな風に参照ローカル変数に変えて、
    ref var max = ref array[0];

    for (int i = 1; i < array.Length; i++)
    {
        // ref 再代入で済ませるように。
        ref var x = ref array[i];
        // array (の先頭)に maxIndex を足す作業が減る。
        if (max < x) max = ref x;
    }

    return ref max;
}

for/foreach のループ変数を参照に

Ver. 7.3

C# 7.3から、forステートメントやforeachステートメントのループ変数も、参照ローカル変数にできるようにないました。

forの方は分かりやすいでしょう。単に、for (初期化式; 条件式; 更新式)の初期化式内で参照ローカル変数を定義できるようになっただけです。

var array = new[] { 1, 3, 5, 2, 4 };

var x = 0;

for (ref int i = ref x; i < array.Length; i++)
{
    if (array[i] == 5) break;
}

Console.WriteLine(x); // break した時点の i の値 = 2

用途はそんなに思い浮かびませんが、例えば、C++でよくやるような、ポインター風の配列列挙に使えるかもしれません。

foreachの方も、通常のforeachと同じパターンで、MoveNextCurrentの呼び出しに展開されるだけです。 Currentが参照戻り値を返すとき、それをrefループ変数で受け取ることができます。

using System;

class Program
{
    static void Main()
    {
        var array = new int[10];
        foreach (ref var x in array.AsRef())
        {
            // ちゃんとこれで、配列の各要素を書き換えられる。
            x = 1;
        }

        foreach (var x in array)
        {
            // 全要素 1 になってる。
            Console.WriteLine(x);
        }
    }
}

// 標準で ref 戻り値になっている Enumerable はないので自作。
struct RefArrayEnumerable<T>
{
    T[] _array;
    public RefArrayEnumerable(T[] array) => _array = array;
    public RefArrayEnumerator<T> GetEnumerator() => new RefArrayEnumerator<T>(_array);
}

struct RefArrayEnumerator<T>
{
    int _index;
    T[] _array;
    public RefArrayEnumerator(T[] array) => (_index, _array) = (-1, array);
    // Current が ref 戻り値になっているのがポイント。
    public ref T Current => ref _array[_index];
    public bool MoveNext() => ++_index < _array.Length;
}

static class RefExtensions
{
    public static RefArrayEnumerable<T> AsRef<T>(this T[] array) => new RefArrayEnumerable<T>(array);
}

この例でもコメントに書いていますが、 言語機能として認められたと言っても、現状はこのパターン通りの列挙子がほとんどないので、 この機能の恩恵はなかなか受けづらくはあります。 また、「IEnumerable<T>のref版」のようなインターフェイスもありません。

ただ、.NET Core 2.1 から導入されたSpan<T>であれば、 Enumeratorref 戻り値な Current を持っています。AsSpan拡張メソッドで配列をSpan<T>にできるので、以下のようなコードが書けます。

using System;
 
class Program
{
    static void Main()
    {
        var array = new int[10];
        foreach (ref var x in array.AsSpan())
        {
            // ちゃんとこれで、配列の各要素を書き換えられる。
            x = 1;
        }
 
        foreach (var x in array)
        {
            // 全要素 1 になってる。
            Console.WriteLine(x);
        }
    }
}

余談(将来の話): let や readonly 引数・ローカル変数

ローカル変数に対して ref readonly var xというように書くのは長ったらしくて多少しんどいものがあります。

ref readonlyだけが先に入ることになりましたが、(参照ではなく単に) readonly な引数やローカル変数も今後入る予定です。 その際、readonly varの省略形としてletなど1単語を使った書き方ができるようになる予定です。 (letはもう少し高度な機能として提供される予定ですが、“readonly varとしても”使えます。)

// (将来の予定)
static void F(readonly int x)
{
    readonly int a = 1;
    readonly var b = 1;
    let c = 1;

    // 以下、いずれもコンパイル エラー
    x = 1;
    a = 2;
    b = 3;
    c = 3;
}

ちなみに、ref readonlyの語順がこの順になっている理由も、この仕様を見越してのことです。 将来的には、以下のような使い分けを考えています。

  • ref: 「再参照」も「参照先の値の書き換え」もできる
  • readonly: 「値の書き換え」ができない
  • readonly ref: 「再参照」できない
  • readonly ref readonly: 「再参照」も「参照先の値の書き換え」もできない

更新履歴

ブログ