C#を勉強しなおしてみた(7)
参照渡し(ref/out)

どもです。

本投稿は、以下の投稿の続きモノです。

今回は、関数呼び出しの際の「参照渡し」について書きます。

0. 事前確認

以前の記事で書いた通り、C#の関数呼び出しでは、引数は「値渡し」です。「値渡し」の詳細はこの記事を確認してほしいのですが、引数の型が「参照型」であっても「値渡し」となります。呼び出された側で「値渡し」された「参照型」の内容(プロパティ)の変更は、呼び出し元にも反映されます。

1. 参照渡し

「C#の引数は値渡し」ということが、(簡単ではありますが)確認できたところで、本エントリの本題である「参照渡し」について書いていきます。

1.1. 「参照渡し」とは

「参照渡し」とは、引数を渡す際に「参照を渡す」ということです。読んで字の如く、そのままです。引数を参照渡しにする最も基本的な方法は、引数の型の前に「ref」を付与する、という方法です。具体的には、以下のようなコードになります。

void RefArgSample(ref SampleObj input)

引数を「参照渡し」とすることで、引数の型が「値型」であっても「参照」が渡されるようになります。それっぽく言うと、「『値型』の『参照渡し』」なります。

1.2. 「値型」の「参照渡し」

「値型」を「参照渡し」する場合について、書きます。

「値型」を「参照渡し」した場合、「値型」への「参照」が関数に渡されます

イメージ:


この場合、呼び出された関数内で引数の値を変更すると、呼び出し元にもその値が反映されます。

Console.WriteLine($"At caller : {nameof(input),8} = {input}");

RefArgSample(ref input);

Console.WriteLine($"At caller : {nameof(input),8} = {input}");

static void RefArgSample(ref int input)
{
	input++;

	Console.WriteLine($"At called : {nameof(input),8} = {input}");
}

このコードでは、呼び出された関数(RefArgSample())の引数(input)は、呼び出し元の変数(input)への参照となっています。そのため、RefArgSample()内でinputの値を更新すると、呼び出し元のinputの値も変更されます。C#の値型の変数に対して、C++のポインタや参照の引数と同じような処理を実行したい場合に、この「値型の参照渡し」を使用します。

実際にこのコードを実行すると、以下のような結果が得られます。



1.3. 「参照型」の「参照渡し」

次に、「参照型」の「参照渡し」です。

「参照型」の「参照渡し」は、(私の個人的な考え、見解ですが)あまり使わないため、深く理解しておく必要は無いと思っています。なぜなら、「『参照型』の『参照渡し』」と「『参照型』の『値渡し』」では、得られる効果、影響に差は無い(と、私は思っている)からです。

では、どのような場合に「『参照型』の『参照渡し』」を使用するのか?それは、「呼び出した関数側でnewした参照型のインスタンスを、引数を通じて呼び出し元に返す」場合です。

具体的なコードの例を示します。

Worker worker = new Worker()
{
	Id = 1,
	Name = "Sample Worker"
};

SampleMethod(ref worker);

Console.WriteLine($"{nameof(worker.Id),8} = {worker.Id}");
Console.WriteLine($"{nameof(worker.Name),8} = {worker.Name}");

static void SampleMethod(ref Worker worker)
{
	worker = new Worker()
	{
		Id = 100,
		Name = "Other worker information."
	};

	Console.WriteLine($"{nameof(worker.Id),8} = {worker.Id}");
	Console.WriteLine($"{nameof(worker.Name),8} = {worker.Name}");
}

class Worker
{
	public int Id { get; set; } = 0;

	public string Name { get; set; } = string.Empty;
}

このコードの実行結果は、以下のようになります。



コードと実行結果を比較してみると分かる通り、SampleMethod()内で引数inputに渡した値が、呼び出し元のinputにも反映されています。

「参照型」の変数を「参照渡し」した場合には、呼び出し元の変数が参照しているオブジェクトそのものを変更する(違うオブジェクトを参照するように変更する)ことができます。

1.4. 別の「参照型」の「参照渡し」

「参照型」の「参照渡し」には「ref」とは別に、「out」という別の修飾子があります。この修飾子は、「『参照型』を『参照渡し』」するということは同じですが、大きく違うのは「オブジェクトを確保する責任は、呼び出されたメソッドにある」ということです。

「ref」修飾子を付与した場合には、呼び出されたメソッド側には、変数にオブジェクトをセットする責任はありません。しかし、「out」修飾子を付与した場合には、呼び出されたメソッド側に、変数にオブジェクトをセットする責任があります。なお、メソッド内で変数にオブジェクトをセットしない場合には、ビルドエラーになります。

2. まとめ

今回は、C#の「参照渡し」について書きました。

「参照渡し」は、関数の引数に、オブジェクトの「値」ではなく、「参照」を渡します。そのため、変数の型が「値型」の場合に、メソッドの中で値を変更したい場合に使用します。変数の型が「参照型」の場合には、オブジェクトの内容を変更できます。「参照型」の場合には、メソッドの内部でnewしたオブジェクトを引数を介して呼び出し元に返したい場合に、「参照渡し」を指定します。

いずれにしても、内部で「値型」の値、「参照型」の参照するオブジェクト(そのもの)を変更する場合に、「参照渡し」を指定します。簡単ではありますが、このように理解しています。

今回のエントリが誰かの助けになれば幸いです。ではっ!

Ex. マイクロソフトのページに書いてあったこと

今回のエントリを書く際に、Microsoftのページを色々調べてみました。その中でこのページに、面白いこと、今回のエントリを根本からひっくり返すようなことが書いてありました。その内容は、

out または ref パラメーターは使用しないようにしてください。

です。この記載に続いて以下の記載もありました。

  • out または ref パラメーターを使用するには、ポインターの使用経験、値型と参照型の違いの理解、および複数の戻り値を持つメソッドの処理が必要になる。
  • out パラメーターと ref パラメーターの違いはあまり理解されていない。
  • 開発者全般に向けてフレームワークをデザインする場合、ユーザーが out パラメーターまたは ref パラメーターの扱い方を習得することは期待しないこと。

おっとぉ。驚きです。

また、次のような記載もあります。

参照型を参照で渡さないでください。

この記述については、「例外がある」とのことですが…それでもやっぱり…。

C#や.NET Frameworkは、プログラム/アプリケーションを組みやすい言語/環境ですが、中々にして奥が深いと感じさせる内容でした。

ではっ!