最新C#言語概説

最新C#言語概説

C# 7.0で知っておくべき10の新機能(前編)

2017年3月10日 改訂 (初版:2017/02/23)

Visual Studio 2017およびVisual Studio Codeで利用可能になったC#言語の新バージョン「7.0」の新機能を、公開されている議論を基に解説。前編として「データ中心設計」に関連する4つの新機能を説明する。

  • このエントリーをはてなブックマークに追加

 C#の新バージョン「7.0」は、Visual Studio 2017、もしくは.NET Core SDKVisual 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新機能の一覧】

 この分類はMSDN .NET BlogのエントリWhat’s New in C# 7.0に基づいている。また本記事で利用している全てのサンプルコードを1つのプロジェクトにまとめたものをGitHubのリポジトリとして公開している。

データ中心設計(data consumption)

▲新機能の一覧に戻る

1outパラメーター付き引数での変数宣言(Out Var)

 C#には以前からoutパラメーター修飾子という引数を参照渡しする仕組みがあった。リスト1のようにoutキーワードを引数に指定してメソッドを宣言し、リスト2のようにoutキーワードを付けてメソッドを呼び出すのが一般的な利用法だ。

C#
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;
  }
}
リスト1 outキーワード付きのメソッド宣言(Point.cs)
C#
void OldStyle(Point p)
{
  int x, y; //事前に変数を宣言する必要があった
  p.GetCoordinates(out x, out y);
  WriteLine($"({x}, {y})");
}
リスト2 C# 6.0以前でのoutキーワード付きのメソッド呼び出し(CS7_01_OutVariables.cs)

 この書き方だと事前に変数宣言を記述する必要があったが、C# 7.0ではリスト3のように引数リストの中で変数宣言が同時にできるようになった。明示的に型指定することに加えて、varを使って暗黙的に型指定することもできる。また、変数のスコープは従来と同じく変数宣言しているメソッド呼び出しと同じスコープ内となるが、変数宣言より前に参照することはできない。

C#
void PrintCoordinates(Point p)
{
  WriteLine(x1); // 変数のスコープ内であっても宣言前には参照するとコンパイルエラー
  p.GetCoordinates(out var x1, out int y1);
  WriteLine($"(x1,y1)=({x1}, {y1})"); // 変数のスコープはメソッド呼び出しと同じスコープ内
}
リスト3 C# 7.0で導入されたoutキーワードにおける変数宣言(CS7_01_OutVariables.cs)

 リスト4のようにTryParseメソッドのようにbool型の返り値を持ったoutキーワード付きのメソッドはif文の条件に利用できるが、その場合、変数のスコープはif文内だけではなく、if文を記述しているスコープ内となる。

C#
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を参照できる
}
リスト4 if文の条件に利用した場合のスコープはifを含んだスコープ内となる(CS7_01_OutVariables.cs)

 また、outキーワードの引数をその後、参照する必要がない場合、リスト5のように_を変数に指定できる。なお、指定した_は通常の変数とは別であるため、後から参照することはできず、同じスコープ内に変数_が存在してもエラーとはならず宣言した変数_に影響も与えない。

C#
void DeclareUnderscore(Point p)
{
  var _ = 1;
  p.GetCoordinates(out var _, out int y);
  WriteLine(_); // 「1」と出力される
}
リスト5 変数宣言した_と、outキーワードでの変数宣言に使う_の併用(CS7_01_OutVariables.cs)

 通常あまり使われないと思われるが、リスト6のように引数リストで変数宣言した変数は、その後に続く引数で代入できる。これは明示的に型指定したときのみ許され、暗黙的に型指定した場合はコンパイルエラーとなる。

C#
void UseDeclareAgain(Point p)
{
  p.GetXSetY(out int x, x = 2);
  p.GetXSetY(out var x1, x1 = 2); // 暗黙的な場合
}
リスト6 明示的に型を指定して宣言したoutキーワードの変数は、その後の引数リストで代入可能だが、暗黙的な型の変数には代入不可能(CS7_01_OutVariables.cs)

 この機能の導入により、オーバーロードの解決条件に、暗黙的に型指定している引数リストでの変数宣言は、任意の型に暗黙的に変換できることが考慮されるようになった。具体例を挙げると、リスト7のようなオーバーロードされるメソッドを宣言することはできるが、このメソッドを利用する場合は明示的に型指定しないとコンパイルエラーとなる。単独でvar i = 1;と宣言すると、C#言語規約上、iint型となるが、オーバーロード解決時には考慮されず優先順位が付けられないことになっている。

C#
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だとオーバーロード解決できずコンパイルエラー
}
リスト7 outパラメーターの付いた引数を持つメソッドのオーバーロード(Point.cs)

▲新機能の一覧に戻る

2パターンマッチング(Pattern matching)

 データ中心の設計を取り入れるために、C#では代数的データ型や関数型言語でのパターンマッチングの機能の導入を検討してきた。当初はC# 7.0でより多くの機能を導入する可能性もあったが、最終的にis演算子の拡張とswitch文の拡張の2つがパターンマッチング機能として追加された。なお、パターンマッチングの機能に関しては「F#およびScalaの機能を参考にした」と記述されている。

is演算子の拡張

 C# 6.0以前では、変数の型を判別する場合にis演算子が使えた。しかし、「変数の型ごとに処理を分けたい」などの目的でダウンキャストする場合は、リスト8のようにas演算子とnullチェックを使っていた。

C#
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));
  }
}
リスト8 C# 6.0以前のダウンキャストのコードサンプル(CS7_02_PatternMatching.cs)

 C# 7.0ではis演算子が拡張され、

  • constant pattern
  • type pattern
  • var pattern

の3つ(詳細後述)が利用できるようになった。リスト9にそのサンプルを載せた。

C#
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);
  }
}
リスト9 is演算子の拡張のコードサンプル(CS7_02_PatternMatching.cs)

 constant patternは、定数であるかどうかでマッチングする機能だ。nullかどうかのis nullをはじめ、is int.MaxValueis "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チェックが必要である。

C#
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; }
}
リスト10 C# 6.0以前のnull条件演算子の結果をキャストする例(CS7_02_PatternMatching.cs)

 C# 7.0ではリスト11のようにis演算子の拡張を使うことで、null条件演算子の評価結果がnullでないときのみ、int型の変数iに代入できる(つまりnullチェック不要)。

C#
void PrintStarts(SomeObject o)
{
  if (o?.Inner?.Value is int i)
    WriteLine(new string('*', i));
}
リスト11 is演算子の拡張を使ったnull条件算子の結果をキャストする例(CS7_02_PatternMatching.cs)
switch文の拡張(Type Switch)

 C# 7.0で導入されたパターンマッチングのもう一つの機能はリスト12に載せたswitch文の拡張だ。

C#
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;
  }
}
リスト12 switch文の拡張のサンプルコード(CS7_02_PatternMatching.cs)

 is演算子の拡張でも導入されたtype patternvar 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が先頭に来ているが、変数shapeCirclenullの場合はそれぞれのcaseラベルが実行される。あまり見かけないものの、defaultラベルの位置は以前から任意の位置に記述できた。switch文の拡張は上から順番に評価されることになったが、defaultはその順番に左右されないため注意が必要だ。

C#
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;
  }
}
リスト13 defaultラベルが最初に来るswitch文の例(CS7_02_PatternMatching.cs)

 また、上から実行されるため、ガード節との組み合わせで明らかに到達不能なcase節を記述するとコンパイルエラーとなる。ただしリスト14のように実際は到達不能でもコンパイルエラーにならないケースもある。

C#
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;
  }
}
リスト14 到達不可能なcaseラベルがコンパイルエラーになるケースと到達不可能と検出されないケース(CS7_02_PatternMatching.cs)

▲新機能の一覧に戻る

3タプル(Tuples)

  • Visual Studio 2017時点ではタプルを使う場合、NuGetからSystem.ValueTupleをプロジェクトに追加する必要がある。この制限は「Preview時点のみ」という記述もあったが、正式リリース後も追加が必要なままとなっている。なお、.NET Standard 2.0リリースによりこの挙動が変わる可能性もある。

 C# 6.0以前でメソッドの返り値に複数のオブジェクトの集合を返したいが独立したクラスとして適切な名前が思いつかない場合、独立したメソッドとせずに匿名オブジェクトとして扱うか、Tuple<T1, T2, ……>を利用していた。リスト15に例を挙げているが、匿名オブジェクトはメソッドをまたげないこと、Tupleはプロパティ名が無意味なItem1Item2、……で分かりづらくなることが問題だった。

C#
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()));
}
リスト15 C# 6.0以前の匿名オブジェクトとTupleの利用例(CS7_03_Tuples.cs)

 C# 7.0で導入されたタプル型は、実体はValueTupleクラス(System名前空間)である。リスト16のように利用できる。また「タプル」という表現がC# 6.0以前のTupleと紛らわしいため、NuGetからダウンロードしてVisual Studioなどから参照できるcorefx(.NET Coreライブラリ)などのドキュメントコメントではC# 6.0以前のTupleを「組」と表現している。

C#
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()));
}
リスト16 ValueTupleのコード例(CS7_03_Tuples.cs)

 タプル型を宣言する方法をリスト17にまとめた。タプルリテラルという形式でインスタンス化できる。通常のオブジェクトを初期化するnew演算子の利用も検討されたが最終的に利用できなくなっている。要素名を一部省略することもできるが、同じ要素名を重複して利用することはできない。

C#
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());
}
リスト17 タプルの宣言例(CS7_03_Tuples.cs)

 タプルリテラルで宣言したタプルは、該当する型引数を持つValueTuple<T1,T2,……>型オブジェクトのメンバーに(オプションで追加して要素名で)アクセスできるように振る舞うため、通常のオブジェクトのようにToStringメソッドを呼び出すこともできる。逆に、ToStringなどValueTuple型が持っているメンバー名を要素名に指定することはできない(リスト18)。

 また、Item1Item2、……といった要素名は、同じ位置の要素名にのみ利用できる。なお、System.Tupleは型引数が8個のものまでしか用意されていないため要素数が8個までしか利用できないが、ValueTupleは要素数が8個以上の場合、ValueTuple<T1,T2,T3,T4,T5,T6,T7, ValueTuple<T8>>と再帰的に宣言されているためコンパイラー上は要素数の上限なく利用できる。ただし、環境ごとに決まっているスタックの上限に達すると実行時エラーになる。

C#
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);           // 実際の位置と異なるなる位置で利用するとコンパイルエラー
}
リスト18 タプルの要素名の使い方の例(CS7_03_Tuples.cs)

 タプルの型はリスト19に示すとおり要素名に応じた新しい型は生成しない。警告なしに要素名の異なるタプルに代入できる。そのため、コンパイラー警告は表示されるが、宣言しているタプルの要素名と異なる要素名で代入することもできる。

C#
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) ;  // コンパイラー警告は表示されるが異なる要素名でリテラルを宣言できる
}
リスト19 タプルを要素名の異なる同じ型に代入する例(CS7_03_Tuples.cs)

 タプルを含む場合のオーバーライドとオーバーロードの例をリスト20に載せた。オーバーライドする場合は要素名も一致させないとオーバーライドできない。一方、オーバーロードは要素名だけが異なる型は同じシグネチャと見なされてオーバーロードできなくなっている。

C#
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) {/*...*/}
}
リスト20 タプルのオーバーロードの利用例(CS7_03_Tuples.cs)

 タプルの要素名はコンパイラーが追跡するが、ランタイムでは要素名は追跡していない。そのため、リスト21のように一度アップキャストした後、ダウンキャストする際に別の要素名を使っても、コンパイルエラーも実行時エラーも発生せず、同じ位置にある要素をそのまま扱える。

C#
void NameErasure()
{
  object o = (a: 1, b: 2);        // アップキャスト
  var t = ((int moo, int boo)) o; // ダウンキャスト可能
  WriteLine(t.moo);  // 1
}
リスト21 タプルのキャスト例(CS7_03_Tuples.cs)

 タプルの各要素は型を持っている必要があるため、nullやラムダ式など型を持たないものはそのままでは指定できない。キャストしたり、明示的に型宣言したりすることで指定できる(リスト22)。

C#
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
}
リスト22 nullやラムダ式をタプルの要素に指定する例(CS7_03_Tuples.cs)

 リスト23に、タプルを引数に持つメソッドのオーバーロード解決の例をまとめた。

 完全に一致する型があれば、そのメソッドが解決される。

 また、完全に一致する型はないが暗黙的な変換で解決できるメソッドがあれば、それが選ばれる。例えばリスト23のOverloadメソッド内の2番目のタプルはTuple<string, string>に完全に一致する型はないがTuple<object, object>に変換が可能なため、そのメソッドが解決されている。

C#
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) が呼ばれる
}
リスト23 タプルを使ったオーバーロードの解決例(CS7_03_Tuples.cs)

▲新機能の一覧に戻る

4分解(Deconstruction)

 タプルが導入されたおかげで複数の要素の組み合わせを独立したクラスとして宣言せずに扱えるようになったが、タプルを受け取る変数の側もタプルのまま変数を受けるのではなく要素ごとに分けて変数として受けたいケースもあるだろう。そこでタプルを要素ごとの変数として受け取る分解Deconstruction)という機能が導入された。英単語としてはdestructor(デストラクター=インスタンスを破棄する場合に呼ばれるメソッド)と紛らわしい部分もあるが、文法的には別機能である。

 分解の基本的な使い方をリスト24に載せた。分解する際には、「左辺の要素がすでに宣言されている変数(文法上は式として扱われる)で、その変数に代入するパターン」と、「左辺の要素が変数宣言であり、変数宣言と同時に分解した変数値を代入するパターン」の2通りがあるが、左辺でこの2つを併用することはできない。また、前述の1Out Varと同じく_で読み捨てることができるが、これは変数宣言扱いなので式との併用はできない。

C#
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);
}
リスト24 分解の利用例(CS7_04_Deconstruction.cs)

独自に定義したPointクラスと、そのDeconstructメソッドの内容については、GitHub上のサンプルコードを参照してほしい。

式(=宣言されている変数)に代入するパターン

 式に代入する場合をリスト25でもう少し詳しく見てみよう。

 式の場合、左辺の要素の型はすでに決まっている。そのため、右辺がタプルリテラルの場合、左辺で定義されたタプルリテラルと同じ型として表記できるリテラルであれば代入可能である。

 右辺が宣言済み(=すでに型が決まっている)タプルの場合は、そのタプルの各要素が暗黙的に型変換できれば代入可能であるが、そうでない場合はコンパイルエラーとなる。

 右辺がタプルでない場合は、左辺の要素と同じ数のoutパラメーターとそれぞれ(暗黙的に変換可能な)同じ型を持つDeconstructメソッドが存在する場合(例えば本稿の例ではPointクラスにDeconstruct(out int x, out int y)メソッドが実装されている)、そのDeconstructメソッドの結果が左辺に代入される。

 なお、C# 6.0以前の組(System.Tuple)を分解することもできるが、匿名オブジェクトは分解できない。

C#
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
}
リスト25 タプルを分解して式に代入するコード例(CS7_04_Deconstruction.cs)
変数宣言と同時に分解した変数値を代入するパターン

 新しく宣言した変数に分解する例をリスト26にまとめた。

 こちらも同じく暗黙的な型変換ができれば代入可能であり、暗黙的に宣言した変数も利用できる。

 また、Deconstructメソッドが拡張メソッドの場合は解決できず、コンパイルエラーとなる。

 Deconstructメソッドがオーバーロードされている場合、要素数が同じDeconstructメソッドが他にない場合は問題ない。つまり、outパラメーターが2つのDeconstructメソッドが1つ、3つのメソッドが1つ、……とオーバーロードされていれば、左辺の要素数に応じて該当するDeconstructメソッドが利用される。しかし、outパラメーターの数が同じDeconstructメソッドを複数オーバーロードする場合、メソッドを宣言する場合は問題ないが、outパラメーターの数と同じ要素数のタプルに分解しようとするところでコンパイルエラーとなるので、注意が必要である。

C#
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;
  }
}
リスト26 タプルを分解して新しい変数宣言式に代入するコード例(CS7_04_Deconstruction.cs)

 以上、前編では「データ中心設計」に関連する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つの新機能を説明する。

イベント情報(メディアスポンサーです)

Azure Central の記事内容の紹介

Twitterでつぶやこう!


Build Insider賛同企業・団体

Build Insiderは、以下の企業・団体の支援を受けて活動しています(募集概要)。

ゴールドレベル

  • 日本マイクロソフト株式会社
  • グレープシティ株式会社