LoginSignup
13
8

【C#】(値型と参照型の)値渡しと参照渡し -引数を渡す

Last updated at Posted at 2021-12-24

Qiita Advent Calendar 2回目の参加です。よろしくお願いします。
C#を習得するときかなり大コケをした「値渡しと参照渡し」について振り返り、まとめました。

値型の値と参照・参照型の値と参照 

以下、全ての画像はイメージです。アドレスやメモリの配置などはそれっぽいものとして受け取ってください。
<値型> 値=「1」参照=「0x2742」
値型の値は「1 や 10 などの実体(インスタンス)」です。
image.png
<参照型> 値=「0x2904」参照=「0x2742」
参照型の値は「1 や 10 などの実体(インスタンス)」ではなく参照先にある実体の「アドレス」を持っています。
image.png
この他にポインターという型が存在しますが、今回は扱いません。▶ ​アンセーフ コード、ポインター型、関数ポインター

値渡しと参照渡しについて

「値渡し」「参照渡し」渡し方は2種類あります。ですが、C#には型の種類が2つあるため組み合わせとしては2×2=4パターンとなります。値型を渡すことが値渡し、参照型を渡すことが参照渡し、と呼ばない点が大きなポイントです。C#では参照型と参照渡しの概念は別物です。

  • 値型の値渡し
  • 参照型の値渡し
  • 値型の参照渡し
  • 参照型の参照渡し

image.png

値渡しと参照渡しの違い

名前の通り、を渡すのが「値渡し」参照を渡すのが「参照渡し」です。先程の画像を例に挙げます。
値型 paramValue値渡しだと「10」参照渡しだと「0x2742」を渡します。
image.png

参照型 paramRef値渡しだと「0x2904」参照渡しだと「0x2742」を渡します。
image.png

参照渡しで出来ること

  • 参照渡しで引数を渡す
  • 参照戻り値
  • ref ローカル変数
  • ref readonly
  • ref 条件式 などなど…

出来ることは色々ありますが(説明できるかどうか自信がないので)今回は 参照渡しで引数を渡す をピックアップします。

参照渡しのキーワード

C#で参照渡しを使用する際は、以下のキーワードを使用します。

1. ref

        private void ReferenceTypes_PassByReference_ref(ref int[] param)
        {
            param = new int[] { 1000, 2000, 3000 };
        }

        public void Call()
        {
            // 変数の初期化をしないとエラー
            var paramRef2 = new int[] { 1, 2, 3 };
            ReferenceTypes_PassByReference_ref(ref paramRef2);
        }
  • 呼び出し時に ref キーワードが必須
  • メソッド内で書き換え可能(必須ではない)
  • 変数の初期化必須

2. in 

        private void ReferenceTypes_PassByReference_in(in int[] param)
        {
            // メソッド内で代入をするとエラーになる 書き換え不可
            // param = new int[] { 1000, 2000, 3000 };
        }

        public void Call()
        {
            // 変数の初期化をしないとエラー
            var paramRef = new int[] { 1, 2, 3 };
            ReferenceTypes_PassByReference_in(paramRef);
        }
  • 呼び出し時に in キーワードは不要( in を付けて呼び出してもエラーではない)
  • メソッド内で書き換え不可(読み取り専用)
    image.png
  • 変数の初期化必須

3. out

        private void ReferenceTypes_PassByReference_out(out int[] param)
        {
            // メソッド内で代入をしなければエラーになる 書き換え必須
            param = new int[] { 1000, 2000, 3000 };
        }

        public void Call()
        {
            // 変数の初期化をしなくてもエラーが出ない
            int[] paramRef;
            ReferenceTypes_PassByReference_out(out paramRef);
        }
  • 呼び出し時に out キーワードが必須
  • メソッド内での書き換え必須
    image.png
  • 変数の初期化をしなくてもよい(必須ではない)

値渡し/参照渡しで引数を渡す

この記事内で「書き換え」という言葉を使用しますが、これは参照自体の値を変更することを指します。
値型でいえば param = 10; 参照型でいえば param = new int[] { 1000, 2000, 3000 }; などのコードが該当します。
param[1] = 10; param.Name = "AAA"; など、参照先のオブジェクトに属するデータを変更するコード(配列の要素を変更、クラスのメンバーを変更)は該当しません。要素やメンバーを変更すると、参照渡しでなくとも変更は反映されるからです。
参考: 参照型パラメーターの引き渡し (C# プログラミング ガイド) より

参照型の変数には、そのデータが直接入っているのではなく、そのデータへの参照が含まれています。値(渡し)で参照型パラメーターを渡す場合、クラスメンバの値など参照先のオブジェクトに属するデータを変更することができます。ただし、参照自体の値を変更することはできません。参照自体の値を変更する場合は ref または out キーワードを使用してパラメーターを渡します。

前述の通り ref in out どのキーワードでも「参照渡し」はできますが、「参照渡し+書き換える」ことが可能なのは refout です。
以下の表は値渡しで書き換えた場合と、参照渡し(ref または out)で書き換えた場合の比較表です。

「値渡し」で書き換え 「参照渡し(ref/out)」で書き換え
値型 呼び出し元の引数(実引数)に変更が反映されない 呼び出し元の引数(実引数)に変更が反映される
参照型 呼び出し元の引数(実引数)に変更が反映されない 呼び出し元の引数(実引数)に変更が反映される

引数を渡して書き換える① 値型の値渡し・値型の参照渡し

サンプルコードでは ref を使用します。

値型の値渡し・参照渡し
        private void ValueTypes_PassByValue(int param)
        {
            param = 1000;
        }

        private void ValueTypes_PassByReference_ref(ref int param)
        {
            param = 1000;
        }

        public void Call()
        {
            // 値型の値渡し
            var paramValue = 1;
            Console.WriteLine($"呼び出し前 {paramValue}"); // 呼び出し前 1

            ValueTypes_PassByValue(paramValue);
            Console.WriteLine($"呼び出し後 {paramValue}"); // 呼び出し後 1

            // 値型の値渡し
            var paramValue2 = 1;
            Console.WriteLine($"呼び出し前 {paramValue2}"); // 呼び出し前 1

            ValueTypes_PassByReference_ref(ref paramValue2);
            Console.WriteLine($"呼び出し後 {paramValue2}"); // 呼び出し後 1000
        }

参照渡しは仮引数の param に実引数である paramValue2 の参照が渡されるため、その先にある実体に書き換えの結果が反映されます。=呼び出し元に書き換え結果が反映されます。
image.png

引数を渡して書き換える② 参照型の値渡し・参照型の参照渡し

サンプルコードでは ref を使用します。

参照型の値渡し・参照渡し
        private void ReferenceTypes_PassByValue(int[] param)
        {
            param = new int[] { 1000, 2000, 3000 };
        }

        private void ReferenceTypes_PassByReference_ref(ref int[] param)
        {
            param = new int[] { 1000, 2000, 3000 };
        }

        public void Call()
        {
            // 参照型の値渡し
            var paramRef = new int[] { 1, 2, 3 };
            Console.WriteLine($"呼び出し前 {string.Join(", ", paramRef)}"); // 呼び出し前 1, 2, 3

            ReferenceTypes_PassByValue(paramRef);
            Console.WriteLine($"呼び出し後 {string.Join(", ", paramRef)}"); // 呼び出し後 1, 2, 3
            
            // 参照型の参照渡し
            var paramRef2 = new int[] { 1, 2, 3 };
            Console.WriteLine($"呼び出し前 {string.Join(", ", paramRef2)}"); // 呼び出し前 1, 2, 3

            ReferenceTypes_PassByReference_ref(ref paramRef2);
            Console.WriteLine($"呼び出し後 {string.Join(", ", paramRef2)}"); // 呼び出し後 1000, 2000, 3000
        }

image.png
参照型の参照渡しも同じく仮引数の param に実引数である paramRef2 の参照が渡されるため、その先にある実体を書き換えていることになります。そのため、呼び出し元に書き換え結果が反映されます。参照型の参照渡しは実体がヒープに存在するため複雑に見えますが、値型の参照渡しと動作は同じです。
image.png

おまけ 参照型で値渡しをしたはずなのに、引数が変更されてしまった?

それは、参照型であるクラスのメンバーを変更しているからなのでは……?

値渡しも参照渡しもバッチリ🎶と思い込んでいたとき、値渡しでクラスのメンバーを変更するコードを書いたのですが「どうして引数に変更が反映されているんだろう??」と混乱しました。なぜか参照型の特徴を無視して「値渡しなら呼び出し元の引数は絶対に変わらない」と思い込んでいたようです。
参照型パラメーターの引き渡し (C# プログラミング ガイド)「参照型の変数には、そのデータが直接入っているのではなく、そのデータへの参照が含まれています。値(渡し)で参照型パラメーターを渡す場合、クラスメンバの値など参照先のオブジェクトに属するデータを変更することができます」と、書いてあります。その通りです。

class SampleClass
{
    public string Name { get; set; } = "STAR";
    public string[] Names { get; set; } = new string[] { "A", "B", "C" };
}

class MemberChangeClass
{
    public void Call()
    {
        var sample = new SampleClass();
        Console.WriteLine($"呼出前 {sample.Name} {sample.Names[1]}"); // 呼出前 STAR B
        Sample(sample);
        Console.WriteLine($"呼出後 {sample.Name} {sample.Names[1]}"); // 呼出後 MOON BBB
    }

    private void MemberChange(Sample sample)
    {
        // 値渡しでもメンバーの書き換えは反映される
        sample.Name = "MOON";
        sample.Names[1] = "BBB";
    }
}

まとめ

  • 渡し方 
    C#では値型も参照型も明示的に指定しなければ値渡し。逆に言えば、値型も参照型も明示的に指定すれば参照渡し。

  • 参照渡しで出来ること
    引数を渡す(メソッド内で書き換え可能なのは ref out)、戻り値を返す、ローカル引数を設定する、など。

  • 参照渡しのキーワード 
    ref in out というキーワードがある。どれも参照渡しが可能だがそれぞれ制約がある。

  • メソッドに引数を渡して書き換えたときの違い
    値渡しをすると呼び出し元の引数(実引数)に変更が反映されない。参照渡しをすると呼び出し元の引数(実引数)に変更が反映される。メソッドの仮引数に「値」が渡されているのか、それとも「参照」が渡されているのか、という違いがあるため。

記事内で使用したサンプルコードはこちら▶ CsharpRefSample

参考URL

最後まで見ていただきありがとうございました。

13
8
0

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
13
8