参照渡しとポインター

少し内部的な話もしておきましょう。 内部的には、参照渡しとポインターは似たようなものです。

もちろん、型システム上の扱いとしては、以下のような差があります。

参照渡し ポインター
通常のコンテキスト内で使える代わりに、制限がきつい unsafeコンテキストでしか使えない代わりに、自由が利く
基本的に、有効なオブジェクトしか参照できない どこでも参照できる。p + 1など、数値との加減算して隣接するメモリを参照できる
どんな型でも参照できる アンマネージ型」と呼ばれる一部の型しか参照できない

しかし、読み書きに使われる命令的には参照渡しとポインターは全く同じだったりします。 例えば、以下の2つのメソッドを見てみましょう。

public static ref int Max(ref int x, ref int y)
{
    if (x >= y) return ref x;
    else return ref y;
}

public static unsafe int* Max(int* x, int* y)
{
    if (*x >= *y) return x;
    else return y;
}

やっていることは全く同じで、ただ型的に参照渡しかポインターかが違います。 このコードのコンパイル結果は、下図のように、ほとんど同じになります。

参照渡しとポインターを使ったコードのコンパイル結果

型としては、引数と戻り値のところを見ての通り、&*の差があります(&が参照渡しで、*がポインターです)。 一方で、メソッドの中身に関しては一字一句たがわず同じです。

ldindはload indirect (間接ロード)の略で、 ポインターや参照ごしに値を取ってくる命令ですが、 ポインターと参照でまったく同じ命令を使います。

参照渡しとポインターの相互変換

命令上互換性があるわけで、やろうと思えば参照渡しとポインターの間で相互変換が可能です。 C#を使って書けるコードではありませんが、ILを使えば書けます。

そのILで書かれたライブラリを参照すれば、C#からも参照渡し⇔ポインターの変換ができます。 CoreFXによる公式実装があって、以下のように、NuGetパッケージとして公開されています。

このパッケージ中にあるUnsafeクラスを使うと、以下のようなコードが書けます。

unsafe
{
    int x = 1;
    void* pointer = Unsafe.AsPointer(ref x);
    *(int*)pointer = 2;

    Console.WriteLine(x); // 2 になってる

    ref int r = ref Unsafe.AsRef<int>(pointer);
    r = 3;

    Console.WriteLine(*(int*)pointer); // 3 になってる
}

これで何がうれしいかというと、以下のように、タイプが異なるいろんなメモリ領域を統一的に扱えたりすることです。 また、ポインターを使う部分にはunsafeコンテキストが必要ですが、作られたクラスを使うだけなら、使う側にはunsafeを求めません。

using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

struct ManagedBuffer
{
    int[] _array;
    public ManagedBuffer(int length) { _array = new int[length]; }

    public ref int this[int index] => ref _array[index];
}

unsafe struct UnsafeBuffer
{
    void* _pointer;
    public UnsafeBuffer(int* pointer) { _pointer = pointer; }

    public ref int this[int index] => ref Unsafe.AsRef<int>(_pointer);
}

class Program
{
    unsafe static void Main()
    {
        // 配列と
        var b1 = new ManagedBuffer(10);
        b1[0] = 1;

        // スタック領域と
        var stack = stackalloc int[10];
        var b2 = new UnsafeBuffer(stack);
        b2[0] = 1;

        // アンマネージなメモリとを同じように触れる
        var p = Marshal.AllocHGlobal(10 * sizeof(int));
        var b3 = new UnsafeBuffer((int*)p);
        b3[0] = 1;

        Marshal.Release(p);
    }
}

特に、C# の管理外の世界からもらったアンマネージなメモリ領域を手軽に参照できるのは、パフォーマンスの改善に大きく寄与します。

一方で、もちろん、unsafeコンテキストを経由するので、通常のC#の感覚からするとおかしなこともできます。 例えば、本節の冒頭の表で「参照渡しは有効なオブジェクトしか参照できない」という説明をしましたが、 この制約を破ることができます。 例えば、以下のようなコードで、「参照渡しのnull」を作れます。

using System;
using System.Runtime.CompilerServices;

unsafe static class NullReference
{
    public static ref T Null<T>() => ref Unsafe.AsRef<T>((void*)0);
    public static bool IsNull<T>(ref T x) => Unsafe.AsPointer(ref x) == (void*)0;
}

class Program
{
    static void Main()
    {
        ref var x = ref NullReference.Null<int>();
        Console.WriteLine(NullReference.IsNull(ref x)); // true
        Console.WriteLine(x); // 実行時エラー。NullReferenceException 発生
    }
}

注意して使いましょう。

更新履歴

ブログ