[C#] ref引数, ref戻り値, refローカル変数, etc..

[C#] ref引数, ref戻り値, refローカル変数, etc..

C# の ref いろいろ

C# には参照を表す ref キーワードがいろいろあります。この記事では以下の文脈で使われる ref キーワードの意味をまとめます。

  1. ref引数(参照渡し)
  2. refローカル変数
  3. ref戻り値(参照戻り値)
  4. foreach の ref

ref引数

最もよく使われるであろう ref キーワードが 参照渡しref です。

メソッドの引数に値をコピーして渡す 値渡し ではなく、参照を直接引数として渡す 参照渡し として定義するときに ref を使用します。

// 参照渡しで渡された引数をインクリメントする
public static void Increment(ref int x)
{
    x++;
}

var x = 0;
Increment(ref x);
Console.WriteLine($"x={x}"); // x=1

ref引数で参照渡しすれば呼び出し元の値を書き換えることができます。

注意点としてref引数で参照渡しする場合は、必ずその値が初期化されている必要があります。例えば宣言しているだけで値が初期化されていない場合は、ref引数として渡すことはできません。

static void Increment(ref int x)
{
    x++;
}

// 未定義の変数を参照するためコンパイルエラー
int x;
Increment(ref x);

つまり先に値を定義してからでないとref引数による参照渡しは使用できません。余計な値の初期化等が入るため、場合によっては無駄な処理コストが入ることになります。

参照渡しをした先で値の初期化を行いたい場合は後述の out引数 を使用します。ref引数と似ているので、out引数とin引数についてもまとめます。

out引数

out引数 は 呼び出し元の変数を更新できるという点において、ref引数と似た参照渡しとして使える機能です。

out引数は C#1.0 から使える機能で、もともとは複数の結果を呼び出し元に返したいときに使われるものでした。ただし、現状ではタプルが使えるため複数の結果を返すという理由で使うことはありません。

out引数は出力引数という意味です。処理結果を出力するための引数です。

ref引数と違い、必ず呼び出し先で値を初期化する必要がありますが、呼び出し元で初期化する必要はありません。無駄な初期化処理が削除できます。

static void F(out int a, out int b)
{
    a = 1; b = 2;
}

int a, b;
F(out a, out b);
Console.WriteLine($"{a}, {b}"); // 1, 2

out引数は、out [型名] で宣言と同時に引数に渡すこともできます。

out引数は型変換の int.Parse で使われています。

if (int.TryParse("123", out var num))
{
    Console.WriteLine(num);
}

もし int.TryParse がref引数を受け取る場合いちいち、値の宣言と初期化が必要ですが、out引数なのでいちいち初期化しなくても大丈夫というわけです。

in引数

参照で引数に渡したいけど、渡した先で値を書き換えられるのは困るというような場合、読み取り専用で参照を渡すための in引数 が使えます。

出力用の引数であるout引数に対して、入力用の引数であるin引数という位置づけの命名です。

static void F(in int a)
{
    // Error: 読み取り専用の変数であるため、変数 'in int' に割り当てることができません
    a = 1;
}

サイズの大きな構造体などで使うもので、int型に使ってもあまり意味はありません。。。

参照渡しする対象について、ref, out, in の違いをまとめると以下のようになります。

キーワード 宣言 初期化 読み取り専用
ref 必須 必須 ×
out 呼び出しと同時に可能 不要 ×
in 必須 必須

ref ローカル変数

参照をローカル変数に持つことができます。

var array = new int[] { 0, 1, 2, 3, 4 };
ref var a0 = ref array[0];
a0 += 100;

// 100, 1, 2, 3, 4
Console.WriteLine(string.Join(", ", array));

上の例では配列の要素を参照でローカル変数として保持してそれを更新しています。参照を更新するので参照元の配列の要素が更新されれることが確認できます。

もちろん単純にローカル変数の参照を複製することもできます。

var a = 0;
ref int b = ref a;
var c = a;
ref var d = ref b;
a++; b++; c++; d++;

// a, b, d は同じ
// a=3, b=3, c=1, d=3
Console.WriteLine($"a={a}, b={b}, c={c}, d={d}");

三項演算子とref

参考演算子の結果を参照で受け取ることもできます。

var a = 100;
var b = 200;
ref var c = ref a >= b ? ref a : ref b;
c++;

// b=201, c=201
Console.WriteLine($"b={b}, c={c}");

演算結果にも ref を付ける必要があります。ref a >= b ? ref a : ref b の先頭の ref は a にかかるのではなく演算結果にかかっています。つまり ref (a >= b ? ref a : ref b); と同じです。

ref戻り値

引数だけでなく、参照を戻り値として返すことも可能です。

ただし、参照戻り値はメソッド内のローカル変数として宣言されたものにはできません。これはローカル変数だとメソッドが終了した後、破棄されてデータがなくなるかもしれないからです。

同じような理由で非同期処理などでも使えないみたいです。

以下のコードは、複数の引数から最大値の参照を返す例です。

// 最大値の参照を返す関数
static ref int GetMaxRef(ref int a, ref int b)
{
    if (a >= b) return ref a;
    else return ref b;
}

// 参照を取得して更新する
var a = 100; var b = 200;
ref var maxRef = ref GetMaxRef(ref a, ref b);
maxRef++;

// a=100, b=201, maxRef=201
Console.WriteLine($"a={a}, b={b}, maxRef={maxRef}");

結果を参照ローカル変数として受け取るには関数の呼び出し部分の前にも ref を付けます。

参照は右辺だけでなく左辺に直接もってくることもできます。

GetMaxRef(ref a, ref b)++;

上記のようにすると、参照結果に対して直接インクリメントができます。こうすると ref を付ける数が減ります。

foreach の ref

普通の配列を foreach で走査すると値がコピーされますが、Span<T> を foreach で走査するときに ref キーワードを付与すると、参照を受け取ることができます。

foreach でも更新ができるみたいです。

var x = new int[] { 0, 0, 0, 0, 0 };
foreach (ref var value in x.AsSpan())
{
    value++;
}

以上、C# の ref キーワードまとめでした。

参考URL

C#カテゴリの最新記事