概要
プログラミング言語での値の受け渡しの方法には 値渡し(pass by value)と参照渡し(pass by reference)という2つの方法があります。
C# では、値の受け渡しは基本的に値渡しになります。
しかし、ref
や out
といったキーワードを使うことで参照渡しにすることが出来ます。
ポイント
-
値渡し: メソッド内で引数の値を書きかえても、呼び出し元には影響しない。
-
参照渡し(ref): メソッド内での値の書き換えの影響が呼び出し元に伝搬する。
-
out: 特殊な参照渡し。戻り値以外にも値を返したいとき(複数の値を返したいとか)に使う。
値の受け渡し
値の受け渡しが発生する場所は何カ所かあります。例えば以下のような場所です。
- 変数から変数
- 変数から引数
- 戻り値から変数
var x = 1;
var y = x; // x から y に値を渡す
static void VariableToParameter()
{
var x = 1;
F(x); // 変数 x から、F の引数 x に値を渡す
}
static void F(int x)
{
}
static void ReturnToVariable()
{
var x = F(); // F の戻り値から変数 x に値を渡す
}
static int F() => 1;
受け渡しの方法には、以降で説明する値渡しと参照渡しという2種類の受け渡し方法があります。
C#では、通常(特に何もつけないと)、値渡しになります。 一方、以下のようにして、参照渡しを使うこともできます。
- C# 6以前では、引数の受け渡しの際に
ref
もしくはout
という修飾子を付けることで参照渡しができます - C# 7以降では、変数間の受け渡しや戻り値でも
ref
修飾子を付けることで参照渡しができます
ちなみに、C#には受け渡しの値渡しと参照渡しの他に、型の区分として値型と参照型というものもあります。結果的に、「値型の値渡し」、「値型の参照渡し」、「参照型の値渡し」、「参照型の参照渡し」というような組み合わせもできるので注意が必要です。
値渡し
しばらく、C# 6以前でも使える「引数の受け渡し」で説明して行きましょう。
引数の値渡し(call by value)とは、メソッドを呼び出す際に値のコピーを渡すことを言います。 C# では普通にメソッドを定義すると、その引数は値渡しになります。 例えば、以下のようなプログラムがあったとします。
using System;
class ByValueTest
{
static void Main()
{
int a = 100;
Console.Write("{0} → ", a);
Test(a);
Console.Write("{0}\n", a);
}
static void Test(int a)
{
a = 10; // メソッド内で値を書き換える。
}
}
Test
メソッドの変数 a
には Main
メソッドの a
のコピーが渡されています。
したがって、図1のように、
Test
内で変数 a
を書き換えても
Main
内の a
の値は変わりません。
そのため、このプログラムの実行結果は以下のようになります。
100 → 100
同様に、参照型の変数を値渡しする場合、図2, 3に示すように、参照情報をコピーして渡すことになります。
参照渡しの引数
引数の参照渡し(call by reference)とは、メソッドを呼び出す際に値の参照情報を渡すことを言います。
C# では、ref
引数、in
引数、out
引数という3種類の参照渡しがあります。
参照引数(ref 引数)
C# で単に「参照引数」という場合、ref
引数を指します。
後述するin
(読み取り専用)やout
(戻り値的に使う引数)のような制約がなく、読み書き両方できるものです。
以下の例のように、メソッドの引数に ref
キーワードを付けることでその引数は参照渡しになります。
using System;
class ByReferenceTest
{
static void Main()
{
int a = 100;
Console.Write("{0} → ", a);
Test(ref a);
Console.Write("{0}\n", a);
}
static void Test(ref int a)
{
a = 10; // メソッド内で値を書き換える。
}
}
Test
メソッドの変数 a
は Main
メソッドの a
に対する参照になっています。
したがって、図4のように、
Test
内で変数 a
を書き換えた場合、
Main
内の a
の値も同時に書き換わります。
そのため、このプログラムの実行結果は以下のようになります。
100 → 10
同様に、参照型の変数を値渡しする場合、図5に示すように、参照情報をさらに参照することになります。
ここで1つ注意しなければいけないのは、
メソッドの呼び出し側にも ref
キーワードをつける必要があるということです。
参照渡しを行うと、メソッドの中で値が書き換えられる可能性があります。
(というよりも、書き換える必要があるから参照渡しにする。)
引数が参照渡しであることを知らずにメソッドを呼び出してしまうと、
プログラマの意図しないところで値が書き換わってしまう可能性があり、
これはバグの原因になります。
そのため、呼び出し側でも明示的に ref
キーワードを付けなければならいないという制約をつけることによって、
知らないうちに参照渡しのメソッドを呼び出してしまう危険性をなくしています。
サンプル
using System;
class ByRefferanceTest
{
static void Main()
{
int[] array = new int[]{4, 6, 1, 8, 2, 9, 3, 5, 7};
BubbleSort(array);
foreach(int a in array)
{
Console.Write("{0,3}", a);
}
}
/// <summary>
/// バブルソートを使って配列を整列する
/// </summary>
static void BubbleSort(int[] array)
{
for(int i=0; i<array.Length-1; ++i)
for(int j=array.Length-1; j>i; --j)
if(array[j-1] > array[j])
Swap(ref array[j-1], ref array[j]);
}
/// <summary>
/// a と b の値を入れ替える
/// </summary>
static void Swap(ref int a, ref int b)
{
int tmp = a;
a = b;
b = tmp;
}
}
1 2 3 4 5 6 7 8 9
入力参照引数 (in 引数)
Ver. 7.2
C# 7.2 から、「参照渡しだけども読み取り専用」というような引数の渡し方ができるようになりました。
「入力用」ということを示すように、in
キーワードを使います。
(in
を使うのは、C# 1.0の頃からある out
引数(次節で説明)との対比もあります。)
using System;
public partial class Program
{
static void F(in int x)
{
// 読み取り可能
Console.WriteLine(x);
// 書き換えようとするとコンパイル エラー
x = 2;
}
// 補足: in 引数はオプションにもできる
static void G(in int x = 1)
{
}
static void Main()
{
int x = 1;
// ref 引数と違って修飾不要
F(x);
// 明示的に in と付けてもいい
F(in x);
// リテラルに対しても呼べる
F(10);
// 右辺値(式の計算結果)に対しても呼べる
int y = 2;
F(x + y);
}
}
(int
みたいな型にin
引数を使ってもメリットは皆無なんですが、サンプルということでご容赦ください。
後述しますが、大き目の構造体に対して使うべき機能です。)
in
引数は、書き換えできないことがコンパイラーによって保証されています
(書き換えようとするとコンパイル エラーを起こします)。
意図せず書き換わってしまう心配がないので、ref
引数と違って以下ようなことが認めらています。
F(x)
というように、修飾なしで呼ぶ-
F(10)
というように、リテラルを引数として渡す- 既定値を与えてオプション引数にすることもできる
F(x + y)
というように、右辺値(式の計算結果)を引数として渡す
ちなみに、
F(in x)
というように、呼び出し側で in
修飾を明示することもできます。
以下のような呼び分けをできるようにするために使います。
// 値渡しと in 引数でオーバーロードできる
static void F(int x) { }
static void F(in int x) { }
static void Main()
{
int x = 1;
// (※ 古いバージョンのコンパイラーだとコンパイルできないので注意)
// F(int) の方を呼ぶ
F(x);
// F(in int) の方を呼ぶ
F(in x);
}
※コンパイラーのバージョン2.7以降書けるようになりました。
「書き換えないけども参照で渡す」というのは、
大きめの構造体を使う際に役立ちます。
「参照渡しの活用」や「値型の性能」などで触れていますが、
大きめの構造体を値渡し(コピーが発生)すると、結構大きな負担が発生します。
そういう場合に in
引数が有用です。
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
引数にした効果が結構現れます。
注意: in 引数を使ってもコピーが発生する場合
詳しくは「readonly の注意点」で説明しますが、構造体に対してreadonly
を使うと、無駄にコピーが発生してしまうことがあります。
readonly
なものに対してメソッドを呼ぶ際、呼び出し側は「メソッド内部で値が書き換わっていない」という保証を知る由がないため、
メソッドを呼んだ時点で無条件にコピーを作ります。
この問題は、以下のように、in
引数でも起こります。readonly struct
を使えば回避できる点もreadonly
フィールドと同様です。
// 作りとしては 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();
}
この、前者(NoReadOnly
構造体の方)の場合に発生するコピーは、コード上は目に見えません。
だからこそ気づきにくいバグになりがちで、
問題視され、「隠れたコピー」(hidden copy)と呼ばれています。
出力引数 (out 引数)
参照渡しを使うと、メソッド内からメソッド外にある変数を書き換えることができます。
これを、メソッドの戻り値代わりに使うこともできます。
特に、複数の戻り値を返す場合に有効な手段です※。
ただ、ref
修飾子を使った参照引数では、戻り値として使うには以下のようないくつかの問題があります。
using System;
class Program
{
static void Main()
{
int a = 0; // この 0 という値には意味はないけど、必須
int b = 0; // 同上
MultipleReturns(ref a, ref b); // a, b を
Console.Write("{0}\n", a);
}
static void MultipleReturns(ref int a, ref int b)
{
a = 10; // a を初期化
// 本当は b も初期化してやらないといけないけど、忘れててバグってる
}
}
(※C# 6以前では、複数の戻り値を返す唯一の手段でした。C# 7以降ではタプル型というものを使って複数の戻り値を返すことができるようになっています。)
問題を要約すると以下の2点です
-
呼び出し元で、特に意味のない値で変数を初期化しておかなければならない
- メソッドの中で必ず上書きする想定なので、無駄な初期化になる
- メソッドの中で代入を忘れてしまってもコンパイル エラーにならない
そこで、戻り値として使いたい場合(メソッド内で変数を初期化する予定である場合)、
以下のように out
修飾子を用いて、出力用の参照引数であることを明示してやります。
using System;
class ByValueTest
{
static void Main()
{
int a;
Test(out a); // out を使った場合、変数を初期化しなくてもいい
Console.Write("{0}\n", a);
}
static void Test(out int a)
{
a = 10; // out を使った場合、メソッド内で必ず値を代入しなければならない
}
}
10
out
キーワードを用いて宣言された引数は参照渡しになります。
ref
キーワードとの違いは、上述のとおり、
- メソッド呼び出し前に初期化する必要がなくなる
- メソッド内で必ず値を割り当てなければいけない
の2点です。
サンプル
メソッドで複数の値を返したい場合、 戻り値では1つしか値を返せないので出力変数を使います。
using System;
class OutTest
{
/// <summary>
/// コンソールから係数を入力して2次方程式の根を計算し、出力する。
/// </summary>
static void Main()
{
string line = Console.ReadLine();
string[] token = line.Split(' ');
double a = double.Parse(token[0]);
double b = double.Parse(token[1]);
double c = double.Parse(token[2]);
Console.Write("{0}x^2 + {1}x + {2} = 0\n", a, b, c);
double x, y;
int type;
CalcRoot(a, b, c, out type, out x, out y);
if(type == 0) Console.Write("x = {0}, {1}\n", x, y);
else if(type == 1) Console.Write("x = {0} ±i {1}\n", x, y);
else Console.Write("x = {0}\n", x);
}
/// <summary>
/// 2次方程式 ax^2 + bx + c = 0 の根を求める
/// </summary>
/// <param name="a">2次の係数</param>
/// <param name="b">1次の係数</param>
/// <param name="c">定数項</param>
/// <param name="type">根のタイプ。0:実数根2つ、-1:重根1つ、1:虚数根</param>
/// <param name="x">根1(虚数根の場合、根の実部)</param>
/// <param name="y">根2(虚数根の場合、根の虚部)</param>
static void CalcRoot(
double a, double b, double c,
out int type, out double x, out double y)
{
b /= 2;
double d = b * b - a * c;
if(d < 0)
{
type = 1;
x = -b / a;
y = Math.Sqrt(-d) / a;
return;
}
if(d > 0)
{
type = 0;
double t1 = -b;
double t2 = Math.Sqrt(d);
x = (t1 + t2) / a;
y = (t1 - t2) / a;
return;
}
type = -1;
x = -b / a;
y = x;
}
}
出力変数宣言
Ver. 7
C# 7で、出力引数を受け取るのと同時に式中で変数を宣言できるようになりました。 これを出力変数宣言(out variable declaration。あるいは、略して out-var)と呼びます。
以前は、出力引数で値を受け取るためには、メソッドなどの呼び出しよりも前に変数を宣言しておく必要がありました。 例えば以下のようになります。
static int? ParseOrDefault(string s)
{
int x;
return int.TryParse(s, out x) ? x : default(int?);
}
これに対して、C# 7では、以下のような書き方ができるようになります。
式の中で変数 x
を宣言しつつ、出力引数の値を受け取っています。
static int? ParseOrDefault(string s)
{
return int.TryParse(s, out int x) ? x : default(int?);
}
ちなみに、var
を使った型推論もできます。
static int? ParseOrDefault(string s)
{
return int.TryParse(s, out var x) ? x : default(int?);
}
この例では、C# 6以前の書き方では、変数宣言ステートメントが必須で、式1つにまとめることができませんでした。
一方、C# 7以降の書き方ならば1つの式で済んでいます。
C# 6で導入された =>
を使った形式でメソッドを書くことができます。
static int? ParseOrDefault(string s) => int.TryParse(s, out var x) ? x : default(int?);
出力変数宣言で作った変数のスコープは、概ね、その式を囲っているブロック内になります。 つまり、式の直前に変数を宣言したのと同じスコープになります。
using System;
struct Point
{
public int X { get; set; }
public int Y { get; set; }
public void GetCoordinate(out int x, out int y)
{
x = X;
y = Y;
}
}
class Program
{
static void Main()
{
// x, y のスコープはこのブロック内
// この辺りで x, y という名前の変数は作れない
var p = new Point { X = 1, Y = 2 };
p.GetCoordinate(out var x, out var y);
// 以下のような書き方をしたのと同じ
// int x, y;
// p.GetCoordinate(out x, out y);
// この行から下で x, y を使える
Console.WriteLine($"{x}, {y}");
}
}
正確にいうともう少し複雑なルールになっていますが、詳細については「式の中で変数宣言」を参照してください。
in も out も内部的には ref
C# コンパイラーとしてはin
引数やout
引数をref
引数と区別していますが、
.NET の型システムのレベルでは実は区別がありません。
.NET 的にはin
引数もout
引数もref
引数扱いになっています。
そのため、以下のような不便があります。
- オーバーロードの区別に使えない
- 共変・反変にできない
まず、ref
、in
、out
だけの違いのオーバーロードは作れません。
例えば以下のコードではF
、G
、H
のいずれもコンパイル エラーになります。
void F(ref int x) { }
void F(in int x) { }
void G(ref int x) { }
void G(out int x) => x = 0;
void H(in int x) { }
void H(out int x) => x = 0;
もう1つは、in
引数やout
引数を持つインターフェイスやデリゲートには変性を指定しません。
入力にしか使わない型引数は反変(in
制約)に、
出力にしか使わない型引数は共変(out
制約)にできます。
この条件に沿って考えるなら本来、in
引数は反変、out
引数は共変にできるはずです。
ところが、 .NET の型システム上はin
引数・out
引数はref
引数と同等のものなので、
「入力/出力にしか使わない」という判定ができません。
以下のようなコードはコンパイル エラーになります。
interface Contravariance<in T>
{
// 普通の引数は共変
void M(T x);
// 本来できてもいいはずなものの、.NET 的には無理
void M(in T x);
}
interface Covariance<out T>
{
// 普通の戻り値は反変
T M();
// 本来できてもいいはずなものの、.NET 的には無理
void M(out T x);
}
ちなみに、最新のコンパイラーでin
引数を使ったメソッドを作って、
それを古いコンパイラー(Visual Studio 2017 15.4以前)で使おうとするとref
引数扱いされます。
(実際のところ、in
引数は、ref
引数にIsReadOnly
属性が付いているだけ。)
参照引数の制限
別項で少し話していますが、参照はスタック上でしか使えません。
参照引数もこの制限に引っかかります。
その結果、参照引数(ref
、in
、out
いずれも)には以下のような制限があります。
例えば以下のコードはコンパイル エラーになります。
using System;
using System.Collections;
using System.Threading.Tasks;
class Program
{
void M(ref int x)
{
// クロージャに使えない
Action<int> a = i => x = i;
void f(int i) => x = i;
}
// イテレーターの引数に使えない
IEnumerable Iterator(ref int x)
{
yield break;
}
// 非同期メソッドの引数に使えない
async Task Async(ref int x)
{
await Task.Delay(1);
}
}