Quantcast
Channel: ++C++; // 未確認飛行 C
Viewing all articles
Browse latest Browse all 75

タプル

$
0
0

概要

Ver. 7

名前のない複合型」で説明したように、 型には常によい名前が付くわけではなく、名無しにしておきたいことがあります。 そういう場合に使うもののうちの1つがC# 7で導入されたタプルです。

タプルの最大の用途は多値戻り値です。 関数の戻り値は引数と対になるものなので、タプルの書き心地は引数に近くなるように設計されています。

ポイント

  • (int x, int y)というような、引数みたいな書き方で「名前のない型」を作れます
  • この書き方をタプルと呼びます

タプル

C# 7で導入されたタプル(tuple)は、 (int x, int y)というような、引数みたいな書き方で「名前のない型」を作る機能です。

タプルという名前

前節の戻り値は「最小値と最大値と平均値を並べたもの」なわけですが、こういう「データを複数並べたもの」を意味する単語がタプルです。

英語では倍数を「double, triple, quadruple, ...」などという単語で表しますが、これを一般化して n-tuple (nは0以上の任意の整数)と書くことがあり、これがタプルの語源です。 n倍、n重、n連結というような意味しかなく、まさに「名前のない複合型」にピッタリの単語です。

型の明示

(int x, int y)みたいな書き方で、1つの型を表します。 タプルの型の書き方はメソッドの仮引数リスト(引数を受け取る側の書き方)に似ていて、()の中に「型名 メンバー名」を , 区切りで並べます。

これは、型を書ける場所であれば大体どこにでもこの「型」を書けます。 まず、以下のように、フィールドや戻り値などの型にできます。

class Sample
{
    private (int x, int y) value;
    public (int x, int y) GetValue() => value;
}

以下のように、ローカル変数の型としても明示できます。

var s = new Sample();
(int x, int y) t = s.GetValue();

もちろん、varを使った型推論も効きます。

varで型推論

また、ジェネリックな型の型引数にも使えます。

var dic = new Dictionary<(string s, string t), (int x, int y)>
{
    { ("a", "b"), (1, 2) },
    { ("x", "y"), (4, 8) },
};

Console.WriteLine(dic[("a", "b")]); // (1, 2)

次節の「タプル リテラル」という書き方もあるのであまり使わないとは思いますが、 一応、new演算子を使った初期化もできます。

// var t = new T(1, 2); みたいなのと同じノリ
var t1 = new (int x, int y) (1, 2);

// var t = new T { x = 1, y = 2}; みたいなのと同じノリ
var t2 = new (int x, int y) { x = 1, y = 2 };

ちなみに、タプルのメンバーは2つ以上な必要があります。()(int x)というようなタプルは現在の仕様では作れません。

タプル リテラル

タプルは(1, 2)というような書き方でリテラルを書くことができます。 タプル リテラルは実引数リスト(引数を渡す側の書き方)に似ています。

// メソッド呼び出し時の F(1, 2); みたいなノリ
(int x, int y) t1 = (1, 2);

// メソッド呼び出し時の F(x: 1, y: 2); みたいなノリ
var t2 = (x: 1, y: 2);

nullのように単体では型が決まらないものも、左辺に型があれば推論が効きます。 一方で、左辺もvar等になっていて型が決まらない場合、コンパイル エラーになります。

// これは左辺から型推論が聞くので、null も書ける
(string s, int i) t1 = (null, 1);

// これはダメ。null の型が決まらない。
var t2 = (null, 1); // コンパイル エラー

メンバー参照

メンバーの参照の仕方は普通の型と変わりません。(int x, int y)であれば、xyという名前でアクセスできます。 ちなみに、タプルのメンバーは書き換え可能です。

var t = (x: 1, y: 2);
Console.WriteLine(t.x); // 1
Console.WriteLine(t.y); // 2

// メンバーごとに書き換え可能
t.x = 10;
t.y = 20;
Console.WriteLine(t.x); // 10
Console.WriteLine(t.y); // 20

// タプル自身も書き換え可能
t = (100, 200);
Console.WriteLine(t.x); // 100
Console.WriteLine(t.y); // 200

ちなみに、タプルのメンバーはフィールドになっています (プロパティではない)。 フィールドになっているということは、例えば、参照引数(ref)に直接渡せます (これが、プロパティだと無理)。

例えば以下のようなメソッドがあったとします。

static void Swap<T>(ref T x, ref T y)
{
    var t = x;
    x = y;
    y = t;
}

このとき、以下のようにタプルのメンバーを渡せます。

var t = (x: 1, y: 2);
Swap(ref t.x, ref t.y);
Console.WriteLine(t.x); // 2
Console.WriteLine(t.y); // 1

タプルの分解

タプルは、各メンバーを分解して、それぞれ別の変数に受けて使うことができます。

var t = (x: 1, y: 2);

// 分解宣言1
(int x1, int y1) = t; // x1, y1 を宣言しつつ、t を分解
// 分解宣言2
var (x2, y2) = t; // 分解宣言の簡易記法

// 分解代入
int x, y;
(x, y) = t; // 分解結果を既存の変数に代入

この分解は、タプル以外の型に対しても使えるものです。 詳しくは「複合型の分解」で説明します。

タプル間の変換

タプル間の代入は、一定の条件下では暗黙的変換が掛かります。

名前違いのタプル

タプル間の代入は、メンバーの宣言位置に基づいて行われます。 逆に言うと、名前は無関係で、メンバーの型の並びだけ一致していれば代入できます。

例えば以下のように書くと、1番目同士(xs)、2番目同士(yt)で値が代入されます。

(int s, int t) t1 = (x: 1, y: 2);
Console.WriteLine(t1.s); // 1
Console.WriteLine(t1.t); // 2

同名であっても、位置が優先です。以下のような書き方をすると、xyが入れ替わります。

(int y, int x) t2 = (x: 1, y: 2);
Console.WriteLine(t2.x); // 2
Console.WriteLine(t2.y); // 1

型違いのタプル

タプルのメンバーの型が違う場合、メンバーごとに暗黙的な変換がかかる場合に限り、 タプル間の暗黙的変換ができます。

例えば以下の場合、xyzも、それぞれが型変換できるので、タプルの暗黙的型変換が掛かります。

object x = "abc"; // string → object は OK
long y = 1; // int → long は OK
int? z = 2; // int → int? は OK
// ↓
(object x, long y, int? z) t = ("abc", 1, 2); // OK

逆に、以下の場合はコンパイル エラーになります。この例では全部のメンバーが変換不能ですが、全部でなくても、どれか一つでも変換できないと、タプル自体の変換もエラーになります。

string x = 1; // int → string は NG
int y = 1L; // long → int は NG
int z = default(int?); // int? → int は NG
// ↓
(string x, int y, int z) t = (1, 1L, default(int?)); // NG

タプルの入れ子

タプルは入れ子にできます。

// タプルの入れ子
(string a, (int x, int y) b) t1 = ("abc", (1, 2));
Console.WriteLine(t1.a);   // abc
Console.WriteLine(t1.b.x); // 1
Console.WriteLine(t1.b.y); // 2

// 型推論も可能
var t2 = (a: "abc", b: (x: 1, y: 2));

メンバー名も匿名

タプルは、メンバー名もなくして、完全に匿名(名無し)にすることもできます。 この場合、メンバーを使う際にはItem1Item2、…というような名前で参照します。

var t1 = (1, 2);
Console.WriteLine(t1.Item1); // 1
Console.WriteLine(t1.Item2); // 2

Item1Item2、… という名前は、後述するValueTuple構造体のメンバー名です。

冒頭や「名前のない複合型」で説明したように、 「メンバー名だけ見れば十分」だから型名を省略するのであって、 メンバー名まで省略するのとさすがにプログラムが読みづらくなります。 メンバー名も持っていない完全な匿名タプルは、おそらくかなり短い寿命でしか使わないでしょう。 例えば、すぐに別の(メンバー名のある)タプル型に代入したり、分解して変数に受けて使うことになります。

タプルの内部実装

タプルがどういうコードに展開されるかについても話しておきましょう。

タプルを使ったコードを古いバージョンの.NET上で動かしたり、 タプルを使ったライブラリを古いバージョンのC#から参照したり、 別のプログラミング言語から参照したい場合もあります。 そのために、タプルは、ValueTupleという構造体に展開されます。

ValueTuple構造体への展開

タプルは、コンパイルの結果としてはValueTuple構造体(System名前空間)に展開されます。

例えば、以下のようなコードを考えます。

var t = (x: 3, y: 5);
var p = t.x * t.y;
var (x, y) = t;
Console.WriteLine($"{x} × {y} = {p}");

以下のようなコードに展開されます。

var t = new ValueTuple<int, int>(3, 5); // (x: 3, y: 5)
var p = t.Item1 * t.Item2; // t.x * t.y
var x = t.Item1;
var y = t.Item2;
Console.WriteLine($"{x} × {y} = {p}");

元々のxyという名前は、内部的には残っていません。ValueTuple構造体のメンバーであるItem1Item2に展開されます。

特に、一度objectdynamicを経由すると、名前を完全に紛失します。 以下のコードでは、xyが見つからず、実行時エラーを起こします。

private static void Dynamic()
{
    // 匿名型は名前が残る
    var a = new { x = 3, y = 5 };
    var s1 = Sum(a); // 大丈夫
    Console.WriteLine(s1);

    // タプル型は名前を紛失する
    var t = (x: 3, y: 5);
    var s2 = Sum(t); // x, yという名前が実行時になくてエラーに
    Console.WriteLine(s2);
}

private static dynamic Sum(dynamic d) => d.x + d.y;

TupleElementNames属性

とはいえ、名前をどこにも残さないと、ライブラリをまたいだ時にxyなどの名前が使えなくて困ります。 そこで、クラスのメンバーにタプルを使う場合には、TupleElementNames属性(System.Runtime.CompilerServices名前空間)を付けて、 C#コンパイラーには名前がわかるようにしています。

例えば、以下のような引数も戻り値もタプルなメソッドを書いたとします。

public (int x, int y) F((int a, int b) t) => (t.a + t.b, t.a - t.b);

このメソッドは、以下のように展開されます。タプルがValueTuple構造体に化けますが、TupleElementNames属性を付けて名前を残します。

[return: TupleElementNames(new[] { "x", "y" })]
public ValueTuple<int, int> F([TupleElementNames(new[] { "a", "b" })] ValueTuple<int, int> t)
    => new ValueTuple<int, int>(t.Item1 + t.Item2, t.Item1 - t.Item2);

C#コンパイラーは、この情報を元に、タプルの名前を復元します。

ValueTuple構造体の中身

タプルの展開結果にあたるValueTupleは、型引数が0~8個の合計9個の構造体があります。 例えば、型引数2個のものは以下のような定義になっています。

[StructLayout(LayoutKind.Auto)]
public struct ValueTuple<T1, T2>
    : IEquatable<ValueTuple<T1, T2>>, IStructuralEquatable, IStructuralComparable, IComparable, IComparable<ValueTuple<T1, T2>>
{
    public T1 Item1;
    public T2 Item2;

    public ValueTuple(T1 item1, T2 item2)
    {
        Item1 = item1;
        Item2 = item2;
    }

    // 後略、インターフェイスのメンバー定義
}

基本的には、publicなフィールドだけを持つ構造体です。 それに、値の比較用の各種インターフェイスが実装されています。

メンバーが9個以上のタプル

最初に言った通り、ValueTuple構造体の型引数は、最大のものでも8個です。 では、メンバーが9個以上のタプルを作るとどうなるかというと、入れ子のValueTuple構造体が作られます。

例えば、以下のようなコードを書いたとします。 メンバー名も匿名で作ったので、9番目のメンバーの参照にItem9という名前が使われています。

var t = (1, 2, 3, 4, 5, 6, 7, 8, 9);
Console.WriteLine(t.Item9);

このコードは、以下のように展開されます。

var t = new ValueTuple<int, int, int, int, int, int, int, ValueTuple<int, int>>(
    1, 2, 3, 4, 5, 6, 7, new ValueTuple<int, int>(8, 9));
Console.WriteLine(t.Rest.Item2);

ValueTuple構造体の定義場所

C# 7のリリースに合わせて、ValueTuple構造体は標準ライブラリに取り込まれる予定です。

一方で、古い.NET (.NET Framework 4.6.2以前、.NET Standard 1.6以前)上でタプルを使いたい場合は、 以下のライブラリを参照します。この中にValueTuple構造体や、TupleElementNames属性が定義されています。

型引数0、1のValueTuple

前述の通り、タプルのメンバーは2つ以上な必要で、()(int x)というようなタプルは作れません。 一方で、ValueTuple構造体には、型引数0個と1個のものが存在します。

var noneple = new ValueTuple();
var oneple = new ValueTuple<int>(1);

// メンバー2個以上はタプル構文を使える
var twople = (1, 2); // new ValueTuple<int, int>(1, 2);
var threeple = (1, 2, 3); // new ValueTuple<int, int, int>(1, 2, 3);

型引数0個のValueTuple(0-tuple)は、いわゆるUnit型です。 voidの代わりにこの型を使うことで、戻り値がある場合とない場合のコードを統一的に書けてうれしい場合があります。 一方、型引数1個のもの(1-tuple)も、用途としては0-tupleと同じです。 型引数2個以上のものと並べて、戻り値や引数の個数違いを統一的に書けます。

例えば、以下の2つのコードはどちらの方が統一性があっていいかという話になります。

// タプルでは0、1は書けない
async Task F0() { }
async Task<int> F1() => 1;
async Task<(int x1, int x2)> F2() => (1, 2);
async Task<(int x1, int x2, int x3)> F3() => (1, 2, 3);
// こう書けると統一性があってきれい(C# 7では書けない)
async Task<()> F0() { }
async Task<(int x1)> F1() => (1);
async Task<(int x1, int x2)> F2() => (1, 2);
async Task<(int x1, int x2, int x3)> F3() => (1, 2, 3);

特に、ソースコード生成などでまとめて、個数違いのメソッドを生成したい場合などには、0-tupleや1-tupleがほしくなります。 0個と1個の時だけ特別扱いが必要になるかどうかという問題です。 0-tupleと1-tupleがあれば、特別扱いなしでソースコード生成ができて楽です。

ということで、0-tuple、1-tupleの需要はあるんですが、問題があって構文を提供できていません。 1-tupleになるであろう構文は(1)というような形になるはずですが、 これが、C#の既存の構文ですでに、単に1と同じ意味で解釈されるため、1-tupleを作れません。 0-tupleの方の()は、これまでは書けなかった書き方なので別にC# 7で追加できますが、 1-tupleだけ飛ばして「0か2以上のみ」とするのも変な話です。

関連

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

Viewing all articles
Browse latest Browse all 75

Trending Articles