関連

タプルには、毛色の似た機能が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");

更新履歴

ブログ