Deep Insider の Tutor コーナー
>>  Deep Insider は本サイトからスピンオフした姉妹サイトです。よろしく! 
Build Insiderオピニオン:岩永信之(3)

Build Insiderオピニオン:岩永信之(3)

次期C# 7: 複数データをまとめるための言語機能、タプル型

2016年2月9日

メソッドが複数の値を戻す場合など、複数のデータを緩くまとめて、扱いたい場合はよくある。C#の次バージョンではこれを簡潔に記述するための機構として「タプル型」が導入される。

岩永 信之
  • このエントリーをはてなブックマークに追加

 現在、C#への機能追加に当たって、いくつかのテーマが設けられている。その中でも大きなものの1つが「データ処理」である。データ処理というと、C# 3.0でLINQと関連して多くの言語機能が取り入れられたが、まだまだ検討すべきことは多く残されている。

 今回は、そんなデータ関連の提案の1つであるタプル型について紹介していこう。

タプルとは

 タプル(tuple)という単語は、倍数を表す「double, triple, quadruple, ...」などを一般化したN-tupleという言葉に由来する。単純に「複数のもの」という意味の言葉だ。つまり、「データを複数束ねたもの」程度の意味であり、「タプル型」はかなり「緩い」型を意味する。C# 7で提案されているものは図1に示すようなものである*1

図1: C# 7のタプル型(提案段階)
図1: C# 7のタプル型(提案段階)
  • *1 .NET Framework 4でTupleクラスが追加されているが、次期C# 7のタプル型はC#言語の機能として追加されるという違いがある。

多値戻り値

 タプルの一番の用途は、複数の値を戻り値として返すメソッドである。例えば、数値列を与えて、その和と個数を同時に計算して返すメソッドTallyを考えてみよう。現状のC#でもいくつかの手段がある。

  • out引数の利用
  • 専用の型を作る
  • Tupleクラス(System名前空間)のような汎用の型を使う

 1つ目はout引数を利用するもので、コードはリスト1に示すようになる。

C#
static void Tally(IEnumerable<int> items, out int sum, out int count)
{
  sum = 0;
  count = 0;
  foreach (var x in items)
  {
    sum += x;
    count++;
  }
}
リスト1: out引数を使ったTallyメソッド

 確かにこれで複数の戻り値を返せるが、out引数は使い勝手が悪く、できれば避けたい。特に致命的なのは、非同期メソッドで使えないことである。例えばもし、Tallyメソッドの中にawait演算を必要とするような処理が挟まっていた場合、リスト2のようには書けない。

C#
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: out引数の問題(非同期メソッドで利用不可)。このコードはコンパイルできない

 2つ目は、リスト3に示すように、専用の型を作って返す方法である。

C#
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;
}
リスト3: 専用の型を使ったTallyメソッドの実装

 この方法の問題は、このためだけに型を作るのが適切かどうか、過剰ではないかという問題である。この戻り値の型は、TallyResult(=Tallyメソッドの結果)としか言いようがない。他に候補を挙げるなら、SumAndCount(=sumcountを持つ何か)など、メンバーを見れば分かるような意味のない名前になるだろう。強い型付けの良さは「意味の分かる名前を付けられる」ことであって、「意味のないものにまで名前を付ける」ことではない。

 型名に意味がないのなら、リスト4に示すように、Tupleクラスのような汎用の型で置き換えるという方法を取ることもできる。

C#
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;
}
リスト4: 汎用的な型を使ったTally実装

ただし、このコードは実際にはコンパイルできない。Tuple型のメンバーItem1Item2は書き換え不能である。このコードはあくまで概念の説明用。

 この場合は逆に、sumcountといった意味のある名前まで消えてしまう。リスト3で戻り値の型名に意味がないといったのは、あくまでも「メンバー名を見れば分かるから冗長」という話である。メンバー名まで消えるのではさすがに情報が少なすぎる。Item1Item2のどちらが和で、どちらが個数かわからなくなるようでは実用に耐えないだろう。

 このような背景の中、C# 7でタプル型が提案されている。タプル型を使うとリスト5のような書き方ができる。リスト5にある(int sum, int count)というような書き方がタプル型である。

C#
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;
}
リスト5: C# 7で導入予定のタプル型を使ったTallyメソッドの実装

 すなわち、タプル型は、メンバー名だけで十分にその型の性質を表せ、型自体の名前は不要な場合に使える型である。

 最初にout引数で説明したような非同期処理における問題も、タプル型であれば解消できる。つまり、リスト6に示すようなコードが書ける。

C#
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;
}
リスト6: 非同期メソッドの戻り値でのタプル型利用

引数とタプル型

 メソッドの引数と戻り値は、入力と出力であり、表裏の関係にある。例えば、戻り値として返したものは、そのまま別のメソッドの引数に与えられることがある。先ほどの例でいうと、Tallyメソッドが「和と個数を返す」ものなのに対して、「和と個数を受け取る」メソッドが考えられる。リスト7に示すような平均値の計算が分かりやすい例だろう。

C#
static double Average(int sum, int count) => sum / (double)count;
リスト7: Tallyの表裏。和と個数を受け取るメソッドの例

後述のリスト8~11でこのメソッドを使用する。

 そして、C# 7のタプル型は多値戻り値を最大の目的として導入されたものであるため、表裏の関係にある引数との類似が多く見られる。偶然そうなったものではなく、設計思想として、タプル型は引数と似せてある。

 例えば、リスト8やリスト9に示すように、メソッドへの引数の受け渡しと同じ構文でタプルを構築する。引数の受け渡しに位置指定渡し(リスト8)と名前指定渡し(リスト9)があるのと同様、タプルも位置指定と名前指定ができる。

C#
(int sum, int count) x = (100, 10);
Average(100, 10);
リスト8: 名前なしタプル構築と位置指定引数
C#
var x = (sum: 100, count:  10);
Average(sum: 100, count: 10);
リスト9: 名前付きタプル構築と名前指定引数。この場合はxに対して型推論が利く

 また、リスト10に示すように、引数への分解(splatting)も自動的に行われる。もちろん、リスト11に示すように、タプル型の戻り値を別のメソッドに直接渡すこともできる。

C#
(int sum, int count) x = (sum: 100, count:  10);
Average(x);

Average((sum: 100, count: 10)); // このコードでも同じ意味
Average(sum: 100, count: 10);   // 最終的に、このコードと同じような呼び出しになる
リスト10: タプルから引数への分解
C#
var result = Tally(new[] { 1, 2, 3, 4, 5 });
var a1 = Average(result);

var a2 = Average(Tally(new[] { 1, 2, 3, 4, 5 }));
リスト11: タプル型の戻り値を別のメソッドに直接渡す

Tallyメソッドはリスト5、Averageメソッドはリスト7で定義したもの。

匿名型(C# 3.0)とタプル型

 C#には、3.0のころから匿名型(=匿名クラス)というタプル型とよく似た機能が提供されている。ただ現状の匿名型にはいくつかの問題があり、タプル型ではそれを解消しようとしている。また、匿名型自体にも手を入れて、現状の問題を解消できないか検討中である。結果的に、匿名型とタプル型はより一層似た機能になるが、その違いについても触れておこう。

アセンブリをまたげない問題

 C# 3.0の匿名型を知っている人なら、タプル型を戻り値に使えることにまず驚くかもしれない。タプル型は、言語機能的に匿名型に近いものである。そして、匿名型は、フィールドやメソッドの引数、戻り値には使えない。匿名型をおさらいしておくと、リスト 12のようなコードから、コンパイル時に(少なくとも今の実装では)リスト13のようなクラスを生成する機能である。

C#
var p = new { X = 10, Y = 20 };
リスト12: 匿名型の利用例
C#
internal class Anonymous1
{
  public int X { get; }
  public int Y { get; }

  public Anonymous1(int x, int y)
  {
    X = x;
    Y = y;
  }
}
リスト13: 匿名型からコンパイラーによって生成されるクラス

実際にはGetHashCodeなどのメソッドも生成される。また、名前は通常のC#ではあり得ない記号交じりのものになる。

 問題は、同じ{ X = 10, Y = 20 }という書き方で得られる匿名型であっても、アセンブリごとに異なるクラスが生成され、それらの間の変換はできない点である。その結果、匿名型はアセンブリの間をまたげず、フィールドやメソッド引数、戻り値に使えなくなっている。

 この問題はタプル型では解消する予定である。

タプル型の実装

 タプル型の具体的な実装方法だが、今のところ、リスト14に示すような、汎用の型ValueTuple構造体への展開になりそうだ。

C#
// 展開前
static (int sum, int count) Tally(IEnumerable<int> items) { /* 省略 */ }

// 展開結果
[TupleNames("sum", "count")]
static ValueTuple<int, int> Tally(IEnumerable<int> items) { /* 省略 */ }
リスト14: タプル型の実装方法

 先ほど説明した通り、現在の匿名型のような「クラスの生成方式」ではアセンブリをまたげないという問題がある。この問題を解消するための案はいくつかあり、それぞれに一長一短があるが、現状ではリスト14の方式が最有力である。すなわち、汎用のValueTuple構造体に展開した上で、メンバー名は属性として残すという方式である。

 ここで、ValueTuple構造体はリスト15のような実装となっている。

C#
namespace System
{
  public struct ValueTuple<T1, T2>
  {
    public readonly T1 Item1;
    public readonly T2 Item2;
  }
}
リスト15: ValueTuple構造体の実装

実際にはGetHashCodeEqualsなどのメソッドも持っている。フィールドがreadonlyになるかどうかもまだ変更の可能性がある。

 用途を絞らない場合、実測に基づく結論として、タプル型のようなものは参照型(つまりクラス)の方がよいとされている。ただ、C# 7で提案されているタプル型は、メソッドの戻り値として使い、すぐにメンバーを分解してそれぞれ別変数で受け取るというような用途が想定されていて、この用途に絞る場合には値型(つまり構造体)の方が有利だろうと判断されている。その結果、今(.NET 4以降)すでにあるTupleクラス(System名前空間)ではなく、新たにValueTuple構造体を導入することになった。

明示可能な匿名型

 前節で説明したような、汎用の型+属性で名前を残すという実装方法は、匿名型に対しても応用できるだろう。そこで、匿名型に対してもリスト16のような書き方を許そうという提案も出ている。

C#
// 匿名型の型の明示
static { int X, int Y } p = new { X = 1, Y = 2 };

// 展開結果
[TupleNames("X", "Y")]
static Tuple<int, int> p = Tuple.Create(1, 2);
リスト16: 型を明示的に書け、アセンブリをまたげる匿名型

 この方法であれば、タプル型と同様に、匿名型も型名を明示できるし(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
表1: 匿名型とタプル型の比較

 値の分解(deconstruction)については、詳細は次回以降、パターン マッチングの回で説明する。

 まず、匿名型はオブジェクト初期化子(プロパティ)、タプル型はコンストラクター(引数)というように、何の類推になっているかという差がある。これは単に書き方の問題である。機能的には参照型か値型かという差になるだろう。

 この参照型か値型かという差だけのために、こんな似て非なる構文を導入するかどうかという問題もある。また、匿名型の改善には、既存の匿名型を使っているコードを壊さずに修正できるかどうかという課題も待っている。その結果、匿名型の改善は優先度が低めになっている。

まとめ

 主に多値戻り値を返すための型として、タプル型が提案されている。タプル型は、(int x, int y)というような書き方で、複数のデータをまとめた型を作る機能である。多値戻り値という用途上、その表裏の関係にあるメソッド引数とよく似た書き方になっている。ただし、タプル型には、アセンブリをどうやってまたぐかや、今ある類似の機能である匿名型との兼ね合いなど、いくつかの課題もある。

 この既知の課題さえクリアできれば、非常に便利な機能であることは間違いない。非同期メソッドと多値戻り値の相性の悪さが改善するし、無意味な型を増やす必要がなくなる。型名をなくす緩さを嫌う人もいるだろうが、強い型付けの良さは「意味の分かる名前を付けられる」ことであって、「意味のないものにまで名前を付ける」ことではない。

岩永 信之(いわなが のぶゆき)

岩永 信之(いわなが のぶゆき)

 

 

 ++C++; の中の人。C# 1.0がプレビュー版だった頃からC#によるプログラミング入門を公開していて、C#とともに今年で15年になる。最近の自己紹介は「C#でググれ」。

 

 

 

 

※以下では、本稿の前後を合わせて5回分(第1回~第5回)のみ表示しています。
 連載の全タイトルを参照するには、[この記事の連載 INDEX]を参照してください。

Build Insiderオピニオン:岩永信之(3)
1. オープンソースのC#/Roslynプロジェクトで見たこと、感じた教訓

日本を代表する「C#(でぐぐれ)」の人、岩永信之氏によるコラムが遂に登場。今回はオープンソースで開発が行われているC#と開発者の関わり方について。

Build Insiderオピニオン:岩永信之(3)
2. 次期C# 7: 式の新機能 ― throw式&never型/式形式のswitch(match式)/宣言式/シーケンス式

C# 6が出てまだ間もないが、すでに次バージョン「C# 7」についての議論が進んでいる。その中で提案されている「式」に関する新機能を取り上げる。

Build Insiderオピニオン:岩永信之(3)
3. 【現在、表示中】≫ 次期C# 7: 複数データをまとめるための言語機能、タプル型

メソッドが複数の値を戻す場合など、複数のデータを緩くまとめて、扱いたい場合はよくある。C#の次バージョンではこれを簡潔に記述するための機構として「タプル型」が導入される。

Build Insiderオピニオン:岩永信之(3)
4. 次期C# 7: 型に応じた分岐や型の分解機能、パターンマッチング

オブジェクトの型を階層的に調べて分岐処理を行いたい場合がある。そのための機能として、C#の次バージョンでは「パターンマッチング」が追加される。

Build Insiderオピニオン:岩永信之(3)
5. 次期C# 7: パターンマッチングの内部的な仕組み

C#の次バージョンに追加される「パターンマッチング」の挙動(パターンマッチングがどう展開されるのかや、その実現方法)について説明する。

サイトからのお知らせ

Twitterでつぶやこう!