7/12 (火) の早朝、長らく検討/開発されてきた待望の Deconstructions (= 型分解) が Roslyn の master ブランチにマージされました!(型分解というのは僕のテキトーな訳で、正式名称は未定)
タプル構文が複数の値をひとつにまとめる機能だったのに対し、Deconstructions はその逆の複数の値への分解をサポートします。C# 7 への搭載の期待もさることながら、直近では Visual Studio 15 Preview 4 にも含まれるのではないかと思われます。タプル構文については以下を参照ください。
基本的な記法
まず、System.ValueTuple
を分解しつつ基本の書き方を見ていきましょう。以下のような 3 種類の書き方があります。(もっとあったらゴメンナサイ...
var t = (123, "abc");
(int x, string y) = t;
(var x, var y) = t;
var (x, y) = t;
Console.WriteLine($"({x}, {y})");
既存の変数への割り当て
先の例では変数を生成しつつ値を分解/割り当てしましたが、すでに宣言された既存の変数に対しても適用できます。この場合は型の指定は必要ありません。
int x;
string y;
(x, y) = (123, "abc");
配列要素やプロパティへの割り当て
変数にだけ代入できるわけではありません。もちろん配列の要素や 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;
(char x, string y) = t;
任意の型の分解
ここまで ValueTuple
型の分解を見てきました。Deconstructions はタプル構文の対になる機能なので ValueTuple
型にしか適用できないのか、と言うと当然そんなことはなく、ちゃんと任意の型に対して適用できます。ただ、当然ながら何も追加実装をしなくても値の分解ができるわけではありません。既存の型をどのようなルールで分解するかがコンパイラには分からないからです。そのルールは以下のように Deconstruct
メソッドを実装することで実現/決定します。
static void Main()
{
var p = new Person();
var (name, age) = p;
Console.WriteLine($"({name}, {age})");
}
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;
var (x3, y3, z3) = v;
}
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;
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)
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})");
超簡単スワップ! キタ━(゚∀゚)━!
タプル構文と型分解のおかげで、値のスワップが神がかり的に簡単になります!.Swap(x, y)
メソッドよ、安らかに眠れ... (-人-)
var x = 1;
var y = 2;
(x, y) = (y, x);
Console.WriteLine($"({x}, {y})");