最新C# 7言語概説
C# 7.0で知っておくべき10の新機能(前編)
Visual Studio 2017およびVisual Studio Codeで利用可能になったC#言語の新バージョン「7.0」の新機能を、公開されている議論を基に解説。前編として「データ中心設計」に関連する4つの新機能を説明する。
C#の新バージョン「7.0」は、Visual Studio 2017、もしくは.NET Core SDKとVisual Studio Codeの最新版(stableおよびinsiderのどちらでも可)にC#拡張をインストールした環境*1で検証可能になっている。
- *1 サンプルコードの動作確認はVisual Studio 2017と.NET Core 1.1.1の組み合わせで検証している。また、Linux環境においても、Red Hat Enterprise Linux 7.3に.NET Core 1.1.1を入れた環境で動作確認をしている。正式版で仕様が変更される可能性もあるので、ご了承いただきたい。
前のバージョンであるC# 6.0はコード名“Roslyn”と呼ばれるオープンソースのコンパイラープラットフォームとともにリリースされた一方、言語機能については使い勝手の向上を中心とした控えめな機能追加であった。C# 6.0がリリースされた後、.NET Coreが発表され、C#を含めた.NET RuntimeはLinuxやmacOSでもサポートされるという大きな変化があった。また、コンパイラー、ランタイム、クラスライブラリなどC#および.NET CoreまわりがGitHub上に公開されたオープンソースとなり、言語機能についてもその初期段階からGitHub上のIssueなどで議論が公開されている。このようにC#を取り巻く環境も大きく変わった一方、言語機能についてもC# 7.0では大きな機能追加が行われおり、それらのうち一部はさらに次以降のバージョンでの大きな機能追加の布石ともなっている。
本稿ではC# 7.0で追加される機能を10個に分け、さらに「データ中心設計」「パフォーマンス改善」「コードの書きやすさの向上」の3つに分類して紹介する。
【C# 7.0新機能の一覧】
- データ中心設計:
1outパラメーター付き引数での変数宣言(Out Var)
2パターンマッチング(Pattern matching)
3タプル(Tuples)
4分解(Deconstruction) - パフォーマンス改善:
5ローカル関数(Local Functions)
6参照返り値と参照ローカル変数(Ref Returns and Ref Locals)
7asyncメソッドの返り値型の一般化(Generalized async return types) - コードの書きやすさの向上:
8リテラル表記の向上(Literal improvements)
9式形式メンバーの追加(More expression bodied members)
10スロー式(Throw Expressions)
この分類はMSDN .NET BlogのエントリWhat’s New in C# 7.0に基づいている。また本記事で利用している全てのサンプルコードを1つのプロジェクトにまとめたものをGitHubのリポジトリとして公開している。
データ中心設計(data consumption)
1outパラメーター付き引数での変数宣言(Out Var)
C#には以前からout
パラメーター修飾子という引数を参照渡しする仕組みがあった。リスト1のようにout
キーワードを引数に指定してメソッドを宣言し、リスト2のようにout
キーワードを付けてメソッドを呼び出すのが一般的な利用法だ。
class Point
{
public int X { get; }
public int Y { get; private set; }
public void GetCoordinates(out int x, out int y)
{
x = X;
y = Y;
}
}
|
void OldStyle(Point p)
{
int x, y; //事前に変数を宣言する必要があった
p.GetCoordinates(out x, out y);
WriteLine($"({x}, {y})");
}
|
この書き方だと事前に変数宣言を記述する必要があったが、C# 7.0ではリスト3のように引数リストの中で変数宣言が同時にできるようになった。明示的に型指定することに加えて、var
を使って暗黙的に型指定することもできる。また、変数のスコープは従来と同じく変数宣言しているメソッド呼び出しと同じスコープ内となるが、変数宣言より前に参照することはできない。
void PrintCoordinates(Point p)
{
WriteLine(x1); // 変数のスコープ内であっても宣言前には参照するとコンパイルエラー
p.GetCoordinates(out var x1, out int y1);
WriteLine($"(x1,y1)=({x1}, {y1})"); // 変数のスコープはメソッド呼び出しと同じスコープ内
}
|
リスト4のようにTryParse
メソッドのようにbool
型の返り値を持ったout
キーワード付きのメソッドはif
文の条件に利用できるが、その場合、変数のスコープはif
文内だけではなく、if
文を記述しているスコープ内となる。
void PrintStars(string s)
{
if (int.TryParse(s, out var i))
WriteLine(new string('*', i));
else
WriteLine("Cloudy - no stars tonight!");
WriteLine($"input value is : {i}"); // 変数iを参照できる
}
|
また、out
キーワードの引数をその後、参照する必要がない場合、リスト5のように_
を変数に指定できる。なお、指定した_
は通常の変数とは別であるため、後から参照することはできず、同じスコープ内に変数_
が存在してもエラーとはならず宣言した変数_
に影響も与えない。
void DeclareUnderscore(Point p)
{
var _ = 1;
p.GetCoordinates(out var _, out int y);
WriteLine(_); // 「1」と出力される
}
|
通常あまり使われないと思われるが、リスト6のように引数リストで変数宣言した変数は、その後に続く引数で代入できる。これは明示的に型指定したときのみ許され、暗黙的に型指定した場合はコンパイルエラーとなる。
void UseDeclareAgain(Point p)
{
p.GetXSetY(out int x, x = 2);
p.GetXSetY(out var x1, x1 = 2); // 暗黙的な場合
}
|
この機能の導入により、オーバーロードの解決条件に、暗黙的に型指定している引数リストでの変数宣言は、任意の型に暗黙的に変換できることが考慮されるようになった。具体例を挙げると、リスト7のようなオーバーロードされるメソッドを宣言することはできるが、このメソッドを利用する場合は明示的に型指定しないとコンパイルエラーとなる。単独でvar i = 1;
と宣言すると、C#言語規約上、i
はint
型となるが、オーバーロード解決時には考慮されず優先順位が付けられないことになっている。
public void GetX(out int x)
{
x = X;
}
public void GetX(out long x)
{
x = X;
}
void Overload(Point p)
{
p.GetX(out int x);
p.GetX(out var x1); // varだとオーバーロード解決できずコンパイルエラー
}
|
2パターンマッチング(Pattern matching)
データ中心の設計を取り入れるために、C#では代数的データ型や関数型言語でのパターンマッチングの機能の導入を検討してきた。当初はC# 7.0でより多くの機能を導入する可能性もあったが、最終的にis
演算子の拡張とswitch
文の拡張の2つがパターンマッチング機能として追加された。なお、パターンマッチングの機能に関しては「F#およびScalaの機能を参考にした」と記述されている。
is演算子の拡張
C# 6.0以前では、変数の型を判別する場合にis
演算子が使えた。しかし、「変数の型ごとに処理を分けたい」などの目的でダウンキャストする場合は、リスト8のようにas
演算子とnullチェックを使っていた。
void OldPrintStars(object o)
{
if (o is string)
{
WriteLine("must not be string");
return;
}
var i = o as int?;
if (i != null)
{
WriteLine(new string('*', i.Value));
}
}
|
C# 7.0ではis
演算子が拡張され、
- constant pattern
- type pattern
- var pattern
の3つ(詳細後述)が利用できるようになった。リスト9にそのサンプルを載せた。
public void PrintStars(object o)
{
if (o is null) return; // constant pattern "null"
if (o is int.MaxValue) return;
if (o is "a") return;
if (!(o is int i)) // type pattern "int i"
{
WriteLine(i); // is演算子の評価結果がtrueのときのみ変数が割り当てられるので、ここでiを参照するとコンパイルエラー
return;
}
WriteLine(new string('*', i)); // 上のif文でis演算子の評価結果がfalseのときにreturnもしくは例外をスローせずにiを参照すると、確実に代入される保証がないのでコンパイルエラーになる
if (o is var j) // var patternはnullのときも含め常にtrueとなり割り当てられる
{
WriteLine(j);
}
}
|
constant patternは、定数であるかどうかでマッチングする機能だ。nullかどうかのis null
をはじめ、is int.MaxValue
やis "a"
のように、右辺は定数であればよい。
type patternは、C# 6.0以前のis
演算子に似ているが、型の名前だけではなく識別子を加えて変数宣言ができるようになっている。そして、その型に対するis
演算子がtrueを返すときのみ変数がダウンキャストされて代入され、is
演算子の評価結果もtrueになる。is
演算子がfalseと評価される場合は変数が初期化されないため、その後のコードで変数を参照するとコンパイルエラーとなる。
var patternは、nullのときも含めてis
演算子の評価結果が常にtrueとなり、var
により宣言した変数がコンパイルエラーなしで使用できる。
is
演算子の拡張により書きやすくなる例として、C# 6.0で導入されたnull条件演算子を利用して値型のプロパティを返す例が挙げられる。リスト10はC# 6.0でのサンプルだ。Value
は値型のプロパティであるが、変数value
への代入ではvar value = o?.Inner?.Value;
とnull条件演算子が使われているため、Value
の値が代入された変数value
の型はint?
(=Nullable<int>
)となるので、その次のif
文ではnullチェックが必要である。
void OldPrintStarts(SomeObject o)
{
var value = o?.Inner?.Value;
if (value.HasValue)
WriteLine(new string('*', value.Value));
}
class SomeObject
{
public AnotherObject Inner { get; set; }
}
class AnotherObject
{
public int Value { get; set; }
}
|
C# 7.0ではリスト11のようにis
演算子の拡張を使うことで、null条件演算子の評価結果がnullでないときのみ、int
型の変数i
に代入できる(つまりnullチェック不要)。
void PrintStarts(SomeObject o)
{
if (o?.Inner?.Value is int i)
WriteLine(new string('*', i));
}
|
switch文の拡張(Type Switch)
C# 7.0で導入されたパターンマッチングのもう一つの機能はリスト12に載せたswitch
文の拡張だ。
public void PrintShape(object shape)
{
switch (shape)
{
// C# 6.0以前のcase節
case 0:
WriteLine("should not be '0'");
break;
// C# 6.0以前のcaseにガード節追加
case 1 when IsDebug(shape):
WriteLine("should not be '1' if debug is enabled");
break;
// type pattern
// case Circle: のようにプリミティブでない型をC# 6.0以前のように記述するとコンパイルエラー。変数宣言として記述しないといけない
case Circle c:
WriteLine($"circle with radius {c.Radius}");
break;
// type patternにガード節
case Rectangle s when (s.Length == s.Height):
WriteLine($"{s.Length} x {s.Height} square");
break;
// 上のガード節に一致しない場合のtype pattern
case Rectangle r:
WriteLine($"{r.Length} x {r.Height} rectangle");
break;
// var パターンも利用可能
case var i when IsDebug(i):
WriteLine("debug is enabled.");
break;
default:
WriteLine("<unknown shape>");
break;
}
}
|
is
演算子の拡張でも導入されたtype patternやvar patternを記述することで、指定した型にマッチするときのみ、キャストした値を変数に代入できる。C# 6.0以前の文法はconst patternと見ることができる。また、ガード節(case guard)と呼ばれる条件をこれら3つのpatternの後ろに追加して、patternに該当し、かつガード節の条件を満たすときのみ、case
節の中の処理を実行することも可能になった。
以前のswitch
文はcase
節が多数あっても、最悪でも全てのcase
を評価する必要のない実装になっていた。しかし、C# 7.0で拡張されたswitch
文は上から順番に条件判定していくif
文の連続と同等のコンパイル結果になるため、最悪のケースでは全ての条件判定を処理することになる。また、場合によってはcase
節の順番を変えることで挙動が変わることもあり得るだろう。
なお、default
ラベルは例外でdefault
ラベルはどの位置にあっても、全てのcase
の条件に該当しない場合のみ実行されることが保証されている。リスト13のコードはdefault
が先頭に来ているが、変数shape
がCircle
やnullの場合はそれぞれのcase
ラベルが実行される。あまり見かけないものの、default
ラベルの位置は以前から任意の位置に記述できた。switch
文の拡張は上から順番に評価されることになったが、default
はその順番に左右されないため注意が必要だ。
public void PrintShapeWithDefaultFirst(object shape)
{
switch (shape)
{
default:
WriteLine("<unknown shape>");
break;
case Circle c:
WriteLine($"circle with radius {c.Radius}");
break;
case null:
WriteLine("<null>");
break;
}
}
|
また、上から実行されるため、ガード節との組み合わせで明らかに到達不能なcase
節を記述するとコンパイルエラーとなる。ただしリスト14のように実際は到達不能でもコンパイルエラーにならないケースもある。
public void PrintShapeOrder(object shape)
{
switch (shape)
{
case Rectangle r:
WriteLine($"{r.Length} x {r.Height} rectangle");
break;
// 以下のようにコンパイラーが到達不可能であることを検出できる場合はコンパイルエラーとなる
case Rectangle s when (s.Length == s.Height):
WriteLine($"{s.Length} x {s.Height} square");
break;
}
var flg = true;
switch (shape)
{
case Rectangle r when flg:
WriteLine($"{r.Length} x {r.Height} rectangle");
break;
// 以下は到達不可能であるが、現時点でのコンパイラーは検出しない
case Rectangle s when (s.Length == s.Height):
WriteLine($"{s.Length} x {s.Height} square");
break;
}
}
|
3タプル(Tuples)
- ※Visual Studio 2017時点ではタプルを使う場合、NuGetからSystem.ValueTupleをプロジェクトに追加する必要がある。この制限は「Preview時点のみ」という記述もあったが、正式リリース後も追加が必要なままとなっている。なお、.NET Standard 2.0リリースによりこの挙動が変わる可能性もある。
C# 6.0以前でメソッドの返り値に複数のオブジェクトの集合を返したいが独立したクラスとして適切な名前が思いつかない場合、独立したメソッドとせずに匿名オブジェクトとして扱うか、Tuple<T1, T2, ……>
を利用していた。リスト15に例を挙げているが、匿名オブジェクトはメソッドをまたげないこと、Tuple
はプロパティ名が無意味なItem1
、Item2
、……で分かりづらくなることが問題だった。
void OldStyle()
{
// 文字列を全て小文字にしたものと全て大文字にしたものを取得する
// 匿名オブジェクトを利用
var texts =
new[] { "aaA", "bBb", "cCC" }
.Select(x => new { lower = x.ToLower(), upper = x.ToUpper() });
foreach (var text in texts)
{
WriteLine(text);
}
// Tupleを利用
foreach (var text in Convert(new[] { "aaA", "bBb", "cCC" }))
{
WriteLine(text);
}
}
IEnumerable<Tuple<string, string>> Convert(IEnumerable<string> texts)
{
return texts.Select(x => new Tuple<string, string>(x.ToLower(), x.ToUpper()));
}
|
C# 7.0で導入されたタプル型は、実体はValueTuple
クラス(System
名前空間)である。リスト16のように利用できる。また「タプル」という表現がC# 6.0以前のTuple
と紛らわしいため、NuGetからダウンロードしてVisual Studioなどから参照できるcorefx(.NET Coreライブラリ)などのドキュメントコメントではC# 6.0以前のTuple
を「組」と表現している。
void ValueTuple()
{
foreach (var text in ConvertToValueTuple(new[] { "aaA", "bBb", "cCC" }))
{
WriteLine($"lower={text.lower}, upper={text.upper}");
}
}
IEnumerable<(string lower, string upper) > ConvertToValueTuple(IEnumerable<string> texts)
{
return texts.Select(x => (lower: x.ToLower(), upper: x.ToUpper()));
}
|
タプル型を宣言する方法をリスト17にまとめた。タプルリテラルという形式でインスタンス化できる。通常のオブジェクトを初期化するnew
演算子の利用も検討されたが最終的に利用できなくなっている。要素名を一部省略することもできるが、同じ要素名を重複して利用することはできない。
void TupleBasicUsage()
{
var text = "aaAA";
// タプルリテラルで宣言
var t1 = (text.ToLower(), text.ToUpper()); // 要素名は任意
var t2 = (lower: text.ToLower(), upper: text.ToUpper());
// new形式はコンパイルエラー
var t3 = new(int, int)(0, 1);
var t4 = (lower: text.ToLower(), text.ToUpper()); // 一部のみ要素名を省略することも可
WriteLine($"{t4.lower}, {t4.Item2}");
// 同じ要素名で重複するとコンパイルエラー
var t5 = (lower: text.ToLower(), lower: text.ToLower());
}
|
タプルリテラルで宣言したタプルは、該当する型引数を持つValueTuple<T1,T2,……>
型オブジェクトのメンバーに(オプションで追加して要素名で)アクセスできるように振る舞うため、通常のオブジェクトのようにToString
メソッドを呼び出すこともできる。逆に、ToString
などValueTuple
型が持っているメンバー名を要素名に指定することはできない(リスト18)。
また、Item1
、Item2
、……といった要素名は、同じ位置の要素名にのみ利用できる。なお、System.Tuple
は型引数が8個のものまでしか用意されていないため要素数が8個までしか利用できないが、ValueTuple
は要素数が8個以上の場合、ValueTuple<T1,T2,T3,T4,T5,T6,T7, ValueTuple<T8>>
と再帰的に宣言されているためコンパイラー上は要素数の上限なく利用できる。ただし、環境ごとに決まっているスタックの上限に達すると実行時エラーになる。
void TupleUnderlyingTypes()
{
var t = (sum: 0, count: 1) ;
t.sum = 1;
t.Item1 = 1; // 要素名を指定していてもItemNで参照できる。Visual StudioではデフォルトでRoslynの警告メッセージが表示される
var t1 = (0, 1) ;
t1.Item1 = 1; // 要素名を指定していなければ警告メッセージはでない
WriteLine(t.ToString()); // (1, 1) と出力される。ToStringなどValueTupleクラスが持っているメソッドも利用できる
var t3 = (ToString: 0, ObjectEquals: 1); // ValueTupleクラスが持っているメンバーの名前と同じ要素名を指定するとコンパイルエラー
var t4 = (Item1: 0, Item2: 1) ; // Item1、Item2という要素名は利用できるが、
var t5 = (misc: 0, Item1: 1); // 実際の位置と異なるなる位置で利用するとコンパイルエラー
}
|
タプルの型はリスト19に示すとおり要素名に応じた新しい型は生成しない。警告なしに要素名の異なるタプルに代入できる。そのため、コンパイラー警告は表示されるが、宣言しているタプルの要素名と異なる要素名で代入することもできる。
void TupleIdentityConversion()
{
var t = (sum: 0, count: 1);
ValueTuple<int, int> vt = t; // 同一の型に変換
(int moo, int boo) t2 = vt; // 要素名が異なっても同一の型となる
t2.moo = 1;
var n = noo();
}
(int sum, int count) noo()
{
return (count: 1, sum: 3) ; // コンパイラー警告は表示されるが異なる要素名でリテラルを宣言できる
}
|
タプルを含む場合のオーバーライドとオーバーロードの例をリスト20に載せた。オーバーライドする場合は要素名も一致させないとオーバーライドできない。一方、オーバーロードは要素名だけが異なる型は同じシグネチャと見なされてオーバーロードできなくなっている。
class Base
{
public virtual void M1(ValueTuple<int, int> arg) {/*...*/}
}
class Derived : Base
{
public override void M1((int c, int d) arg) {/*...*/} // 要素名が異なるのでオーバーライドできない
}
class Derived2 : Derived
{
public override void M1((int, int) arg) {/*...*/} // 要素名を省略した場合は同一であるためオーバーライド可能
}
class InvalidOverloading
{
public virtual void M1((int c, int d) arg) {/*...*/}
public virtual void M1((int x, int y) arg) {/*...*/} // 要素名が異なるだけではオーバーロードできない
public virtual void M1(ValueTuple<int, int> arg) {/*...*/} // 同じく省略してもオーバーロードできない
public virtual void M1((int c, string d) arg) {/*...*/}
}
|
タプルの要素名はコンパイラーが追跡するが、ランタイムでは要素名は追跡していない。そのため、リスト21のように一度アップキャストした後、ダウンキャストする際に別の要素名を使っても、コンパイルエラーも実行時エラーも発生せず、同じ位置にある要素をそのまま扱える。
void NameErasure()
{
object o = (a: 1, b: 2); // アップキャスト
var t = ((int moo, int boo)) o; // ダウンキャスト可能
WriteLine(t.moo); // 1
}
|
タプルの各要素は型を持っている必要があるため、nullやラムダ式など型を持たないものはそのままでは指定できない。キャストしたり、明示的に型宣言したりすることで指定できる(リスト22)。
void TargetTyping()
{
(string name, byte age) t = (null, 5); // 左辺でstringを指定しているためnullを指定できる
var t2 = ("John", 5);
var t3 = (null, 5); // nullは型を持たないためコンパイルエラー
((1, 2, null), 5).ToString(); // 同じくコンパイルエラー
ImmutableArray.Create((() => 1, 1)); // ラムダ式自体は型を持たないためエラー
ImmutableArray.Create(((Func<int>)(()=>1), 1)); // ラムダ式をキャストしているためOK
}
|
リスト23に、タプルを引数に持つメソッドのオーバーロード解決の例をまとめた。
完全に一致する型があれば、そのメソッドが解決される。
また、完全に一致する型はないが暗黙的な変換で解決できるメソッドがあれば、それが選ばれる。例えばリスト23のOverload
メソッド内の2番目のタプルはTuple<string, string>
に完全に一致する型はないがTuple<object, object>
に変換が可能なため、そのメソッドが解決されている。
void M1((int x, int y) arg) => WriteLine("called M1((int x, int y) arg)");
void M1((object x, object y) arg) => WriteLine("called M1((object x, object y) arg)");
void Overload()
{
M1((1, 2)); // M1((int x, int y) arg) が呼ばれる(完全に一致する型のメソッドがある場合)
M1(("hi", "hello")); // M1((object x, object y) arg) が呼ばれる(暗黙的な変換で解決できるメソッドがある場合)
}
void M2((int x, Func<(int, int)>) arg) => Write("called M2((int x, Func<(int, int)>) arg)");
void M2((int x, Func<(int, byte)>) arg) => WriteLine("called M2((int x, Func<(int, byte)>) arg)");
void Overload2()
{
M2((1, () => (2, 3))); // M2((int x, Func<(int, int)>) arg) が呼ばれる
}
|
4分解(Deconstruction)
タプルが導入されたおかげで複数の要素の組み合わせを独立したクラスとして宣言せずに扱えるようになったが、タプルを受け取る変数の側もタプルのまま変数を受けるのではなく要素ごとに分けて変数として受けたいケースもあるだろう。そこでタプルを要素ごとの変数として受け取る分解(Deconstruction)という機能が導入された。英単語としてはdestructor(デストラクター=インスタンスを破棄する場合に呼ばれるメソッド)と紛らわしい部分もあるが、文法的には別機能である。
分解の基本的な使い方をリスト24に載せた。分解する際には、「左辺の要素がすでに宣言されている変数(文法上は式として扱われる)で、その変数に代入するパターン」と、「左辺の要素が変数宣言であり、変数宣言と同時に分解した変数値を代入するパターン」の2通りがあるが、左辺でこの2つを併用することはできない。また、前述の1Out Varと同じく_
で読み捨てることができるが、これは変数宣言扱いなので式との併用はできない。
void DeconstructionUsage()
{
var tuple = (name: "Tom", age: 34);
string name; int age;
(name, age) = tuple; // 宣言済みの変数(=式)に分解して代入する
WriteLine(age);
int x, y;
(x, y) = new Point(3, 5); // Deconstructメソッド(後述)を持つインスタンスを分解する
WriteLine($"{x} {y}");
// 分解して新しく宣言した変数に代入する
(var newName, var newAge) = tuple;
(var myX, var myY) = new Point(3, 5);
// 式と変数宣言の併用はコンパイルエラー
(x, var y2) = new Point(3, 4);
// Out Varと同じく_で無視できる
(var x1, var _) = new Point(4, -3);
WriteLine($"{x1}");
// var _ は変数宣言扱いなので変数への代入と併用するとコンパイルエラー
(x1, var _) = new Point(4, -3);
}
|
独自に定義したPoint
クラスと、そのDeconstruct
メソッドの内容については、GitHub上のサンプルコードを参照してほしい。
式(=宣言されている変数)に代入するパターン
式に代入する場合をリスト25でもう少し詳しく見てみよう。
式の場合、左辺の要素の型はすでに決まっている。そのため、右辺がタプルリテラルの場合、左辺で定義されたタプルリテラルと同じ型として表記できるリテラルであれば代入可能である。
右辺が宣言済み(=すでに型が決まっている)タプルの場合は、そのタプルの各要素が暗黙的に型変換できれば代入可能であるが、そうでない場合はコンパイルエラーとなる。
右辺がタプルでない場合は、左辺の要素と同じ数のout
パラメーターとそれぞれ(暗黙的に変換可能な)同じ型を持つDeconstruct
メソッドが存在する場合(例えば本稿の例ではPoint
クラスにDeconstruct(out int x, out int y)
メソッドが実装されている)、そのDeconstruct
メソッドの結果が左辺に代入される。
なお、C# 6.0以前の組(System.Tuple
)を分解することもできるが、匿名オブジェクトは分解できない。
void DeconstructionAssignment()
{
string name; byte b;
(name, b) = ("a", 1); // 左辺のbがbyteであるとき、右辺の1はbyteの整数リテラルとなるためOK
(name, b) = ("a", 1234); // 1234はbyteにキャストできないのでコンパイルエラー
var t = ("a", 1);
(name, b) = t; // 暗黙的に型付けするとtは(string, int)である。intをbyteにキャストできないためコンパイルエラー
int x; double y;
(x, y) = new Point(2, 4); // doubleはintから暗黙的に変換できるのでOK
(x, b) = new Point(2, 4); // byteはintから暗黙的に変換できないのでコンパイルエラー(右辺のDeconstructメソッドがすでにタプルの要素をintとして定義しているため)
var p = new int[2];
(p[0], p[1]) = (3, 4); // 左辺の要素が代入可能であれば利用できる
int i;
(i, i) = (1, 2); // 割り当ては要素の順番に従って行われる
WriteLine(i); // 2
}
|
変数宣言と同時に分解した変数値を代入するパターン
新しく宣言した変数に分解する例をリスト26にまとめた。
こちらも同じく暗黙的な型変換ができれば代入可能であり、暗黙的に宣言した変数も利用できる。
また、Deconstruct
メソッドが拡張メソッドの場合は解決できず、コンパイルエラーとなる。
Deconstruct
メソッドがオーバーロードされている場合、要素数が同じDeconstruct
メソッドが他にない場合は問題ない。つまり、out
パラメーターが2つのDeconstruct
メソッドが1つ、3つのメソッドが1つ、……とオーバーロードされていれば、左辺の要素数に応じて該当するDeconstruct
メソッドが利用される。しかし、out
パラメーターの数が同じDeconstruct
メソッドを複数オーバーロードする場合、メソッドを宣言する場合は問題ないが、out
パラメーターの数と同じ要素数のタプルに分解しようとするところでコンパイルエラーとなるので、注意が必要である。
void DeconstuctionDeclaration()
{
// 暗黙的な型変換ができればOK
(double myX, long myY) = new Point(3, 5);
// 拡張メソッドは解決できないためコンパイルエラー
(byte x2, byte y2) = new Point(3, 4);
(var s1, var y) = new C1();
// outパラメーターの数が同じ複数のDeconstructメソッドがオーバーロードされている場合(A)、
// 左辺の要素の数と同じ数だけ分解するためにそのメソッドが使われる際にコンパイルエラーになる
(string x2, bool b2, int y2) = new C1();
}
static class PointExtensions
{
// Pointクラスの拡張メソッドとして新たなDeconstructメソッドを実装
public static void Deconstruct(this Point p, out byte x, out byte y)
{
x = (byte)p.X;
y = (byte)p.Y;
}
}
class C1
{
public void Deconstruct(out string x, out int y)
{
x = "a";
y = 1;
}
// (A)outパラメーターの数が同じ複数のDeconstructメソッドをオーバーロード宣言すること自体は許可されている
public void Deconstruct(out string x, out bool b, out int y)
{
x = "a";
b = true;
y = 1;
}
// 上のメソッドと同数のoutパラメーターを持つDeconstructメソッド
public void Deconstruct(out string x, out bool b1, out bool b2)
{
x = "a";
b1 = true;
b2 = false;
}
}
|
■
以上、前編では「データ中心設計」に関連する4つの新機能を説明した。後編では「パフォーマンス向上」と「コード記述の単純化」に関連する6つの新機能を説明しているので、ぜひ次のページもまとめて読み通してほしい。
1. C# 6.0で知っておくべき12の新機能
Visual Studio 2015正式版のリリースで利用可能になったC#言語の最新バージョン「6.0」の新機能を解説する。CTP 5→正式版に合わせて改訂。
2. 【現在、表示中】≫ C# 7.0で知っておくべき10の新機能(前編)
Visual Studio 2017およびVisual Studio Codeで利用可能になったC#言語の新バージョン「7.0」の新機能を、公開されている議論を基に解説。前編として「データ中心設計」に関連する4つの新機能を説明する。
3. C# 7.0で知っておくべき10の新機能(後編)
Visual Studio 2017およびVisual Studio Codeで利用可能になったC#言語の新バージョン「7.0」の新機能を、公開されている議論を基に解説。前編として「パフォーマンス向上」と「コード記述の単純化」に関連する6つの新機能を説明する。