はじめに
C#における、参照まわりの機能が充実してきたので、
変数の渡し方について自分なりにまとめてみました。
C#の機能
変数の渡し方について、関連しそうな機能をピックアップしてみます。
- C#1.0
-
ref
引数 『参照引数』 -
out
引数 『出力参照引数』
-
- C#4.0
- オプション引数
- C#7.0
-
ref
変数 -
ref
戻り値 -
out
変数宣言
-
- C#7.2
-
in
引数 『入力参照引数』 -
ref readonly
変数 -
ref readonly
戻り値 -
readonly
構造体
-
- C#7.3
-
ref
ローカル変数の再割り当て
-
当記事執筆以降に追加された機能をいくつか挙げておきます。
- C#11
-
ref
構造体のref
フィールド -
scoped
修飾子
-
- C#12
-
ref readonly
引数
-
一部の機能について、記事内にコメントを入れています。
引数の型
引数に指定する、ref
,out
はC#の初期からある機能なので今更ですが、C#7.2よりin
が指定できるようになりました。
評価 | 値の変更 | 参照の変更 | 備考 | |
---|---|---|---|---|
(なし) | 値渡し | 可 | - | |
ref |
参照渡し | 可 | 可 | |
out |
参照渡し | 可/必須 | 可 | 未初期化の変数を渡せる |
in |
参照渡し | 不可 | 可 |
void DoSomething(in int x1, ref int x2, out int x3) {
// x1 = 1; // 書けない
x2 = 2; // 書ける(参照先=呼び出し元の値が書き換わる)
x3 = 3; // 書かなくてはならない(参照先=呼び出し元の値が書き換わる)
}
void DoSomethingIn(in int x1, in int x2) {
x1 = ref x2; // 参照先=呼び出し元には影響しないので書ける
}
void DoSomethingOut(out int x1, out int x2) {
x1 = 1;
x2 = 2;
x1 = ref x2;
x1 = 3;
// 呼び出し元には、x1=1, x2=3 が出力(out)される
}
// in引数はオプション引数にできる
void DoSomethingIn(in int x1 = default) {
}
in
は、主に大きな構造体を扱う場合の「コピー」を避けるために使われます。
「読み取り専用」の引数だと思うと、少し奇妙な動作をするので注意が必要。以下の理由などから、単に引数を読み取り専用にしたいだけの目的では使用しないほうが良いと思います。
-
in
の有無でシグネチャが変わる (影響大!) -
ref
をつけた参照の再代入は可能
C#11では、参照引数および参照変数に scoped
修飾できるようになりました。
参照のスコープがこのメソッド内に限られることを指定します。
C#12では、ref readonly
修飾できるようになりました。
in
と似た挙動をしますが、一時変数や式の受け取りを拒否します。(呼び出し元に参照があることを強制します。)
戻り値の型
C#7.0で ref
が、C#7.2で ref readonly
が、それぞれ戻り値の型に指定できるようになりました。
評価 | 備考 | |
---|---|---|
(なし) | 値返し | |
ref |
参照返し | 参照先の値を変更できる |
ref readonly |
参照返し | 参照先の値を変更できない |
ref int DoReturnRef(in int x1, ref int x2, out int x3) {
x3 = 1;
int a = 1;
ref int b = ref a;
// return ref b; // これは返せない
//if (x1 > x2 && x1 > x3) {
// return ref x1; // 読み取り専用の参照(in)を読み取り可能な参照(ref)では返せない
//}
if (x2 > x3) {
return ref x2;
}
return ref x3;
}
ref readonly int DoReturnRefReadonly(in int x1, ref int x2, out int x3) {
x3 = 1;
int a = 1;
ref readonly int b = ref a;
// return ref b; // これは返せない
if (x1 > x2 && x1 > x3) {
return ref x1;
}
if (x2 > x3) {
return ref x2;
}
return ref x3;
}
void Do() {
int a = 1, b = 2;
DoReturnRef(in a, ref b, out int c) = 100;
}
スコープを辿って問題が無いかどうかはコンパイラがチェックしてくれますので、メモリを破壊するような参照にはなりません。
ただ、ref
する階層が深くなるとロジックを追いづらくなるので、そこは要注意。
気を付けて使えば、ロジックをよりスマートに書くこともできると思います。
C#12では、引数を in
の代わりに ref readonly
で修飾することで、ref readonly
戻り値が参照であることを保証できます。
変数の型
C#7.0 より、変数の型にref
を、 C#7.2 より、変数の型にref readonly
を付けることができます。
ref
,ref readonly
に指定された変数は、「参照」として扱われます。
評価 | 値の変更 | 参照の変更 | 備考 | |
---|---|---|---|---|
(なし) | 値 | 可 | - | |
readonly |
値 | 不可 | - | ローカル変数には指定不可 |
ref |
参照 | 可 | 可 | |
ref readonly |
参照 | 不可 | 可 |
int a = 1, b = 2;
ref readonly int x = ref a;
x = ref b;
// x = 100; // これはNG
ref int y = ref a;
y = ref b;
y = b;
// readonly int z = a; // これもNG
ローカル変数では、ref readonly
とは書けるけれども、readonly
とは書けない不思議な状態となっています。
どんな時に使うか
- 大きな構造体に対して、
ref readonly
in
を使用する- 併用:対象の構造体は
readonly
構造体とする。 - 効果:構造体全体のコピーが発生しないため、高速化できる。
- 併用:対象の構造体は
-
ref
戻り値のプロパティやインデクサを持たせたクラスを使用する- 効果:複雑な構造、条件のオブジェクトの書き換えを簡単にできる、かも。
上記二つは、設計思想的には割と対極で、
- 『構造体をきちんとカプセル化してimmutableにする』
- 『カプセル化を緩くして外部からの中身の書き換えをしやすくする』
なのですが、どちらも『結果として処理の負荷を軽減する』ための使い方だと思います。
まとめ
C#7.xの機能は使いどころが難しいものが多いですが、きちんと理解しておくことで必要な場面で選択肢として思い浮かべられるようにしたいところです。