目次

概要

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);

ちなみに、この分解構文は、タプルか、後述する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];

タプル構築と分解の混在

タプルを作る構文と分解代入の構文は似ているわけですが、これらは、以下のようにつなげて書くこともできます。

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

これは、以下のように、分解後に改めてタプルを作るのと同じ意味になります。

int x, y;
(x, y) = (1, 2); // 分解代入
var t = (x, y);  // 改めてタプルを構築

ちなみに、将来的には、以下のように、分解代入と分解宣言の混在もできるようになる予定です。

int x, y;
// C# 7.3 か 8.0 辺り?
(x, var u) = (var v, y) = (1, 2);

分解時の型変換

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

// 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);

引数の数が同じオーバーロード不可

分解構文では、引数の数が同じDeconstructメソッドを呼び分けることができません。 例えば以下の例のように、引数の型がdouble, doubleのものと、double, Radianのものという2つのDeconstructメソッドを定義してしまうと、2変数の分解ができなくなります。

using static System.Math;

struct Radian
{
    public double Value { get; }
    public Radian(double value) => Value = value;
}

struct Vector2D
{
    public double X { get; }
    public double Y { get; }

    // コンストラクターは当然、個数が同じでも、型が違えば呼び分けができる
    public Vector2D(double x, double y) => (X, Y) = (x, y);
    public Vector2D(double radius, Radian angle)
        : this(radius * Cos(angle.Value), radius * Sin(angle.Value)) { }

    // 引数の数が同じ Deconstruct が2つある
    // 片方だけならいいけど、2つあると分解ができなくなる
    public void Deconstruct(out double x, out double y) => (x, y) = (X, Y);
    public void Deconstruct(out double radius, out Radian angle)
        => (radius, angle) = (Sqrt(X * X + Y * Y), new Radian(Atan2(Y, X)));
}

class Program
{
    static void Main()
    {
        // コンストラクターの呼び分け
        var p = new Vector2D(1, 2);
        var q = new Vector2D(10, new Radian(PI / 5));

        // 分解は呼び分けできない
        (double x, double y) = q; // コンパイル エラー
        (double r, Radian a) = p; // コンパイル エラー
    }
}

一方で、引数の数が違えば複数のDeconstructメソッドがあっても大丈夫です。 例えば以下のようなコードであれば、ちゃんと分解が使えます。

struct Vector3D
{
    public double X { get; }
    public double Y { get; }
    public double Z { get; }
    public Vector3D(double x, double y, double z) => (X, Y, Z) = (x, y, z);

    // 引数の数が違えば大丈夫
    public void Deconstruct(out double x, out double y, out double z) => (x, y, z) = (X, Y, Z);
    public void Deconstruct(out double first, out Vector2D rest) => (first, rest) = (X, new Vector2D(Y, Z));
}

class Program
{
    static void Main()
    {
        var p = new Vector3D(1, 2, 3);

        // 分解可能
        var (first, rest) = p;
        var (x, y, z) = p;
    }
}

タプルの構築や分解の最適化

分解構文は、基本的にはDeconstructメソッドの呼び出しに展開されます。 しかし、タプルに対しては、Deconstructメソッドやコンストラクター呼び出しをなくす最適化が掛かります。

例えば以下のようなコード(いわゆるSwap処理)を書いたとします。

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

もしタプルが一般の型と同列に扱われるのなら、 「ValueTuple構造体への展開」で説明した内容や、 前述のDeconstructに展開される仕様を考えると、 これは以下のような意味にとることができます。

var t = new ValueTuple<int, int>(y, x);
t.Deconstruct(out x, out y);

しかし、タプルに限り、単なる一時変数の追加やメンバーアクセスに展開され得ます。 上記の (x, y) = (y, x) は、以下のように展開できます。

var t1 = y; // この t1 の方はさらに最適化で消える可能性あり
var t2 = x;
x = t1;
y = t2;

実際にどこまで最適化されるかは実装依存です。 例えば、C# 7.0の頃には new ValueTuple<int, int>(x, y) が一度作られていましたし、 現在の実装では t1 も消えて var t = x; x = y; y = t; 相当のコードが出力されます。

余談: System.ValueTuple 構造体を要求される

タプルによる分解を使う場合、C# コンパイラーは常にValueTuple構造体を要求します(System.ValueTupleパッケージの参照が必要)。

「常に」というところが少し曲者です。 例えば以下のような2つのステートメントを考えます。

// タプルの仕様上、ValueTuple<int, int> 構造体が作られる
var t = (1, 2);

// 前述の通り、最適化が掛かるので ValueTuple は不要なはず
var (x, y) = (1, 2);

前者は実際にValueTuple構造体を必要としているので問題はありません。必要なものの参照を要求しているだけです。 一方、後者はValueTuple構造体を使わないにも関わらず、C# コンパイラーはValueTuple構造体の参照を求めます。

このコードから「すぐに分解するから最適化で消える」というの判定するのはコンパイラーにとっては意外と大変らしく、 「頑張っても見合わない」とのことで、この仕様を変えるつもりは今のところないようです。

分解の評価のされ方

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

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

単純な場合、例えば(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) を受け取る一時変数をまず用意
var t1 = y;
var t2 = x;
// 一時変数から改めて代入
x = t1;
y = t2;

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

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;

更新履歴

ブログ