関連
タプルには、毛色の似た機能が2つあります。
これらとの関連・使い分けについても話しておきましょう。
匿名型との比較
タプルは、名前がない型という観点で言うと、匿名型と似ています。 しかし、「名前のない複合型」で説明したように、 出自・用途の違いから、内部実装は結構異なります。
以下の表のようになります。
タプル | 匿名型 | |
---|---|---|
主な用途 | 多値戻り値 | 部分的なメンバー抜き出し |
展開結果 | ValueTuple 構造体+属性 |
クラスの生成 |
型の種類 | 値型 | 参照型 |
見た目 | 引数の書き方に似ている | オブジェクト初期化子の書き方に似ている |
展開結果の差は用途の差から来ています。
タプルは戻り値として使います。publicなメンバーの型にも使うことになるので、ライブラリ間をまたげる必要があります。
ValueTuple
構造体に展開することで、ライブラリをまたいでも同じ構造体を参照する状態になります。
一方、匿名型は、ライブラリごとにそれぞれクラスを生成します(「匿名型」参照)。 同じ型に見えて、ライブラリをまたぐと別クラスになってしまいます。 このことから、匿名型は、メソッドの戻り値など、publicになりうる場所には書けません。 メソッド内のローカルな部分で完結して使う必要があります。
とはいえ、ValueTuple
構造体に展開では、前節での説明の通り、実行時に名前を紛失します。
dynamic
や、式木での利用にはタプルは向きません。この用途なら匿名型の方が向いています。
値型か参照型かも実装が異なりますが、これも、戻り値として使う、その後すぐに分解して使うという想定だと、値型の方が実行性能的に有利だからです。 用途が変われば最適な実装は変わります。
出力引数との比較
多値戻り値という用途だと、出力引数という手段もあります。 一般的に言うと、多値戻り値には今後タプルを使うのがおすすめです。 出力引数の方が煩雑な書き方になりがちだからです。
比較のために簡単な例を挙げてみましょう。まず、C# 6以前の出力引数を使ったものです。
static void F(Point p)
{
// 事前に変数を用意しないといけない/var 不可
int x, y;
// 1個1個 out を付けないといけない
Deconstruct(p, out x, out y);
Console.WriteLine($"{x}, {y}");
//非同期メソッドには使えない
}
// 1個1個 out を付けないといけない
static void Deconstruct(Point p, out int x, out int y)
{
// 1個1個代入
x = p.X;
y = p.Y;
}
1個1個out
修飾子を付けて回るのは結構な煩雑さです。
呼び出す前に別途変数宣言が必要なのも面倒です。
これらは単に煩雑なだけなので我慢すれば何とかなりますが、
致命的なのは非同期メソッドで使えないことです。
ちなみに、煩雑さはC# 7で多少マシになりました。出力変数宣言という構文が追加されて、以下のように書けます。
static void F(Point p)
{
// 変数の事前準備は不要に
// でも1個1個 out を付けないといけない
Deconstruct(p, out var x, out var y);
Console.WriteLine($"{x}, {y}");
//非同期メソッドには相変わらず使えない
}
// 1個1個 out を付けないといけない
static void Deconstruct(Point p, out int x, out int y) => (x, y) = (p.X, p.Y);
でも、相変わらず長くなりがちです。 また、非同期メソッドで使えない点は変わりません。
タプルを使えばこの問題は解決です。
static async Task F(Point p)
{
// 1個の var で受け取れる
var t1 = Deconstruct(p);
Console.WriteLine($"{t1.x}, {t1.y}");
// 何なら分解と併せればもっと書き心地よく書ける
var (x, y) = Deconstruct(p);
Console.WriteLine($"{x}, {y}");
// 非同期メソッドで使えるのはタプルだけ
var t2 = await DeconstructAsync(p);
Console.WriteLine($"{t2.x}, {t2.y}");
}
static (int x, int y) Deconstruct(Point p) => (p.X, p.Y); // 1個の式で書けて楽
static async Task<(int x, int y)> DeconstructAsync(Point p) => (p.X, p.Y);
一方で、出力引数を使いたくなる場面も残っています。
TryParse
のように、bool
値を返してif
ステートメントなどの条件式内で使いたい場合- オーバーロードを呼び分けたい場合
if
内で使いたい場合は、例えば以下のようなコードになります。
static void TryPattern()
{
var s = Console.ReadLine();
if (int.TryParse(s, out var x)) Console.WriteLine(x);
}
これはさすがにタプルを使う方が煩雑です。
static void TuplePattern()
{
var s = Console.ReadLine();
var (success, x) = Parse(s);
if (success) Console.WriteLine(x);
}
static (bool success, int value) Parse(string s) => int.TryParse(s, out var x) ? (true, x) : (false, 0);
もっとも、C# 7では、以下のような is
演算子を使ったnull
チェックで同様のことをすると言う手もあります。
この書き方を型スイッチと呼びます(説明ページ準備中。でき次第リンク)。
static void NullCheckPattern()
{
var s = Console.ReadLine();
if (ParseOrDefault(s) is int x) Console.WriteLine(x);
}
static int? ParseOrDefault(string s) => int.TryParse(s, out var x) ? x : default(int?);
もう1つ、オーバーロードですが、C#では(というか.NETでは)、引数でのオーバーロードはできますが、戻り値でのオーバーロードはできません。 そこで、以下のように、オーバーロードに関しては出力引数の方が有利になります。
// これはオーバーロード可能
static void F(out int x, out int y) => (x, y) = (1, 2);
static void F(out int id, out string name) => (id, name) = (1, "abc");
// 戻り値でのオーバーロードはできない
// コンパイル エラーに
static (int x, int y) F() => (1, 2);
static (int id, string name) F() => (1, "abc");