目次

概要

Ver. 7

タプルから値を取り出す際には、メンバーを直接、それぞれバラバラに受け取りたくなることがあります。

名前のない複合型」で説明したように、 メンバー名だけ見ればその型が何を意味するか分かるからこそ型に名前が付かないわけです。 このとき、その型を受け取る変数にも、よい名前が浮かばなくなるはずです。

そこでC# 7では、タプルと同時に、分解(deconstruction)のための構文が追加されました。

分解

以下のような、整数列の個数(count)と和(sum)を同時に計算するメソッドがあったとします。 「名前のない複合型」で説明したように、 戻り値の型として「個数と和」みたいな名前(CountAndSumとか)しか思い浮かばないようなものです。

static (int count, int sum) Tally(IEnumerable<int> items)
{
    var count = 0;
    var sum = 0;
    foreach (var x in items)
    {
        sum += x;
        count++;
    }

    return (count, sum);
}

そうなると、この結果を受け取る変数名も、「個数と和」以上の名前はつかないでしょう。 通常、ローカル変数であれば適当な名前でもそこまで問題ではないので、 xとかyとか、本当に意味がない名前を付けることになると思います。

var x = Tally(new[] { 1, 2, 3, 4, 5 });
Console.WriteLine(x.count);
Console.WriteLine(x.sum);

実際にほしい名前はcountsumだけです。 であれば、最初からcount変数とsum変数に分解して受け取りたいと思うでしょう。 要するに、以下のようなことを1行で書ける構文がほしいです。

// この3行に相当する構文がほしい
var x = Tally(new[] { 1, 2, 3, 4, 5 });
var count = x.count;
var sum = x.sum;
// 以後、もう x は使わない

Console.WriteLine(count);
Console.WriteLine(sum);

タプルのような名前の決まらない型は、この例のように分解して使うのが前提と言えます。

そこで、C# 7では、タプルと一緒に、以下のような分解のための構文を追加しました。

(var count, var sum) = Tally(new[] { 1, 2, 3, 4, 5 });
Console.WriteLine(count);
Console.WriteLine(sum);

導入された分解構文には、 分解と同時に変数宣言もする「分解宣言」と、 既存の変数に分解する「分解代入」の2種類あります。

ちなみに、この分解構文は、タプルか、後述するDeconstructメソッドを持つ任意の型に対して使えます。

分解宣言

以下のような書き方で、分解と同時に変数を宣言できます。 これを分解宣言(deconstruction declaration)と言います。

// count, sum を宣言しつつ、タプルを分解
(int count, int sum) = Tally(items);

// ↓こう書くとタプル型の変数の宣言
// (int count, int sum) t = Tally(items);

この例の後半のコメントのように、分解宣言はタプルの型宣言の書き方によく似ています。 ただ、タプルの型宣言と違って、型推論のvarが使えます。

// 型推論で count, sum を宣言しつつ、タプルを分解
(var count, var sum) = Tally(items);

// ↓タプルだと var は使えない。これはコンパイル エラー
// (var count, var sum) t = Tally(items);

このとき、部分的に型推論(var)を使うこともできます。

// 部分的に var を使う
(var count, long sum) = Tally(items);

一方で、宣言したいすべての変数を型推論する場合であれば、先頭に1つだけ var キーワードを書く以下のような書き方もできます。

// 「var + 変数リスト」でタプルを分解
var (count, sum) = Tally(items);

この書き方は、foreachforなどでの変数宣言でも使えます。

(int x, int y)[] array = new[] { (1, 2), (3, 4) };

foreach (var (x, y) in array)
{
    Console.WriteLine($"{x}, {y}");
}

for ((int i, int j) = (0, 0); i < 10; i++, j--)
{
    Console.WriteLine($"{i}, {j}");
}

(仕様書状はクエリ式のletfrom でも使えることになっているものの、プレビュー版である現在は未実装。)

分解代入

既存の変数を使って分解することもできます。 こちらは分解代入(deconstruction assignment)といいます。

int x, y;

// 既存の変数を使って分解
(x, y) = Tally(items);

文法説明のために簡素化したものとはいえ、この例では分解宣言で十分で、 再代入(既存の変数xyの書き換え)の必要性があまりありません。 実際は、以下の例のように、ループで書き換えたりすることになるでしょう。

var x = 1.0;
var y = 5.0;

for (int i = 0; i < 100; i++)
{
    (x, y) = ((x + y) / 2, Math.Sqrt(x * y));
}

分解代入の左辺には、書き換え可能なものであれば何でも書けます。 例えば、配列アクセスや参照戻り値などを分解代入の左辺に書けます。

private static void DeconstractionAssingment()
{
    var a = new[] { 1, 2 };

    // 配列アクセス
    var b = new int[a.Length];
    (b[1], b[0]) = (a[0], a[1]);

    // 参照戻り値
    var c = new int[a.Length];
    (Mod(c, 5), Mod(c, 8)) = (a[0], a[1]);

    Console.WriteLine(string.Join(", ", b));
    Console.WriteLine(string.Join(", ", c));
}

static ref T Mod<T>(T[] array, int index) => ref array[index % array.Length];

分解時の型変換

分解時には、タプル間の型変換と同じルールで暗黙の型変換が働きます。 すなわち、宣言位置で分解されます(メンバー名は見ない)し、メンバーごとに暗黙的型変換が効くなら分解でも暗黙的型変換が効きます。

// Tally の戻り値は (count, sum) の順
var t = Tally(new[] { 1, 2, 3, 4, 5 });

// sum = t.count, count = t.sum の意味になるので注意が必要
(int sum, int count) = t;
Console.WriteLine(sum);   // 5
Console.WriteLine(count); // 15

// int → object も int → long も暗黙的に変換可能
// なので、分解もでもこの変換が暗黙的に可能
(object x, long y) = t;

任意の型を分解

C#の言語機能としてのタプルの他にも、 タプルに類する型はあります。 すなわち、意味のある変数が作れず、分解して使う前提の型です。

代表例はKeyValuePair構造体(System.Collections.Generic名前空間)でしょう。 keyvalueという変数で分解して受け取りたいです。

また、C#の構文としてタプルが導入される以前に使っていた型ですが、 Tupleクラス(System名前空間)というものがあります。 メンバー名まで紛失してしまうので使い勝手はよくありませんが、 「型名がうまく付けられない時に使う型」です。

これらの型に対しても分解構文を使いたいです。 そこで、C# 7では、Deconstructという名前のインスタンス メソッド、もしくは、拡張メソッドさえ持っていれば、 どんな型でも分解構文使えるようにしました。 例としてKeyValuePairTupleに対するDeconstructの書き方を示しましょう。 以下のような拡張メソッドがあれば分解できます。

static class Extensions
{
    public static void Deconstruct<T, U>(this KeyValuePair<T, U> pair, out T key, out U value)
    {
        key = pair.Key;
        value = pair.Value;
    }

    public static void Deconstruct<T1, T2>(this Tuple<T1, T2> x, out T1 item1, out T2 item2)
    {
        item1 = x.Item1;
        item2 = x.Item2;
    }
}

(ちなみに、将来的には、KeyValuePairTuple自体に手が入って、インスタンス メソッドとしてDeconstructメソッドが追加される可能性もあります。)

これで、KeyValuePairTupleに対して分解構文が使えます。以下のようなコードが書けます。

var pair = new KeyValuePair<string, int>("one", 1);
var (k, v) = pair;
// 以下のようなコードに展開される
// string k;
// int v;
// pair.Deconstruct(out k, out v);

var tuple = Tuple.Create("abc", 100);
var (x, y) = tuple;
// 以下のようなコードに展開される
// string x;
// int y;
// tuple.Deconstruct(out x, out y);

分解の評価のされ方

分解構文では、メンバーごとにそれぞれ代入するような結果を生みます。 このとき、以下のようなルールが働きます。

  • メンバーの評価は左から順
  • メンバーの書き換えは同時に起こる

単純な場合、例えば(a, b) = (x, y);のような時にはこんなルールを気にするまでもなく、a = x; b = y;と同じ結果になります。 ここで、もう少し複雑な場合を考えてみましょう。

まず、左右で同じ変数が出てくる場合についてです。 分解構文では、各メンバーへの代入が同時に行われるかのような結果を生みます。 例えば、xyという2つの変数の値を入れ替え(swap)ようとするとき、逐次実行であれば、以下のような書き方は間違いです。

var x = 1;
var y = 2;

y = x;
x = y; // 上の行で y が書き換わっているので、値の入れ替えにはならない

Console.WriteLine(x); // 1
Console.WriteLine(y); // 1

// 正しくは以下のように書く
// var temp = y;
// y = x;
// x = temp;

これが、分解代入を使って以下のように書くと、正しく値が入れ替わります。

var x = 1;
var y = 2;

// 分解代入であれば、値の書き換えは同時に起こる
(y, x) = (x, y);

Console.WriteLine(x); // 2
Console.WriteLine(y); // 1

値が並行して同時に書き換わっているように見えます。 実際には、以下のように、一時的な変数が挟まったように解釈されています。

// 実際には、同時に書き換わったように見えるように、一時変数が挟まる
// (y, x) = (x, y) であれば、以下のように評価されてる
var t = (x, y); // 一時的にタプルが作られる
y = t.Item1;
x = t.Item2;

タプルが作られる分、var temp = y; y = x; x = temp; というような書き方よりは若干効率が悪くなります。 といっても、タプルは構造体なのでヒープ(「値型と参照型の違い」、「ヒープ」参照)を圧迫したりはしません。 おそらくJITコンパイル時の最適化で消える程度の差です。

さらに複雑になるのは、式が副作用を持つ場合です。 例として、分解代入の両辺に、悪名高いインクリメント演算を混ぜてみましょう。 各メンバーは、左から順に評価されます。

var a = new[] { 0, 1, 2, 3 };
var i = 0;

(a[i++], a[i++]) = (a[i++], a[i++]);

Console.WriteLine(string.Join(", ", a)); // 2, 3, 2, 3
// つまり、以下の評価を受けてる
// (a[0], a[1]) = (a[2], a[3]);

これと同じ動作をタプルと分解なしで書くと、以下のようなコードになります。

var a = new[] { 0, 1, 2, 3 };
var i = 0;

ref var l1 = ref a[i++];
ref var l2 = ref a[i++];
var r1 = a[i++];
var r2 = a[i++];

l1 = r1;
l2 = r2;

更新履歴

ブログ