読者です 読者をやめる 読者になる 読者になる

xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

ref 戻り値 / ref ローカル変数

今回は ref 戻り値 (Ref Returns)ref ローカル変数 (Ref Locals) について見ていきます。それぞれの機能は端的に言うと以下のようなものです。

名称 機能
Ref Returns メソッドの戻り値を参照として返す
Ref Locals 値を参照として受ける

C++ に嗜みがある方であれば & キーワード による参照渡しと言うと分かりやすいかもしれません。むしろ C++er からすれば圧倒的なイマサラ感すら沸き上がるかもしれません...。この機能は GitHub 上の以下の issues で提案/議論されています。

パフォーマンスの向上

基本的に値型でですが、引数に渡したり戻り値として値を返す場合はメモリ領域が再確保され、そこにコピーが生成されます。以下のような超シンプルなコードでも、実は値が 3 回コピーされています。(確かそのはず...

static void Main()
{
    var a = 123;  //--- 1. メモリ領域確保
    var d = PassThrough(a);  //--- 4. 戻されたのを受けとるときにもう一回コピー
}

static int PassThrough(int b)  //--- 2. 引数として渡すのに値をコピー
{
    int c = b;  //--- 3. 別の変数として受ける場合もコピー
    return c;
}

つまり、上記で言うと変数 a / b / c / d はすべてメモリ上の別領域に値を持っています。int 型のような軽量なものであれば特に気にならないのかもしれませんが、サイズのデカい値型となるとコピー (とその領域のガベージコレクションも?) による影響が無視できなくなります。究極的にパフォーマンスを高く持つためには、値のコピー (とメモリ領域の解放も?) の頻度/回数を減らす必要があります。

これをサポートする機能としてこれまでも ref 引数 (参照渡し)が搭載されていましたが、ローカル変数として参照の形で値を受けたり、参照のまま戻り値として返す (= 参照返し) は提供されていませんでした。今回追加される ref 戻り値 / ref ローカル変数で、そのような足りなかったパーツが補われます。

安全性

実は、これまでの C# 6.0 まででもアンセーフコードという機能を使うことで同様のことを実現できました。通常 C# ではポインタは使えないのですが、プロジェクトのプロパティで [アンセーフコードの許可] を設定することでポインタが利用できるようになります。しかし、C# においてはポインタを直で触ることは推奨されません。詳しくは以下に書かれているので参考にしてください。

ref 戻り値 / ref ローカル変数ではアンセーフコードを有効化することなく、安全に参照渡しを実現することができます。

使い方検証

以下のサンプルは「配列の要素を書き換える」というのをヒジョーに回りくどくやってみたものです。アチコチに ref が入ってだいぶ気持ち悪いですが、こんな風に書くんだというところだけ掴んでいただければと思います。

static void Main()
{
    var a = new int[] { 0, 1, 2, 3, 4 };
    Console.WriteLine(string.Join(",", a));
    // 0, 1, 2, 3, 4

    ref var d = ref GetValue(a);  //--- 参照返しを変数 d で受ける
    d = 5;                        //--- 書き換え
    Console.WriteLine(string.Join(",", a));
    // 0, 1, 5, 3, 4
}

static ref int GetValue(int[] b)
{
    ref var c = ref b[2];  //--- b の 3 番目の要素を参照する変数 c を作る
    return ref c;          //--- 変数 c の参照先を返す
}

ローカル変数の参照返しはダメ

ローカル変数として定義された値を参照返ししようとするとコンパイルエラーになります。スコープを越えてガベージコレクションの対象となったメモリ領域を参照していることは危険しかないので、シッカリ防いでくれます。

static ref int GetValue()
{
    var a = 123;
    return ref a;  //--- コンパイルエラー
}

逆に言うと、フィールド / プロパティ / 呼び出し元のローカル変数など関数内で定義されたものを対象しないのであれば、ref 戻り値や ref ローカル変数を利用することができます。

ref 引数を返す

ref 引数は参照渡しなので、その値をそのまま戻すことは可能です。

static ref int GetValue(ref int a, ref int b)
{
    if (a > b)
        return ref a;
    return ref b;
}

条件演算子 (三項演算子) が使えない

仕様なのかバグなのかは定かではありませんが、上記のサンプルに対して条件演算子を使ってみるとコンパイルエラーになります (謎ぃ...

static ref int GetValue(ref int a, ref int b)
    => (a > b) ? ref a : ref b;  //--- コンパイルエラー

プロパティで参照返し

参照返しはメソッドだけでなくプロパティにも適用できます。ただし自動実装プロパティに対しては適用できません。

class Person
{
    //--- OK! setter はないけど直接書き換え可能
    public ref string Name => ref this.name;
    private string name = "Anders";

    //--- NG! 自動実装プロパティの参照渡しはできない
    //public ref string Name { get; }
}

static void Main()
{
    var p = new Person();
    Console.WriteLine(p.Name);  //--- Anders
    p.Name = "xin9le";
    Console.WriteLine(p.Name);  //--- xin9le
}

インデクサで参照返し

参照返しのインデクサを作ることで、あたかも配列のように扱うこともできます。

class ReadOnlyIntArray
{
    private int[] Source { get; }
    public ref int this[int index] => ref this.Source[index]; 
    public ReadOnlyIntArray(int[] source)
    {
        this.Source = source;
    }
}

static void Main()
{
    var a = new int[] { 0, 1, 2, 3, 4 };
    var b = new ReadOnlyIntArray(a);
    Console.WriteLine(b[2]);  //--- 2
    b[2] = 5;                 //--- ref がない場合はコンパイルエラー
    Console.WriteLine(b[2]);  //--- 5
}

逆コンパイル

どのように実現されているかを知るために逆コンパイルをしてみましょう!今回は先の例でも利用した以下のサンプルを使います。

using System;

namespace RefReturnsRefLocals
{
    class Program
    {
        static void Main()
        {
            var a = new int[] { 0, 1, 2, 3, 4 };
            Console.WriteLine(string.Join(",", a));

            ref var d = ref GetValue(a);
            d = 5;
            Console.WriteLine(string.Join(",", a));
        }

        static ref int GetValue(int[] b)
        {
            ref var c = ref b[2];
            return ref c;
        }
    }
}

逆コンパイル結果が以下です。unsafe コードとして展開されていますがどうやらこれは ILSpy の誤判定っぽく、IL を直接のぞくと int& (= 参照渡し) として展開されていることが分かります。IL レベルとしてはずっと以前から参照返しをサポートしていましたが、C# 側での制限として実装されていなかったようですね。

using System;

namespace RefReturnsRefLocals
{
    internal class Program
    {
        private unsafe static void Main()
        {
            int[] a = new int[]{ 0, 1, 2, 3, 4 };
            Console.WriteLine(string.Join<int>(",", a));
            int* d = Program.GetValue(a);
            *d = 5;
            Console.WriteLine(string.Join<int>(",", a));
        }

        //--- unsafe コードとして展開されているように見えるけど...
        private unsafe static int* GetValue(int[] b)
            => ref b[2];
    }
}
.method private hidebysig static 
    int32& GetValue (  //--- IL では & キーワードによる参照渡しで出力されている
        int32[] b
    ) cil managed 
{
    .maxstack 2
    .locals init (
        [0] int32& c,
        [1] int32&
    )

    IL_0000: nop
    IL_0001: ldarg.0
    IL_0002: ldc.i4.2
    IL_0003: ldelema [mscorlib]System.Int32
    IL_0008: stloc.0
    IL_0009: ldloc.0
    IL_000a: stloc.1
    IL_000b: br.s IL_000d

    IL_000d: ldloc.1
    IL_000e: ret
}