xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

型分解 - タプルから変数への展開

7/12 (火) の早朝、長らく検討/開発されてきた待望の Deconstructions (= 型分解) が Roslyn の master ブランチにマージされました!(型分解というのは僕のテキトーな訳で、正式名称は未定)

タプル構文が複数の値をひとつにまとめる機能だったのに対し、Deconstructions はその逆の複数の値への分解をサポートします。C# 7 への搭載の期待もさることながら、直近では Visual Studio 15 Preview 4 にも含まれるのではないかと思われます。タプル構文については以下を参照ください。

f:id:xin9le:20160714055622p:plain

基本的な記法

まず、System.ValueTuple を分解しつつ基本の書き方を見ていきましょう。以下のような 3 種類の書き方があります。(もっとあったらゴメンナサイ...

//--- こんなタプル型のインスタンスがあったとする
var t = (123, "abc");

//--- Let's 分解
(int x, string y) = t;  //--- 型を明示
(var x, var y) = t;     //--- 一部 (or 全部) の型を推論
var (x, y) = t;         //--- すべてを型推論で解決

//--- それぞれの別々の変数として利用できる
Console.WriteLine($"({x}, {y})");  //--- (123, abc)

既存の変数への割り当て

先の例では変数を生成しつつ値を分解/割り当てしましたが、すでに宣言された既存の変数に対しても適用できます。この場合は型の指定は必要ありません

int x;
string y;
(x, y) = (123, "abc");  //--- ValueTuple を生成して即座に x, y に分解

配列要素やプロパティへの割り当て

変数にだけ代入できるわけではありません。もちろん配列の要素や setter のあるプロパティにも適用できます。

//--- 配列要素の入れる
var a = new int[2];
(a[0], a[1]) = (1, 23);

//--- プロパティに入れる
class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}
var p = new Person();
(p.Name, p.Age) = ("xin9le", 31);

逆コンパイル

あまりに見慣れない書き方過ぎてもはやワケわからん状態かと思うので (?)、逆コンパイルしてどう実現されているかを確認してみましょう。

//--- これは
var (x, y) = (123, "abc");

//--- こう展開される
ValueTuple<int, string> t = new ValueTuple<int, string>(123, "abc");
int x = t.Item1;
string y = t.Item2;

変数を並べた順に .Item1 .Item2 ...のプロパティが割り当てられていることが分かります。

暗黙的型変換

(当然ですが) 型を指定することで、暗黙的型変換の適用により互換のある型として値を受けとることができます。もちろん、互換のない型を指定するとコンパイルエラーになります。

var t = (123, "abc");
(long x, object y) = t;  //--- OK!!
(char x, string y) = t;  //--- int は char で受けられないので NG!!

任意の型の分解

ここまで ValueTuple 型の分解を見てきました。Deconstructions はタプル構文の対になる機能なので ValueTuple 型にしか適用できないのか、と言うと当然そんなことはなく、ちゃんと任意の型に対して適用できます。ただ、当然ながら何も追加実装をしなくても値の分解ができるわけではありません。既存の型をどのようなルールで分解するかがコンパイラには分からないからです。そのルールは以下のように Deconstruct メソッドを実装することで実現/決定します。

static void Main()
{
    var p = new Person();
    var (name, age) = p;  //--- ここで Deconstruct メソッドが呼ばれる
    Console.WriteLine($"({name}, {age})");  //--- (xin9le, 31)
}

class Person
{
    public string Name { get; } = "xin9le";
    public int Age { get; } = 31;

    //--- 型分解のための特殊なメソッド
    public void Deconstruct(out string name, out int age)
    {
        name = this.Name;
        age = this.Age;
    }
}

拡張メソッドとしての実装

上記の方法では Deconstruct メソッドをインスタンスメソッドとして実装しましたが、拡張メソッドとして実装しても OK です。以下のような感じです。

class Person
{
    public string Name { get; } = "xin9le";
    public int Age { get; } = 31;
}

static class PersonExtensions
{
    public static void Deconstruct(this Person self, out string name, out int age)
    {
        if (self == null)
            throw new ArgumentNullException(nameof(self));
        name = self.Name;
        age = self.Age;
    }
}

複数の分解方法の提供

Deconstruct メソッドはひとつでなければならないという制限はありません。複数の分解方法を提供したい場合は、複数の Deconstruct メソッドを実装することもできます。

static void Main()
{
    var v = new Vector3 { X = 1, Y = 2, Z = 3 };
    var (x2, y2) = v;      //--- 引数 2 つの方が呼び出される
    var (x3, y3, z3) = v;  //--- 引数 3 つの方が呼び出される
}

class Vector3
{
    public int X { get; set; }
    public int Y { get; set; }
    public int Z { get; set; }

    public void Deconstruct(out int x, out int y)
    {
        x = this.X;
        y = this.Y;
    }

    public void Deconstruct(out int x, out int y, out int z)
    {
        x = this.X;
        y = this.Y;
        z = this.Z;
    }
}

逆コンパイル

もう想像できているかもしれませんが、任意の型の分解がどのように実現されているかを確認してみましょう。以下のように展開されます。原理が分かれば簡単ですね!

var person = new Person();

//--- これは
var (name, age) = person;

//--- こう展開される (Deconstruct メソッドの呼び出しになる)
int x;
string y;
person.Deconstruct(out x, out y);
int name = x;
string age = y;

ちなみにすでにお気付きかもしれませんが、先に紹介した ValueTuple 型とは展開のされ方が全然違います。ValueTuple には Deconstruct メソッドの実装もないですし、かなり特別扱いされていることが分かります。

任意の型分解のまとめ

  • メソッド名は Deconstruct
  • 引数に out キーワードを付ける
  • 第 1 引数から順番に変数にマッピングされる
  • インスタンスメソッド or 拡張メソッドとして実装する
  • インスタンスメソッドと拡張メソッドがある場合はインスタンスメソッドが優先される
  • 分解方法 (Deconstruct メソッド) は複数あっても構わない

System.Tuple の分解

System.ValueTuple は特別扱いされて展開されることが分かりました。では参照型になっただけの System.Tuple はどうかと言うと、コンパイラによる特別扱いはありません。なので、同じように型を分解をするためには拡張メソッドの実装が必要になります。

//--- こんな感じ
public static class TupleExtensions
{
    public static void Deconstruct<T1, T2>(this Tuple<T1, T2> value, out T1 item1, out T2 item2)
    {
        item1 = value.Item1;
        item2 = value.Item2;
    }
}

しかし、さすがに毎度×2 個々人が実装するのは無慈悲過ぎるので、CoreFx では System.Tuple 型に対する Deconstruct 拡張メソッドを提供してくれています。よかった。

ちなみに、ValueTuple と似たような型に KeyValuePair がありますが、これも Deconstruct 拡張メソッドを用意しておくと幸せになれると思います。現状では提供されていませんが、Tuple と同様 .NET Framework / .NET Core に標準搭載されるかも (?) しれません。

こんなこともできる / これはできない

ちょっとオマケではありますが、挙動を調べてみたものを並べてみます。執筆時点での話なので、できないものも将来的にできるようになるかもしれません。

foreach で使える

変数を受けるのは、当然 foreach でもできます。場合によってはシンプルになって良いかもしれませんね。

var collection = new []
{
    (1, 'a'),
    (2, 'b'),
    (3, 'c'),
};
foreach (var (x, y) in collection)  //--- こんなのも OK
    Console.WriteLine($"({x}, {y})");

クエリ式では使えない

foreach で使えたのでクエリ式でも行けるかも!と思ったけれど、使えませんでした。

//--- できてほしいけれど、現状はまだダメ
var query = from (x, y) in collection
            where x >= 2
            select y;

out var では使えない

先日紹介した変数宣言式 (= Out Variable Declarations) で使えるかも試してみましたが、ここでも使えませんでした。できたら結構便利そうなんだけどなぁ...。

bool OutVar(out ValueTuple<int, string> result)
{
    result = (123, "abc");
    return true;
}

if (OutVar(out var (x, y)))  //--- コンパイルエラー
    Console.WriteLine($"({x}, {y})");

入れ子の分解ができる

入れ子になっていてもちゃんと分解できます。なんとも親切!

//--- こんな入れ子になったものも
(int, (string, char)) GetValueTuple()
    => (1, ("abc", 'あ'));

//--- 分解/展開できちゃう!
var (x, (y, z)) = GetValueTuple();
Console.WriteLine($"({x}, {y}, {z})");  //--- (1, abc, あ)

超簡単スワップ! キタ━(゚∀゚)━!

タプル構文と型分解のおかげで、値のスワップが神がかり的に簡単になります!.Swap(x, y) メソッドよ、安らかに眠れ... (-人-)

var x = 1;
var y = 2;
(x, y) = (y, x);  //--- なんとこれだけ!直観的!
Console.WriteLine($"({x}, {y})");  //--- (2, 1)