【C#】「参照型」を渡しても「参照渡し」にはならない件


はじめに

参照型とか参照渡しとかごっちゃになりそうだったので自分なりに整理してみた結果、表題の通り「『参照型』を渡したからといって、『参照渡し』にはなっていない」ということに気づきました。
以下に整理してみます。

「参照型」と「参照渡し」は違う

クラスなどの「参照型」ならば「参照渡し」になると思っていませんか?
というか僕は思っていたんですけどね。全く違いました。

「参照型」とは、変数が「インスタンスへの参照」を示す型のことです。
「参照渡し」とは、渡したいもの1の「参照」を渡す渡し方のことです。
「参照型」とは「型の性質」であり、「参照渡し」とは「インスタンスの引き渡し方法」なので、それらは互いに関係し合いません。
なので当然、「参照型」ならば「参照渡し」である、とはならないわけです。

「値型」と「値渡し」

対するものとして、「値型」と「値渡し」があります。
「値型」とは、変数が「インスタンスそのもの」を示す型のことです。
「値渡し」とは、渡したいもの1の「コピー」を渡す渡し方のことです。
そして、C#では基本的に「参照渡し」ではなく「値渡し」となります。
したがって、「参照型」のインスタンスを普通に渡した場合、「参照型の値渡し」となります。参照型だからといって、参照渡しにはなりません。

参照型の「値渡し」

「参照型の値渡し」とはどういうことかを上の説明に当てはめて考えてみると、《「変数が『インスタンスへの参照』を示す型」の変数を、「渡したいもののコピーを渡す方法で渡す」》ということになります。
この場合、「渡したいもの」とは「インスタンスへの参照」となりますので、「インスタンスへの参照のコピーを渡す」ということになりますね。

※本来、参照型の実体はヒープに置かれますがわかりやすくするためクラスAの中に書いています

参照型の「参照渡し」

C#では基本的に値渡しとなるわけですが、御存知の通りref修飾子を使えば「参照渡し」をすることもできます。
では、「参照型」に対して「参照渡し」、つまり「参照型の参照渡し」をするとどうなるのでしょうか?

また上の説明に当てはめて考えてみると、《「変数が『インスタンスへの参照』を示す型」の変数を、「『渡したいものの参照』を渡す方法で渡す」》ということになります。
この場合、「渡したいもの」とは「インスタンスへの参照」なので、「インスタンスへの参照の参照を渡す」ということになります。

つまり、もともと「インスタンスへの参照」だったものへのさらに深い2重参照をしていることになります。

このように、「参照型」でも「値渡し」「参照渡し」の2通りの渡し方で渡すことができ、それぞれ内部で異なる処理がされていることがわかりました。
では、「参照型を値渡しした場合」と、「参照型を参照渡しした場合」とで、実際の挙動はどう違うのでしょうか?

「参照型の値渡し」と「参照型の参照渡し」の挙動の違い

「参照型の値渡し」と「参照型の参照渡し」との違いとしては、渡った先でインスタンス自体を書き換えられるかどうかが挙げられます。

「参照型の値渡し」では、「インスタンスへの参照のコピー」が渡されるのでした。
参照とはいえコピーなので、それ自体を別のものに書き換えたところで、コピー元には何も影響がありません。

しかし、「参照型の参照渡し」では、「インスタンスへの参照の参照」が渡されるのでした。
渡されているものは「参照」なので、それを別のものに書き換えれば、当然元データもそのものに書き換えられてしまいます。

これが「参照型の値渡し」と「参照型の参照渡し」との挙動の違いになります。
では、実際にどのように挙動の違いがあるか試してみます。

挙動の違いを実際に試して実感する

「参照型の値渡し」と「参照型の参照渡し」との挙動の違いを、以下のソースコードで試してみます。
比較のために、「値型の値渡し」と「値型の参照渡し」も同時に挙動の違いを確認します。

class Program
{
    static void Main(string[] args)
    {
        //参照型と値型のインスタンスを4つずつ用意
        var vals = Enumerable.Repeat(0, 4)
                             .Select(_ => new ValueStruct("初期値"))
                             .ToArray();
        var refs = Enumerable.Repeat(0, 4)
                             .Select(_ => new ReferenceClass("初期値"))
                             .ToArray();

        //値型を値渡しでプロパティ書き換え
        OverwriteProperty(vals[0]);
        //値型を参照渡しでプロパティ書き換え
        OverwriteProperty(ref vals[1]);
        //値型を値渡しでインスタンス置き換え
        ReplaceInstance(vals[2]);
        //値型を参照渡しでインスタンス置き換え
        ReplaceInstance(ref vals[3]);

        //参照型を値渡しでプロパティ書き換え
        OverwriteProperty(refs[0]);
        //参照型を参照渡しでプロパティ書き換え
        OverwriteProperty(ref refs[1]);
        //参照型を値渡しでインスタンス置き換え
        ReplaceInstance(refs[2]);
        //参照型を参照渡しでインスタンス置き換え
        ReplaceInstance(ref refs[3]);

        foreach (var item in vals)
        {
            Console.WriteLine(item.Message);
        }
        foreach (var item in refs)
        {
            Console.WriteLine(item.Message);
        }

        Console.ReadKey();
    }

    static void OverwriteProperty(ValueStruct @struct)
    {
        @struct.Message = "値型を値渡しでプロパティ書き換えました";
    }
    static void OverwriteProperty(ref ValueStruct @struct)
    {
        @struct.Message = "値型を参照渡しでプロパティ書き換えました";
    }
    static void OverwriteProperty(ReferenceClass @class)
    {
        @class.Message = "参照型を値渡しでプロパティ書き換えました";
    }
    static void OverwriteProperty(ref ReferenceClass @class)
    {
        @class.Message = "参照型を参照渡しでプロパティ書き換えました";
    }

    static void ReplaceInstance(ValueStruct @struct)
    {
        @struct = new ValueStruct("値型を値渡しでインスタンス置き換えました");
    }
    static void ReplaceInstance(ref ValueStruct @struct)
    {
        @struct = new ValueStruct("値型を参照渡しでインスタンス置き換えました");
    }
    static void ReplaceInstance(ReferenceClass @class)
    {
        @class = new ReferenceClass("参照型を値渡しでインスタンス置き換えました");
    }
    static void ReplaceInstance(ref ReferenceClass @class)
    {
        @class = new ReferenceClass("参照型を参照渡しでインスタンス置き換えました");
    }
}

/// <summary>
/// 参照型
/// </summary>
class ReferenceClass
{
    public ReferenceClass(string message)
    {
        this.Message = message;
    }
    public string Message { get; set; }
}
/// <summary>
/// 値型
/// </summary>
struct ValueStruct
{
    public ValueStruct(string message)
    {
        this.Message = message;
    }
    public string Message { get; set; }
}

このソースコードが何をしているかというと、①「インスタンスの中身(プロパティ)の書き換え」と②「インスタンス自体の書き換え」という2つの操作を、「値型の値渡し」「値型の参照渡し」「参照型の値渡し」「参照型の参照渡し」という4種類の渡し方で試して、元データにどのような影響があるかを確認しています。

結果は以下のようになりました。「初期値」となっているものは、元データへの影響がないことを示しています。

初期値
値型を参照渡しでプロパティ書き換えました
初期値
値型を参照渡しでインスタンス置き換えました
参照型を値渡しでプロパティ書き換えました
参照型を参照渡しでプロパティ書き換えました
初期値
参照型を参照渡しでインスタンス置き換えました

この結果をまとめると以下のようになります。
※✕が書き換えできなかったことを示します

プロパティ書き換え インスタンス置き換え
値型 値渡し
値型 参照渡し
参照型 値渡し
参照型 参照渡し

値型の場合は想像通りですね。値渡しをすれば、渡った先で何をされようが元データには影響ありません。コピーが渡っているからですね。

参照型の場合は、値型と違って値渡しでもプロパティの書き換えが可能です。しかし、値渡しではインスタンス自体の書き換えはできなくなっています
これは、「値渡し」とはあくまでも「渡したいもののコピー」を渡す渡し方なので、「参照のコピー」に対して別のものに書き換えを行ったところで、「元の参照」には何も影響がないためです。
「参照型の参照渡し」を行うと、「渡したいものの参照」を渡すので、「参照の参照」が引き渡されるため、引き渡し先でインスタンス自体を書き換えると、元の参照にも影響が及ぶというわけですね。

さいごに

参照型と参照渡しの関係性について、ごちゃごちゃになっていたので自分なりに整理してみました。
情報が間違っていたらご指摘等お待ちしています。

[11/23追記]
早速ご指摘いただきました。
「変数」のことを「インスタンス」と表現してしまっていました。お詫びして修正します。


  1. 分かりやすくするため、「渡したいもの」と表現していますが、参照型の場合は