LoginSignup
13

C# 7.0 で導入されたタプルが可変な値型なので、注意点を説明しようとしたら驚きの結果に

Posted at

TL;DR(この記事の要約)

  • C# 7.0 で導入されたタプル(ValueTuple 構造体)は値型なので、代入などの際にコピーが発生する。
  • かつ、可変(ミュータブル)であり、変更がコピー元に波及しない。
  • しかし、以下の例のように関数やインデクサの戻り値がタプル(ValueTuple 型)であった場合、直接変更ができないようになっていた。
(int, int) func() => (10, 20);
func().Item1 = 300; // コンパイルエラー

はじめに

C# 7.0 でタプルが導入されました。周回遅れどころか4周くらい遅れていますが、自分も使ってみたいと思い試してみました。

詳しい説明は他の記事に譲るとして、この記事ではタプルが値型 (ValueType) であることによって、どのような注意点が発生するかについて説明しようと思います。タプルについて知らない方は以下の記事などが参考になると思います。

引数で渡すとコピーが作成される

さて、タプルを使うにあたって注意する点ですが、まず、ひとつは引数で渡した場合です。引数で渡すとタプルのコピーが作成され、呼び出し元の変数が更新されません。

public static void Main()
{
    var tuple = (10, 20);
    Console.WriteLine($"tuple: {tuple}");

    UpdateTuple(tuple); // 引数としてタプルを渡す

    Console.WriteLine($"Updated?? : {tuple}"); // 更新されていない
}

public static void UpdateTuple((int, int) tuple)
{
    tuple.Item1 = 300;
    tuple.Item2 = 400;
    Console.WriteLine($"Updated! : {tuple}");
}
tuple: (10, 20)
Updated! : (300, 400)
Updated?? : (10, 20)

このように、参照型だと思ってタプルを使うと思わぬバグを生み出してしまう可能性があります。

ただし、これは int 型などのプリミティブ型でも同じことなので、慣れれば違和感もなくなるかもしれません。必要なら参照渡しなどを検討しましょう。

コレクションの TValue にタプルを指定する場合

また、コレクションにタプルを格納する場合にも注意が必要です。まず、次のような List に MyPoint 型を格納するプログラムを考えてみます。

public static void Main()
{
    var list = new List<MyPoint>();
    list.Add(new MyPoint(10, 20));
    list[0].X = 300;
    Console.WriteLine(list[0]); // => (300, 20)
}

public class MyPoint
{
    public int X;
    public int Y;

    public MyPoint(int x, int y)
    {
        X = x;
        Y = y;
    }

    public override string ToString()
    {
        return $"({X}, {Y})";
    }
}

このように、list[0].X = 300; というコードにより list[0] そのものが更新されています。

この MyPoint 型を (int, int) 型に置き換えてみるとどうなるでしょうか。

var list = new List<(int X, int Y)>();
list.Add((10, 20));
list[0].X = 300; // => エラー CS1612	変数ではないため、'List<(int X, int Y)>.this[int]' の戻り値を変更できません
Console.WriteLine(list[0]);

え!?

image.png

どええええええええ!!?

どえぇえ.png

ということで、C# でコレクションに格納されている値型のメンバを直接変更しようとするとエラーになるようです。そこまで対策済みとは恐れ入りました……

結果的に、タプルがイミュータブルである場合と実質的に同様のエラーを発生させてくれているように思えました。

ですので、例えば以下の DictionaryWithDefault のようなコレクションでも、default 式で生成された構造体が更新されてしまうことを(完全ではありませんが)防ぐことができます。

public static void Main()
{
    var dict = new DictionaryWithDefault<int, (int Num, bool Flag)>();
    dict[123] = (10, true);

    // ↓エラー! dict[456] は default 式で生成されているため、実体がコレクションに格納されていない
    // dict[456].Num = 200;

    // 変数に代入して無理やり書き換えることはできるが、コレクションには反映されない
    var temp = dict[456];
    temp.Num = 200;

    Console.WriteLine(dict[123]); // => (10, True)
    Console.WriteLine(dict[456]); // => (0, False)
}

public class DictionaryWithDefault<TKey, TValue> : Dictionary<TKey, TValue>
{
    public new TValue this[TKey key]
    {
        get { return TryGetValue(key, out TValue t) ? t : default; }
        set { base[key] = value; }
    }
}

参照型では default が null になるので、このような問題は発生しません。

まとめ

タプルがミュータブルであることに関する全体的な話は以下の記事が詳しいかと思います。

もしこの記事に何か誤りなどありましたら指摘して頂けると嬉しいです。

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