LoginSignup
14
10

変数の渡し方 ref out in

Last updated at Posted at 2018-08-29

はじめに

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とは書けない不思議な状態となっています。

どんな時に使うか

  1. 大きな構造体に対して、 ref readonly in を使用する
    • 併用:対象の構造体は readonly構造体とする。
    • 効果:構造体全体のコピーが発生しないため、高速化できる。
  2. ref戻り値のプロパティやインデクサを持たせたクラスを使用する
    • 効果:複雑な構造、条件のオブジェクトの書き換えを簡単にできる、かも。

上記二つは、設計思想的には割と対極で、

  1. 『構造体をきちんとカプセル化してimmutableにする』
  2. 『カプセル化を緩くして外部からの中身の書き換えをしやすくする』

なのですが、どちらも『結果として処理の負荷を軽減する』ための使い方だと思います。

まとめ

C#7.xの機能は使いどころが難しいものが多いですが、きちんと理解しておくことで必要な場面で選択肢として思い浮かべられるようにしたいところです。

14
10
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
10