1. ホーム 
  2. 備忘録 
  3. C Sharp

参照渡し

参照渡し

プログラミング言語での値の受け渡し方には2つの方法があり、それぞれ『値渡し』『参照渡し』という

C#では基本的に値の受け渡しは『値渡し』となる

しかし ref や out キーワードを使うことで『参照渡し』を行うことができる

値渡し
メソッド内で引数の値を書き換えても呼び出し元に影響しない
参照渡し(ref)
メソッド内で引数の値を書き換えると呼び出し元にも影響する
out
特殊な参照渡し
戻り値以外にも値を返したいとき(複数の値を返したいとき)に使用する

C# 6以前では、引数の受け渡しの際に ref または out キーワードを付けることで参照渡しができる

C# 7以降では、変数間の受け渡しや戻り値にも ref キーワードをつけることで参照渡しができるようになった

なお、受け渡し方は『値渡し』と『参照渡し』があるが、別に型の区分として『値型』『参照型』があるので、『値型の値渡し』『値型の参照渡し』『参照型の値渡し』『参照型の参照渡し』というような組み合わせがあるので注意すること


参照引数 ref

C#には ref引数 と in引数 と out引数 の3種類の参照渡しがある

単に『参照引数』という場合は ref引数 を指す

in引数 や out引数 のように制約がなく、読み書き両方ができる引数となっている

メソッドの引数に ref キーワードをつけることで、その引数は参照渡しとなる

また、呼び出し側も引数に ref を付ける必要があることに注意する
これはメソッドの使用者が明確に意図して参照引数を使用したことを確認するためである

using System;

class Program
{
    static void Main()
    {
        int a = 100;
        Console.Write( "{0} → ", a );

        // ref キーワードを使わない「値型の値渡し」
        TestNoRef( a );
        Console.Write( "{0} → ", a );

        // ref キーワードを使う「値型の参照渡し」
        TestRef( ref a );
        Console.Write( "{0}\n", a );

        // 結果
        // 100 → 100 → 10
    }

    // 通常の引数 中で書き換えても影響が伝搬しない
    static void TestNoRef( int a )
    {
        a = 10;
    }

    // 参照引数 中で書き換えると影響が伝搬する
    static void TestRef( ref int a )
    {
        a = 10;
    }
}

入力参照引数 in

C# 7.2 から、「参照渡しだけども読み取り専用」というような引数の渡し方ができるようになった

「参照渡しだけども読み取り専用」というのは、 大きめの構造体を使う際に役立つ

大きめの構造体を値渡しすると、構造体は値型なのでコピーが発生し、それが負担となるケースがある

以下は 参考文献1 よりコードを引用

public struct Quarternion
{
    public double W;
    public double X;
    public double Y;
    public double Z;
    public Quarternion(double w, double x, double y, double z) => (W, X, Y, Z) = (w, x, y, z);

    // 足し算4つくらいならインライン展開されて、値渡しでもコピーのコストが掛からない
    public static Quarternion operator +(Quarternion a, Quarternion b)
        => new Quarternion(
            a.W + b.W,
            a.X + b.X,
            a.Y + b.Y,
            a.Z + b.Z);

    // このくらい中身が大きい(掛け算16個、足し算9個)と、インライン展開されないので in 引数にする効果が結構出る
    public static Quarternion operator *(in Quarternion a, in Quarternion b)
        => new Quarternion(
            a.W * b.W - a.X * b.X - a.Y * b.Y - a.Z * b.Z,
            a.W * b.X + a.X * b.W + a.Y * b.Z - a.Z * b.Y,
            a.W * b.Y + a.Y * b.W + a.Z * b.X - a.X * b.Z,
            a.W * b.Z + a.Z * b.W + a.X * b.Y - a.Y * b.X);
}

構造体を用いたメソッド内で、インライン展開されないような大きい処理を行う場合は in引数 を用いた参照渡しを活用することも検討する


他にも注意点として in引数 を使ってもコピーが発生する場合の記載があったのでこちらも引用させていただく

// 作りとしては readonly を意図しているので、何も書き換えしない
// でも、struct 自体には readonly が付いていない
struct NoReadOnly
{
    public readonly int X;
    public void M() { }
}

// NoReadOnly と作りは同じ
// ちゃんと readonly struct
readonly struct ReadOnly
{
    public readonly int X;
    public void M() { }
}

class Program
{
    // in を付けたので readonly 扱い → M を呼ぶ際にコピー発生
    static void F(in NoReadOnly x) => x.M();

    // readonly struct であれば問題なし(コピー回避)
    static void F(in ReadOnly x) => x.M();
}

コピーが発生する理由としては、呼び出し側としては「メソッド内部で値(上記のコードでいえば例えば M() の中で X の値を書き換える)が書き換わっていない」ことが保証できないため、メソッドを呼んだ時点で無条件にコピーを作る挙動をとるようだ

なので struct 自身に readonly をつけると「書き変わらない(読み取り専用)」ことが保証できるためコピーを回避できる、ということのようだ

こちらもあわせて意識していきたい


出力引数 out

参照渡しを使うとメソッド内からメソッド外にある変数を書き換えることができる

これをメソッドの戻り値代わりに使うこともできる

ref引数 を使った場合は事前に初期化しておく必要があったり、メソッドの中で代入を忘れてしまったときにバグが発生してしまったりと問題がある

out引数 を使った場合は上記の問題を解消できる利点がある

using System;

class Program
{
    static void Main()
    {
        // ref を使う場合は外側で初期化が必要
        int a = 0;
        int b = 0;
        TestRef( ref a, ref b );
        Console.WriteLine( "a = {0}, b = {1}", a, b );
        
        // 結果
        // a = 10, b = 0
        // b = 0 のままなことに出力してから気付く

        // out を使う場合は外側で初期化しなくてもよい
        int c, d;
        TestOut( out c, out d );
        Console.WriteLine( "c = {0}, d = {1}", c, d );
        
        // 結果
        // c = 10, d = 5
    }

    static void TestRef( ref int a, ref int b )
    {
        a = 10;
        // 本当は b も代入すべきところを忘れるとバグが発生する
    }

    static void TestOut( out int c, out int d )
    {
        c = 10; // メソッド内で必ず値を代入しなければならない
        d = 5;  // 書き忘れるとコンパイルエラーが発生するのですぐにわかる
    }
}

複数の戻り値を返す必要がある場合、C# 6以前では参照引数を使用するのが唯一の手段だった

C# 7以降ではタプル型が追加されて複数の値を返すことができるようになった


注意

参照はスタック上でしか使えないこともあり、参照引数もこの制限に引っかかる

その結果、参照引数(ref、in、out)には以下の制限がある

  • クロージャにキャプチャできない
  • イテレータや非同期メソッドの引数には使えない

    参考文献

  1. [C# によるプログラミング入門]参照渡し