Build Insiderオピニオン:岩永信之(3)
次期C# 7: 複数データをまとめるための言語機能、タプル型
メソッドが複数の値を戻す場合など、複数のデータを緩くまとめて、扱いたい場合はよくある。C#の次バージョンではこれを簡潔に記述するための機構として「タプル型」が導入される。
現在、C#への機能追加に当たって、いくつかのテーマが設けられている。その中でも大きなものの1つが「データ処理」である。データ処理というと、C# 3.0でLINQと関連して多くの言語機能が取り入れられたが、まだまだ検討すべきことは多く残されている。
今回は、そんなデータ関連の提案の1つであるタプル型について紹介していこう。
タプルとは
タプル(tuple)という単語は、倍数を表す「double, triple, quadruple, ...」などを一般化したN-tupleという言葉に由来する。単純に「複数のもの」という意味の言葉だ。つまり、「データを複数束ねたもの」程度の意味であり、「タプル型」はかなり「緩い」型を意味する。C# 7で提案されているものは図1に示すようなものである*1。
- *1 .NET Framework 4でTupleクラスが追加されているが、次期C# 7のタプル型はC#言語の機能として追加されるという違いがある。
多値戻り値
タプルの一番の用途は、複数の値を戻り値として返すメソッドである。例えば、数値列を与えて、その和と個数を同時に計算して返すメソッドTally
を考えてみよう。現状のC#でもいくつかの手段がある。
out
引数の利用- 専用の型を作る
Tuple
クラス(System
名前空間)のような汎用の型を使う
1つ目はout
引数を利用するもので、コードはリスト1に示すようになる。
static void Tally(IEnumerable<int> items, out int sum, out int count)
{
sum = 0;
count = 0;
foreach (var x in items)
{
sum += x;
count++;
}
}
|
確かにこれで複数の戻り値を返せるが、out
引数は使い勝手が悪く、できれば避けたい。特に致命的なのは、非同期メソッドで使えないことである。例えばもし、Tally
メソッドの中にawait
演算を必要とするような処理が挟まっていた場合、リスト2のようには書けない。
static async Task TallyAsync(IEnumerable<int> items, out int sum, out int count)
{
sum = 0;
count = 0;
foreach (var x in items)
{
await SomeAsyncOperation();
sum += x;
count++;
}
}
|
2つ目は、リスト3に示すように、専用の型を作って返す方法である。
struct TallyResult
{
public int sum;
public int count;
}
static TallyResult Tally(IEnumerable<int> items)
{
var result = new TallyResult();
foreach (var x in items)
{
result.sum += x;
result.count++;
}
return result;
}
|
この方法の問題は、このためだけに型を作るのが適切かどうか、過剰ではないかという問題である。この戻り値の型は、TallyResult
(=Tally
メソッドの結果)としか言いようがない。他に候補を挙げるなら、SumAndCount
(=sum
とcount
を持つ何か)など、メンバーを見れば分かるような意味のない名前になるだろう。強い型付けの良さは「意味の分かる名前を付けられる」ことであって、「意味のないものにまで名前を付ける」ことではない。
型名に意味がないのなら、リスト4に示すように、Tuple
クラスのような汎用の型で置き換えるという方法を取ることもできる。
static Tuple<int, int> Tally(IEnumerable<int> items)
{
var result = Tuple.Create(0, 0);
foreach (var x in items)
{
result.Item1 += x;
result.Item2++;
}
return result;
}
|
ただし、このコードは実際にはコンパイルできない。Tuple
型のメンバーItem1
とItem2
は書き換え不能である。このコードはあくまで概念の説明用。
この場合は逆に、sum
、count
といった意味のある名前まで消えてしまう。リスト3で戻り値の型名に意味がないといったのは、あくまでも「メンバー名を見れば分かるから冗長」という話である。メンバー名まで消えるのではさすがに情報が少なすぎる。Item1
とItem2
のどちらが和で、どちらが個数かわからなくなるようでは実用に耐えないだろう。
このような背景の中、C# 7でタプル型が提案されている。タプル型を使うとリスト5のような書き方ができる。リスト5にある(int sum, int count)
というような書き方がタプル型である。
static (int sum, int count) Tally(IEnumerable<int> items)
{
var result = (sum: 0, count: 0);
foreach (var x in items)
{
result.sum += x;
result.count++;
}
return result;
}
|
すなわち、タプル型は、メンバー名だけで十分にその型の性質を表せ、型自体の名前は不要な場合に使える型である。
最初にout
引数で説明したような非同期処理における問題も、タプル型であれば解消できる。つまり、リスト6に示すようなコードが書ける。
static async Task<(int sum, int count)> TallyAsync(IEnumerable<int> items)
{
var result = (sum: 0, count: 0);
foreach (var x in items)
{
await SomeAsyncOperation();
result.sum += x;
result.count++;
}
return result;
}
|
引数とタプル型
メソッドの引数と戻り値は、入力と出力であり、表裏の関係にある。例えば、戻り値として返したものは、そのまま別のメソッドの引数に与えられることがある。先ほどの例でいうと、Tally
メソッドが「和と個数を返す」ものなのに対して、「和と個数を受け取る」メソッドが考えられる。リスト7に示すような平均値の計算が分かりやすい例だろう。
static double Average(int sum, int count) => sum / (double)count;
|
※後述のリスト8~11でこのメソッドを使用する。
そして、C# 7のタプル型は多値戻り値を最大の目的として導入されたものであるため、表裏の関係にある引数との類似が多く見られる。偶然そうなったものではなく、設計思想として、タプル型は引数と似せてある。
例えば、リスト8やリスト9に示すように、メソッドへの引数の受け渡しと同じ構文でタプルを構築する。引数の受け渡しに位置指定渡し(リスト8)と名前指定渡し(リスト9)があるのと同様、タプルも位置指定と名前指定ができる。
(int sum, int count) x = (100, 10);
Average(100, 10);
|
var x = (sum: 100, count: 10);
Average(sum: 100, count: 10);
|
また、リスト10に示すように、引数への分解(splatting)も自動的に行われる。もちろん、リスト11に示すように、タプル型の戻り値を別のメソッドに直接渡すこともできる。
(int sum, int count) x = (sum: 100, count: 10);
Average(x);
Average((sum: 100, count: 10)); // このコードでも同じ意味
Average(sum: 100, count: 10); // 最終的に、このコードと同じような呼び出しになる
|
var result = Tally(new[] { 1, 2, 3, 4, 5 });
var a1 = Average(result);
var a2 = Average(Tally(new[] { 1, 2, 3, 4, 5 }));
|
Tally
メソッドはリスト5、Average
メソッドはリスト7で定義したもの。
匿名型(C# 3.0)とタプル型
C#には、3.0のころから匿名型(=匿名クラス)というタプル型とよく似た機能が提供されている。ただ現状の匿名型にはいくつかの問題があり、タプル型ではそれを解消しようとしている。また、匿名型自体にも手を入れて、現状の問題を解消できないか検討中である。結果的に、匿名型とタプル型はより一層似た機能になるが、その違いについても触れておこう。
アセンブリをまたげない問題
C# 3.0の匿名型を知っている人なら、タプル型を戻り値に使えることにまず驚くかもしれない。タプル型は、言語機能的に匿名型に近いものである。そして、匿名型は、フィールドやメソッドの引数、戻り値には使えない。匿名型をおさらいしておくと、リスト 12のようなコードから、コンパイル時に(少なくとも今の実装では)リスト13のようなクラスを生成する機能である。
var p = new { X = 10, Y = 20 };
|
internal class Anonymous1
{
public int X { get; }
public int Y { get; }
public Anonymous1(int x, int y)
{
X = x;
Y = y;
}
}
|
実際にはGetHashCode
などのメソッドも生成される。また、名前は通常のC#ではあり得ない記号交じりのものになる。
問題は、同じ{ X = 10, Y = 20 }
という書き方で得られる匿名型であっても、アセンブリごとに異なるクラスが生成され、それらの間の変換はできない点である。その結果、匿名型はアセンブリの間をまたげず、フィールドやメソッド引数、戻り値に使えなくなっている。
この問題はタプル型では解消する予定である。
タプル型の実装
タプル型の具体的な実装方法だが、今のところ、リスト14に示すような、汎用の型ValueTuple
構造体への展開になりそうだ。
// 展開前
static (int sum, int count) Tally(IEnumerable<int> items) { /* 省略 */ }
// 展開結果
[TupleNames("sum", "count")]
static ValueTuple<int, int> Tally(IEnumerable<int> items) { /* 省略 */ }
|
先ほど説明した通り、現在の匿名型のような「クラスの生成方式」ではアセンブリをまたげないという問題がある。この問題を解消するための案はいくつかあり、それぞれに一長一短があるが、現状ではリスト14の方式が最有力である。すなわち、汎用のValueTuple
構造体に展開した上で、メンバー名は属性として残すという方式である。
ここで、ValueTuple
構造体はリスト15のような実装となっている。
namespace System
{
public struct ValueTuple<T1, T2>
{
public readonly T1 Item1;
public readonly T2 Item2;
}
}
|
実際にはGetHashCode
やEquals
などのメソッドも持っている。フィールドがreadonly
になるかどうかもまだ変更の可能性がある。
用途を絞らない場合、実測に基づく結論として、タプル型のようなものは参照型(つまりクラス)の方がよいとされている。ただ、C# 7で提案されているタプル型は、メソッドの戻り値として使い、すぐにメンバーを分解してそれぞれ別変数で受け取るというような用途が想定されていて、この用途に絞る場合には値型(つまり構造体)の方が有利だろうと判断されている。その結果、今(.NET 4以降)すでにあるTuple
クラス(System
名前空間)ではなく、新たにValueTuple
構造体を導入することになった。
明示可能な匿名型
前節で説明したような、汎用の型+属性で名前を残すという実装方法は、匿名型に対しても応用できるだろう。そこで、匿名型に対してもリスト16のような書き方を許そうという提案も出ている。
// 匿名型の型の明示
static { int X, int Y } p = new { X = 1, Y = 2 };
// 展開結果
[TupleNames("X", "Y")]
static Tuple<int, int> p = Tuple.Create(1, 2);
|
この方法であれば、タプル型と同様に、匿名型も型名を明示できるし(denotable)、アセンブリをまたぐこともできる。
匿名型とタプル型の差
タプル型も匿名型への改善も提案が通ったとすると、これらは非常に似たものとなる。比較を表1に示す。
匿名型 | タプル型 | |
---|---|---|
型の明示 (denotation) |
{ int X, int Y} | (int x, int y) |
値の作り方 (construction) |
var p = new { X = 1, Y = 2 }; | (int x, int y) p = (1, 2); var p = (x: 1, y: 2); |
値の分解 (deconstruction) |
let { X is int x } = p; | let (int x, *) = p; |
類似のもの | オブジェクト初期化子(プロパティ) new T { X = 1, Y = 2 } |
コンストラクター(引数) new T(1, 2) new T(x: 1, y: 2) |
値か参照か | 参照型Tuple | 値型ValueTuple |
値の分解(deconstruction)については、詳細は次回以降、パターン マッチングの回で説明する。
まず、匿名型はオブジェクト初期化子(プロパティ)、タプル型はコンストラクター(引数)というように、何の類推になっているかという差がある。これは単に書き方の問題である。機能的には参照型か値型かという差になるだろう。
この参照型か値型かという差だけのために、こんな似て非なる構文を導入するかどうかという問題もある。また、匿名型の改善には、既存の匿名型を使っているコードを壊さずに修正できるかどうかという課題も待っている。その結果、匿名型の改善は優先度が低めになっている。
まとめ
主に多値戻り値を返すための型として、タプル型が提案されている。タプル型は、(int x, int y)
というような書き方で、複数のデータをまとめた型を作る機能である。多値戻り値という用途上、その表裏の関係にあるメソッド引数とよく似た書き方になっている。ただし、タプル型には、アセンブリをどうやってまたぐかや、今ある類似の機能である匿名型との兼ね合いなど、いくつかの課題もある。
この既知の課題さえクリアできれば、非常に便利な機能であることは間違いない。非同期メソッドと多値戻り値の相性の悪さが改善するし、無意味な型を増やす必要がなくなる。型名をなくす緩さを嫌う人もいるだろうが、強い型付けの良さは「意味の分かる名前を付けられる」ことであって、「意味のないものにまで名前を付ける」ことではない。
岩永 信之(いわなが のぶゆき)
※以下では、本稿の前後を合わせて5回分(第1回~第5回)のみ表示しています。
連載の全タイトルを参照するには、[この記事の連載 INDEX]を参照してください。
1. オープンソースのC#/Roslynプロジェクトで見たこと、感じた教訓
日本を代表する「C#(でぐぐれ)」の人、岩永信之氏によるコラムが遂に登場。今回はオープンソースで開発が行われているC#と開発者の関わり方について。
2. 次期C# 7: 式の新機能 ― throw式&never型/式形式のswitch(match式)/宣言式/シーケンス式
C# 6が出てまだ間もないが、すでに次バージョン「C# 7」についての議論が進んでいる。その中で提案されている「式」に関する新機能を取り上げる。
3. 【現在、表示中】≫ 次期C# 7: 複数データをまとめるための言語機能、タプル型
メソッドが複数の値を戻す場合など、複数のデータを緩くまとめて、扱いたい場合はよくある。C#の次バージョンではこれを簡潔に記述するための機構として「タプル型」が導入される。
4. 次期C# 7: 型に応じた分岐や型の分解機能、パターンマッチング
オブジェクトの型を階層的に調べて分岐処理を行いたい場合がある。そのための機能として、C#の次バージョンでは「パターンマッチング」が追加される。