C#の参照型の値渡しに気をつけろ

背景

  • C++のコードをC#にしたら想定外の挙動になった

C++のコード

#include <iostream>
#include <vector>
#define rep(i, n) for (int i = 0; i < (n); ++i)
using namespace std;
using ll = long long;
using p = pair<int, int>;

void foo(vector<int> A)
{
    if (A.size() == 10)
    {
        return;
    }

    A.push_back(1);
    foo(A);

    // print
    rep(i, A.size())
    {
        cout << A[i];
    }
    cout << endl;
}

int main()
{
    foo(vector<int>(1, 1));
}

出力結果

1111111111
111111111
11111111
1111111
111111
11111
1111
111
11

C#のコード

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main(string[] args)
    {
        Program obj = new Program();
        obj.Foo(new List<int> { 1 });
    }

    void Foo(List<int> A)
    {
        if (A.Count == 10)
        {
            return;
        }

        A.Add(1);
        this.Foo(A);

        // print
        foreach (var e in A)
        {
            Console.Write(e);
        }
        Console.WriteLine();
    }
}

出力結果

1111111111
1111111111
1111111111
1111111111
1111111111
1111111111
1111111111
1111111111
1111111111

!?

Aの値が更新されてしまっている

改善する

  • どうやらListは参照型らしい
  • refをつけなければ値渡しになる
  • つまり参照型の値渡しなのでオブジェクトを渡すときと同じ挙動になる
  • したがって、渡された値まるごと置き換えた場合は値がコピーされるが(値渡し)、中身の一部を変えた場合(配列に要素追加するなど)は呼び出し元も変更されてしまう(共通のレジスタを参照)
  • ということで渡された値まるごと置き換えて値渡しにしてあげればよい
using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main(string[] args)
    {
        Program obj = new Program();
        obj.Foo(new List<int> { 1 });
    }

    void Foo(List<int> A)
    {
        var B = new List<int>(A);
        if (B.Count == 10)
        {
            return;
        }

        B.Add(1);
        this.Foo(B);

        // print
        foreach (var e in A)
        {
            Console.Write(e);
        }
        Console.WriteLine();
    }
}

出力結果

1111111111
111111111
11111111
1111111
111111
11111
1111
111
11

まとめ

C#

  • Listは参照型
  • 引数に渡すときはrefを付けない限り値渡しで渡される
  • 参照型の値渡しはオブジェクトを渡すときと同じ挙動になる

C++

  • vectorは値型
  • &を付けない限り値渡しで渡される