C Sharpの基礎 - タプル

提供:MochiuWiki : SUSE, EC, PCB
ナビゲーションに移動 検索に移動

概要

専用のクラスや構造体を定義するほどのことでもないが、複数のオブジェクトを一時的に1つに纏めたい時がある。

例えば、今までは、メソッドは1つのオブジェクトしか返せなかったので、複数の結果を返す場合は、複数の結果を格納するためだけに型を定義したり、
あるいは、out引数を使用していた。

.NET Framework 4.7で導入されたValueTuple構造体(System名前空間)とVisual Studio 2017(C# 7)で導入されたタプル構文を使えば、
簡単に複数の結果を一時的に1つに纏められる。
ここでは、それらの新しい機能のうち、メソッドから複数の結果を返す方法について解説する。


TupleクラスとValueTuple構造体の比較

Tupleクラス ValueTuple構造体 匿名型
名前付け ×
引数・戻り値 ×
型の種類 クラス 構造体 クラス
メンバの種類 プロパティ フィールド プロパティ
不変性 イミュータブル ミュータブル イミュータブル


名前付け

ValueTuple構造体と匿名型は、メンバに名前を付けることができる。
ただし、匿名型と異なり、ValueTuple構造体は初期化時の変数名等から名前を割り当てることはできない。

 var hoge = 1;
 var piyo = "yy";
 
 // ValueTuple構造体
 var vt1 = (x:1, y:"vt");
 var vt2 = (x:hoge, y:piyo);
 (int x, string y) vt3 = (1, "ccc");  //  明示的に、名前付きのValueTuple構造体として宣言できる
 
 // 匿名型
 var a1 = new {x = 1, y = "a"};
 var a2 = new {hoge, piyo};


引数・戻値

Tupleクラスと同様、ValueTuple構造体も引数・戻値として指定できる。

 // 引数・戻り値にTupleクラスを指定する
 public Tuple<int, string> GetTuple(Tuple<int, string> t) => t;
 
 // 引数・戻り値にValueTuple構造体を指定する
 public (int, string) GetValueTuple1((int, string) vt) => vt;
 
 // 引数・戻り値に名前付きValueTuple構造体を指定する
 public (int x, string y) GetValueTuple2((int a, string b) vt) => vt;


型の種類

Tupleクラスは、null許容であるため、nullで初期化できる。
ValueTuple構造体は、null非許容の値型であるため、nullで初期化できない。

 // ValueTuple構造体は、null非許容の値型であるため、nullを '(int, string)' に変換できません
 (int, string) vt = null;
 
 // Tupleクラスは、nullで初期化できる
 Tuple<int, string> t = null;


メンバの種類

ValueTuple構造体のメンバはフィールドであるため、以下の2つの特徴がある。

  • out / refキーワードの引数に渡すことができる。
  • プロパティを前提としたFWでは使用できない。(例. バインディング等)
 var vt = (1, "a");
 Add(ref vt.Item1);
 
 var t = Tuple.Create(1, "a");
 // Add(ref t.Item1);  // コンパイルエラー
                       // プロパティまたはインデクサをout / refキーワードの引数に渡すことはできない


不変性

Tupleクラスのプロパティは、イミュータブル(不変)である。
ValueTuple構造体のメンバはフィールドであるため、ミュータブルである。

 // ValueTuple構造体  割り当て可能
 var vt = (1, "a");
 vt.Item1 = 2;
 
 // Tupleクラス  割り当て不可
 var t = Tuple.Create(1, "a");
 // t.Item1 = 2;  // コンパイルエラー
                  // Tuple<int, string>.Item1(プロパティまたはインデクサ)は読み取り専用であるため、割り当てることはできない
 
 // 匿名型  割り当て不可
 var a = new { Item1 = 1, Item2 = "a" };
 // a.Item1 = 2;  // コンパイルエラー
                  // <anonymous type: int Item1, string Item2>.Item1(プロパティまたはインデクサ)は読み取り専用であるため、割り当てることはできない


なお、ValueTuple構造体のフィールドはミュータブルであるが値型でもあるため、以下のように記述できない。

 (int i, string s)[] arr = { (1, "a"), (2, "b"), (3, "c"), (4, "d")};
 
 foreach(var tuple in arr)
 {
    // tuple.i = 2;  // コンパイルエラー
                     // ValueTuple構造体のフィールドは、foreach文の繰り返し変数であるため変更できない
 }



TupleクラスとValueTuple構造体

Tupleクラスは.NET Fremework 4.0からあり、ValueTuple構造体は.NET Fremework 4.7で導入された。
これら2つはよく似ており、例えば、2つの値をまとめて返すメソッドを次のコードのように記述できる。

 // Tupleクラス(.NET Fremework 4.0以降)
 static Tuple<int, int> OldExchange(int x, int y) => Tuple.Create(y, x);
 
 // ValueTuple構造体(.NET Fremework 4.7以降)
 static ValueTuple<int, int> NewExchange(int x, int y) => ValueTuple.Create(y, x);
 static (int, int) NewExchange(int x, int y) => (y, x);
 static (int x, int y) NewExchange(int x, int y) => (y, x);



複数の値をまとめてメソッドから返す

ValueTuple構造体を返すメソッドは、タプル構文を使用することで簡潔に記述できる。

例えば、ValueTuple<int, int>型は、タプル構文では(int, int)と同義である。
また、タプルを生成するValueTuple.Create(y, x)は、タプル構文では(y, x)と記述するだけでよい。

以下の例では、タプル構文でタプル名を付けずに2つの値を返している。
以下のように記述する場合、戻り値を受け取る側では、タプル内の要素をItem1およびItem2という既定の名前で参照できる。

 // ValueTuple構造体を直接記述する
 static ValueTuple<int, int> NewExchange1(int x, int y) => ValueTuple.Create(y, x);
 
 // 上と同義の処理を、タプル構文を使用して記述する
 static (int, int) NewExchange1(int x, int y) => (y, x);


以下のように、タプル名を付けることにより、戻り値を受け取る側ではタプル内の要素をタプル名で参照できる。

なお、タプル名はTupleElementNames属性(System.Runtime.CompilerServices名前空間)により実現されているが、
この属性を直接コーディングすることはできない。(タプル構文が強制される)

以下の例では、 タプル構文でタプル名を付けて2つの値を返している。
戻り値を受け取る側では、タプル内の要素をxおよびyというタプル名で参照できる。

 static (int x, int y) NewExchange2(int x, int y) => (y, x);


適切なタプル名を付けておくことで、可読性が良くなる。


複数の値をメソッドから受け取る

ValueTuple構造体を返すメソッドの基本的な使用方法は、以下のように記述する。

 // タプル名なし(既定の名前Item1 / Item2等でアクセスする)
 var tResult1 = NewExchange1(2, 3);
 WriteLine($"tResult1.Item1={tResult1.Item1}, tResult1.Item2={tResult1.Item2}");
 
 // タプル名付き
 var tResult2 = NewExchange2(2, 3);
 WriteLine($"tResult2.x={tResult2.x}, tResult2.y={tResult2.y}");


また、以下のように、受け取る時に別のタプル名を付けることもできる。

 // タプル名なしで返すメソッド
 (int X, int Y) tResult1 = NewExchange1(2, 3);
 WriteLine($"tResult1.X={tResult1.X}, tResult1.Y={tResult1.Y}");
 
 // タプル名付きで返すメソッド
 (int M, int N) tResult2 = NewExchange2(2, 3);
 WriteLine($"tResult1.X={tResult1.M}, tResult1.Y={tResult1.N}");


C# 7以降では、タプルを受け取る時に分解することも可能である。(タプルの要素を個別の変数に代入する)

 var (p, q) = NewExchange1(2, 3);
 WriteLine($"p={p}, q={q}");


また、(p, _)のようにして、タプルとして返された値で使用しない要素を_で削除することもできる。(_は書き込み専用の変数)

 var (p, _) = NewExchange1(2, 3);
 WriteLine($"p={p}");