Connect(); 2016

Volume 31 Number 12

.NET Framework - C# 7.0 の新機能

Mark Michaelis

2015 年 12 月に、C# 7.0 の設計について取り上げました (msdn.com/magazine/mt595758)。昨年は多くのことを変更しましたが、チームは現在 C# 7.0 における開発の最終確認を行っており、実質的には C# 7.0 のすべての新機能を Visual Studio 2017 RC に実装しています (実質的というのは、Visual Studio 2017 が実際にリリースされるまでにさらに変更が加えられる可能性を否定できないためです)。 簡単な概要については、itl.tc/CSharp7FeatureSummary (英語) の概要の表で確認してください。ここでは各新機能を詳しく見ていきます。

デコンストラクター

C# 1.0 以降、複数のパラメーターをまとめて 1 つのクラスにカプセル化する関数 (コンストラクター) を呼び出せるようになっています。ただし、オブジェクトを元の構成要素に戻すのに便利な手段はありませんでした。たとえば、ファイル名の各要素 (ディレクトリ名、ファイル名、拡張子) を受け取り、これらの要素を組み合わせてオブジェクトにすることで、オブジェクトのさまざまな要素に対する操作をサポートする PathInfo クラスを考えます。ここで、このオブジェクトから元の構成要素を抽出 (分解) する機能が求められるとします。

C# 7.0 では、デコンストラクターによりこの作業が簡単になります。デコンストラクターは具体的に特定されるオブジェクトの要素を返します。デストラクター (確定的なオブジェクトの割り当て解除とクリーンアップ) やファイナライザー (itl.tc/CSharpFinalizers、英語) とは混同しないようにしてください。

図 1 に示すのが、PathInfo クラスです。

図 1 デコンストラクターを備えた PathInfo クラスと関連テスト

public class PathInfo
{
  public string DirectoryName { get; }
  public string FileName { get; }
  public string Extension { get; }
  public string Path
  {
    get
    {
      return System.IO.Path.Combine(
        DirectoryName, FileName, Extension);
    }
  }
  public PathInfo(string path)
  {
    DirectoryName = System.IO.Path.GetDirectoryName(path);
    FileName = System.IO.Path.GetFileNameWithoutExtension(path);
    Extension = System.IO.Path.GetExtension(path);
  }
  public void Deconstruct(
    out string directoryName, out string fileName, out string extension)
  {
    directoryName = DirectoryName;
    fileName = FileName;
    extension = Extension;
  }
  // ...
}

当然、C# 1.0 と同じような方法で Deconstruct メソッドを呼び出すことも可能です。ただし、C# 7.0 にはこの呼び出しを大幅に単純化する糖衣構文が用意されています。デコンストラクターの宣言が渡されたら、C# 7.0 の新しい「タプルのような」構文を使用して、デコンストラクターを呼び出します (図 2 参照)。

図 2 デコンストラクターの呼び出しと割り当て

PathInfo pathInfo = new PathInfo(@"\\test\unc\path\to\something.ext");
{
  // Example 1: Deconstructing declaration and assignment.
  (string directoryName, string fileName, string extension) = pathInfo;
  VerifyExpectedValue(directoryName, fileName, extension);
}
{
  string directoryName, fileName, extension = null;
  // Example 2: Deconstructing assignment.
  (directoryName, fileName, extension) = pathInfo;
  VerifyExpectedValue(directoryName, fileName, extension);
}
{
  // Example 3: Deconstructing declaration and assignment with var.
  var (directoryName, fileName, extension) = pathInfo;
  VerifyExpectedValue(directoryName, fileName, extension);
}

C# では初めて、複数の変数に異なる値を同時に代入できるようになります。これは、すべての変数が同じ値 (null) に初期化される次のような null 代入宣言とは異なります。

string directoryName, filename, extension = null;

代わりに、新しいタプルのような構文を使うと、各変数の名前に応じて異なる値が代入されるのではなく、宣言と Deconstruct ステートメント内での指定順序に応じて値が代入されます。

ご想像どおり、出力パラメーターの型は代入される変数の型と一致する必要があります。var を使用できるのは、Deconstruct パラメーターの型から型を推測できるためです。ただし、図 2 の Example 3 に示すように、単一の var をかっこの外側に置くことはできますが、現時点ではすべての変数が同じ型であっても、string を外側に置くことはできません。

現状、C# 7.0 のタプルのような構文では、かっこ内に 2 つ以上の変数が含まれている必要があります。たとえば、次のようなデコンストラクターが存在していても (FileInfo path) = pathInfo; は許可されません。

public void Deconstruct(out FileInfo file)

つまり、C# 7.0 では、出力パラメーターが 1 つだけしかない Deconstruct メソッドのデコンストラクター構文は使用できません。

タプルの操作

前述のとおり、上記のそれぞれの例は C# 7.0 のタプルのような構文を利用していました。この構文の特徴は、代入される複数の変数 (またはプロパティ) をかっこで囲むことです。「タプルのような」という表現を使用している理由は、実際のところ、今回のデコンストラクターの例ではいずれもタプル型を内部で使用していないためです (実は、デコンストラクター構文によるタプルの代入は許可されておらず、おそらく、その必要もありません。それは、既に代入済みのオブジェクトが、カプセル化された構成要素を表すインスタンスであるためです)。

C# 7.0 では、タプルを扱う効率の高い特別な構文が用意されています (図 3 参照)。この構文は、宣言、キャスト演算子、型パラメーターなど、型指定子を使用できるところではどこでも使えます。

図 3 C# 7.0 タプル構文の宣言、インスタンス作成、および使用

[TestMethod]
public void Constructor_CreateTuple()
{
  (string DirectoryName, string FileName, string Extension) pathData =
    (DirectoryName: @"\\test\unc\path\to",
    FileName: "something",
    Extension: ".ext");
  Assert.AreEqual<string>(
    @"\\test\unc\path\to", pathData.DirectoryName);
  Assert.AreEqual<string>(
    "something", pathData.FileName);
  Assert.AreEqual<string>(
    ".ext", pathData.Extension);
  Assert.AreEqual<(string DirectoryName, string FileName, string Extension)>(
    (DirectoryName: @"\\test\unc\path\to",
      FileName: "something", Extension: ".ext"),
    (pathData));
  Assert.AreEqual<(string DirectoryName, string FileName, string Extension)>(
    (@"\\test\unc\path\to", "something", ".ext"),
    (pathData));
  Assert.AreEqual<(string, string, string)>(
    (@"\\test\unc\path\to", "something", ".ext"), (pathData));
  Assert.AreEqual<Type>(
    typeof(ValueTuple<string, string, string>), pathData.GetType());
}
[TestMethod]
public void ValueTuple_GivenNamedTuple_ItemXHasSameValuesAsNames()
{
  var normalizedPath =
    (DirectoryName: @"\\test\unc\path\to", FileName: "something",
    Extension: ".ext");
  Assert.AreEqual<string>(normalizedPath.Item1, normalizedPath.DirectoryName);
  Assert.AreEqual<string>(normalizedPath.Item2, normalizedPath.FileName);
  Assert.AreEqual<string>(normalizedPath.Item3, normalizedPath.Extension);
}
static public (string DirectoryName, string FileName, string Extension)
  SplitPath(string path)
{
  // See http://bit.ly/2dmJIMm Normalize method for full implementation.
  return (          
    System.IO.Path.GetDirectoryName(path),
    System.IO.Path.GetFileNameWithoutExtension(path),
    System.IO.Path.GetExtension(path)
    );
}

タプルに詳しくない方にのために説明しておくと、タプルとは、複数の型を、それらの型を格納する 1 つの型にまとめる方法の 1 つで、その構文は軽量です。この 1 つの型は、そのインスタンスが作成されるメソッドの外側で使用することができます。この構文が軽量なのは、クラスや構造体の定義とは異なり、実行時にインラインでタプルを「宣言」できるためです。ただし、インライン宣言とインスタンス作成をサポートする動的な型とは異なり、タプルはそこに含まれているメンバーに外部からアクセスでき、API の一部として組み込むことも可能です。外部 API をサポートするにもかかわらず、タプルにはバージョン互換の拡張手段がありません (ただし、型パラメーター自体が派生をサポートしている場合を除きます)。そのため、パブリック API ではタプルを慎重に使用する必要があります。したがって、パブリック API の戻り値に関しては標準のクラスを使用することをお勧めします。

C# 7.0 以前から既に、.NET Framework ではタプル クラス System.Tuple<…> が用意されていました (Microsoft .NET Framework 4 で導入)。ただし、C# 7.0 は以前のソリューションと異なります。それは、セマンティックな目的が宣言に組み込まれており、タプル値型の   System.ValueTuple<…> を導入しているためです。

では、セマンティックな目的を見てみましょう。図 3 の C# 7.0 のタプル構文では、タプルに含まれる各 ItemX 要素ごとにエイリアス名を宣言しています。たとえば、図 3 の pathData タプル インスタンスは、厳密に型指定された DirectoryName: 文字列、FileName: 文字列、および Extension: 文字列を定義しているため、pathData.DirectoryName のように呼び出すことができます。これは重要な機能強化です。C# 7.0 以前は、ItemX という名前しか使用できませんでした (この X は要素ごとにインクリメントされます)。

現在では、C# 7.0 のタプルの要素は厳密に型指定されるものの、型定義の名前自体は区別されません。したがって、異なる名前のエイリアスで 2 つのタプルを割り当てることができます。この場合、右辺にある名前が無視されることを知らせる警告が表示されるだけです。

// Warning: The tuple element name 'AltDirectoryName1' is ignored
// because a different name is specified by the target type...
(string DirectoryName, string FileName, string Extension) pathData =
  (AltDirectoryName1: @"\\test\unc\path\to",
  FileName: "something", Extension: ".ext");

同様に、すべてのエイリアス要素の名前を定義していない場合でもタプルを他のタプルに代入することができます。

// Warning: The tuple element name 'directoryName', 'FileNAme' and 'Extension'
// are ignored because a different name is specified by the target type...
(string, string, string) pathData =
  (DirectoryName: @"\\test\unc\path\to", FileName: "something", Extension: ".ext");

誤解のないようにいうと、各要素の型と順序によって型の互換性が定義されます。無視されるのは要素の名前のみです。ただし、名前の違いが無視されるとしても、IDE 内では引き続き名前によって IntelliSense が提供されます。

要素名のエイリアスが定義されてるかどうかにかかわらず、すべてのタプルは ItemX の名前を持ちます。この X は要素の番号に対応します。ItemX という名前により C# 6.0 以降でタプルが利用できるようになるため、この名前は重要です。ただし、エイリアス要素の名前は重要ではありません。

注意を必要とする重要な点がもう 1 つあります。それは、C# 7.0 の基になるタプル型が System.ValueTuple であることです。コンパイル対象の .NET Framework バージョンで System.ValueTuple を利用できないとしても、NuGet パッケージを使えばアクセスすることができます。

タプル内部の詳細については、intellitect.com/csharp7tupleiinternals (英語) を参照してください。

is 式によるパターン マッチング

たとえば Storage のような基底クラスがあり、そこから DVD、UsbKey、HardDrive、FloppyDrive (憶えていますか) などのクラスを派生する場合があります。各クラスに Eject メソッドを実装するために利用できるオプションはいくつかあります。

  • as 演算子
    • as 演算子を使用してキャストして代入する
    • 結果が null の場合をチェックする
    • イジェクト操作を実行する
  • is 演算子
    • is 演算子を使用して型をチェックする
    • 型をキャストして代入する
    • イジェクト操作を実行する
  • キャスト
    • 明示的にキャストして代入する
    • 可能性がある例外をキャッチする
    • 操作を実行する
    • こんな面倒な手順は勘弁してほしいですよね。

これらよりはるかに優れた第 4 のアプローチとして、ポリモーフィズムを使用する方法があります。そこでは、仮想関数を用いてディスパッチします。ただし、これを利用できるのは Storage クラスのソース コードがあり、そこに Eject メソッドを追加できる場合に限られます。ここではこのオプションを使用できないものとします。そこで必要になるのはパターン マッチングです。

先ほどの各アプローチの問題は、構文が非常に冗長で、キャスト先のクラスごとに必ず複数のステートメントが必要になることです。C# 7.0 では、テストと代入を 1 つの操作にまとめる手段としてパターン マッチングが提供されます。結果として、図 4 のコードが図 5 に示すようなコードに単純化されます。

図 4 パターン マッチングを使用しない型キャスト

// Eject without pattern matching.
public void Eject(Storage storage)
{
  if (storage == null)
  {
    throw new ArgumentNullException();
  }
  if (storage is UsbKey)
  {
    UsbKey usbKey = (UsbKey)storage;
    if (usbKey.IsPluggedIn)
    {
      usbKey.Unload();
      Console.WriteLine("USB Drive Unloaded.");
    }
    else throw new NotImplementedException();    }
  else if(storage is DVD)
  // ...
  else throw new NotImplementedException();
}

図 5 パターン マッチングを使用する型キャスト

// Eject with pattern matching.
public void Eject(Storage storage)
{
  if (storage is null)
  {
    throw new ArgumentNullException();
  }
  if ((storage is UsbKey usbDrive) && usbDrive.IsPluggedIn)
  {
    usbDrive.Unload();
    Console.WriteLine("USB Drive Unloaded.");
  }
  else if (storage is DVD dvd && dvd.IsInserted)
  // ...
  else throw new NotImplementedException();  // Default
}

この 2 つのコードに大差はありませんが、頻繁に実行する場合 (派生型ごとになど)、前者の構文には厄介な C# の特異性があります。型のテスト、宣言、代入を 1 つの操作にまとめる C# 7.0 の改善により、図 4 の構文はおそらく使われなくなります。前者の構文では、識別子を代入しないで型をチェックすると、「既定」の else まで進むのは好意的に捉えても面倒です。対照的に、識別子を代入すると、型のチェックだけにとどまらず、追加の条件文も実行されます。

図 5 のコードは、最初にパターン マッチングの is 演算子を null 比較演算子のサポートと共に使用しています。

if (storage is null) { ... }

switch ステートメントによるパターン マッチング

is 演算子を使ってパターン マッチングをサポートすることでも改善されますが、swtich ステートメントでパターン マッチングをサポートすると、さらに大きく改善されます。少なくとも、変換先に互換性のある型が複数存在する場合は特に効果が大きくなります。これは、C# 7.0 にはパターン マッチングを伴う case ステートメントがあり、case ステートメント内で型のパターンが一致すれば、識別子の指定、代入、アクセスをすべてその case ステートメント内で行うことができるためです。図 6 に例を示します。

図 6 switch ステートメントでのパターン マッチング

public void Eject(Storage storage)
{
  switch(storage)
  {
    case UsbKey usbKey when usbKey.IsPluggedIn:
      usbKey.Unload();
      Console.WriteLine("USB Drive Unloaded.");
      break;
    case DVD dvd when dvd.IsInserted:
      dvd.Eject();
      break;
    case HardDrive hardDrive:
      throw new InvalidOperationException();
    case null:
    default:
      throw new ArgumentNullException();
  }
}

この例では、case ステートメント内で usbKey や dvd などのローカル変数が宣言され、自動的に代入されています。スコープはその case ステートメント内に限定されます。

ただし、変数の宣言と代入と同程度に重要なのは、when 句を使用して case ステートメントに付加できる追加の条件です。そのため、case ステートメントは、そのステートメント内にフィルターを追加することなく、無効なシナリオを完全にフィルターで除外できます。これには実は、前の case ステートメントの条件が完全に一致しなかった場合に、次の case ステートメントを評価できるメリットもあります。また、case ステートメントが定数に限定されず、switch 式に任意の型を使用できることも意味します。つまり、使用できる型はブール型、char 型、文字列型、整数型、および列挙型には限定されなくなります。

C# 7.0 の新しいパターン マッチング swtich ステートメント機能によって導入される重要な特徴がもう 1 つあります。それは、case ステートメントの順序が意味を持ち、コンパイル時に検証されることです (以前のバージョンの C# では、パターン マッチングがなく、case ステートメントの順序は重要ではありませんでした)。 たとえば、Storage の case ステートメントを、Storage から派生するパターン マッチング case ステートメント (UsbKey、DVD、および HardDrive) よりも前に実装すると、case Storage により、(Storage から派生する) 他の型のパターン マッチングはすべて意味がなくなります。基本データ型の case ステートメントによって他の派生型の case ステートメントが評価されなくなると、評価されない case ステートメントがコンパイル エラーになります。このように、case ステートメントの順序の要件は catch ステートメントの要件に似ています。

null 値に対する is 演算子は false を返すことを思い出してください。したがって、型のパターン マッチングの case ステートメントは null 値を持つ switch 式には一致しません。そのため、null 値の case ステートメントの順序は重要にならず、この動作はパターン マッチングが導入される前の switch ステートメントに一致します。

また、C# 7.0 以前の swtich ステートメントとの互換性がサポートされるため、default ケースは常に最後に評価されます。このとき case ステートメントの順序において default ケースを指定する位置は関係ありません (とはいえ、必ず最後に評価されるため、読みやすさのために一般的には default ケースを最後に置きます)。 また、goto ステートメントも引き続き機能しますが、これは定数による case ラベルに限られます。パターン マッチングでは使用できません。

ローカル関数

デリゲートを宣言してデリゲートに式を割り当てることは既に実現可能ですが、C# 7.0 はこれをさらに 1 歩進めて、他のメンバー内でローカル関数をインラインで完全に宣言できるようにしています。図 7 の IsPalindrome 関数について考えてみましょう。

図 7 ローカル関数の例

bool IsPalindrome(string text)
{
  if (string.IsNullOrWhiteSpace(text)) return false;
  bool LocalIsPalindrome(string target)
  {
    target = target.Trim();  // Start by removing any surrounding whitespace.
    if (target.Length <= 1) return true;
    else
    {
      return char.ToLower(target[0]) ==
        char.ToLower(target[target.Length - 1]) &&
        LocalIsPalindrome(
          target.Substring(1, target.Length - 2));
    }
  }
  return LocalIsPalindrome(text);
}

この実装では、IsPalindrome に渡される引数が null またはホワイト スペースのみではないことを最初にチェックしています (null チェックに "text is null" のパターン マッチングを使用することも可能です)。 次に、関数 LocalIsPalindrome を宣言し、その中で最初と最後の文字を再帰的に比較します。このアプローチのメリットはクラスのスコープ内で LocalIsPalindrome を宣言していないことです。クラスのスコープ内で宣言すると、LocalIsPalindrome が誤って呼び出され、その結果、IsNullOrWhiteSpace チェックは回避される可能性があります。言い換えれば、ローカル関数によって追加のスコープ制限が提供されます。ただし、この制限はその関数の内側に限られます。

図 7 のパラメーター検証シナリオは、ローカル関数の一般的なユースケースの 1 つです。もう 1 つのユースケースは、IsPalindrome 関数をテストする場合など、単体テスト内でよく見受けられます (図 8 参照)。

図 8 単体テストでよく使用されるローカル関数

[TestMethod]
public void IsPalindrome_GivenPalindrome_ReturnsTrue()
{
  void AssertIsPalindrome(string text)
  {
    Assert.IsTrue(IsPalindrome(text),
      $"'{text}' was not a Palindrome.");
  }
  AssertIsPalindrome("7");
  AssertIsPalindrome("4X4");
  AssertIsPalindrome("   tnt");
  AssertIsPalindrome("Was it a car or a cat I saw");
  AssertIsPalindrome("Never odd or even");
}

IEnumerable<T> 要素を返す反復子関数や戻り値の要素を生成する反復子関数も、ローカル関数の一般的なユースケースの 1 つです。

このトピックのまとめとして、ローカル関数について注意が必要になる点をいくつか紹介します。

  • ローカル関数では、アクセシビリティ修飾子 (public、private、protected) を使用できません。
  • ローカル関数はオーバーロードをサポートしません。シグネチャが同じでなくても、同じメソッド内に同じ名前の 2 つのローカル関数を記述することはできません。
  • コンパイラは、一度も呼び出されることのないローカル関数に対して警告を発行します。
  • ローカル関数は、そのローカル関数を囲むスコープ内に存在する変数すべてにアクセスでき、これにはローカル変数も含まれます。この動作はローカルで定義されるラムダ式と同じです。ただし、ローカルで定義されるラムダ式とは異なり、ローカル関数はクロージャを表すオブジェクトを割り当てることはありません。
  • ローカル関数は、呼び出されるのがそのローカル関数の宣言前か宣言後かを問わず、メソッド全体のスコープ内に含まれます。

参照による戻り値

C# 1.0 以降、C# では引数を参照 (ref) によって関数に渡せるようになっています。その結果、パラメーター自体に対する変更が呼び出し元に返されます。次の swap 関数を考えてみます。

static void Swap(ref string x, ref string y)

このシナリオでは、呼び出されたメソッドは呼び出し元の本来の変数を新しい値で更新できます。したがって、最初の引数と 2 番目の引数に格納された値が交換されます。

C# 7.0 からは、ref パラメーターだけではなく、関数 return を使って参照を返すこともできるようになります。たとえば、赤目に関する画像の最初のピクセルを返す関数について考えてみます (図 9 参照)。

図 9 ref 戻り値と ref ローカル宣言

public ref byte FindFirstRedEyePixel(byte[] image)
{
  //// Do fancy image detection perhaps with machine learning.
  for (int counter = 0; counter < image.Length; counter++)
  {
    if(image[counter] == (byte)ConsoleColor.Red)
    {
      return ref image[counter];
    }
  }
  throw new InvalidOperationException("No pixels are red.");
}
[TestMethod]
public void FindFirstRedEyePixel_GivenRedPixels_ReturnFirst()
{
  byte[] image;
  // Load image.
  // ...
    // Obtain a reference to the first red pixel.
  ref byte redPixel = ref FindFirstRedEyePixel(image);
  // Update it to be Black.
  redPixel = (byte)ConsoleColor.Black;
  Assert.AreEqual<byte>((byte)ConsoleColor.Black, image[redItems[0]]);
}

画像への参照を返すことで、その後、呼び出し元はピクセルを別の色に更新できるようになります。配列を使用して更新をチェックすると、値が black になっていることが示されます。参照によるパラメーターを使用する代替手段は、異論があるかもしれませんが、それほど明白ではなく、読みやすくもありません。

public bool FindFirstRedEyePixel(ref byte pixel);

参照による戻り値には 2 つの重要な制約があります。両方とも、オブジェクトの有効期限に起因します。その制約とは、参照されている間はオブジェクト参照がガベージ コレクションの対象になってはならないこと、およびオブジェクト参照は参照先がなくなった場合にメモリを消費してはならないことの 2 つです。第 1 に、返すことができるのは、フィールドへの参照、参照を返す他のプロパティや関数への参照、または参照によって返す関数にパラメーターとして渡されるオブジェクトへの参照に限られます。たとえば、FindFirst­RedEyePixel は画像配列の項目への参照を返します。この参照はこの関数のパラメーターでした。同様に、画像がクラス内でフィールドとして格納された場合は、次の参照によってそのフィールドを返すことができます。

byte[] _Image;
public ref byte[] Image { get {  return ref _Image; } }

第 2 に、ref ローカル宣言は、メモリの特定の保存場所に初期化され、別の場所を示すように変更することはできません (参照へのポインターを用意して、その参照を変更することはできません。これは、C++ 開発者の言葉で言えばポインターのポインターです)。

参照による戻り値については、いくつかの特性を認識しておく必要があります。 

  • 参照を返す場合には明示的に返す必要があります。つまり、図 9 の例では、赤目のピクセルが存在しない場合でも、ref バイトを返す必要があります。回避策は例外をスローすることくらいしかありません。一方、参照によるパラメーター アプローチでは、パラメーターには手を加えずに、成功を示すブール値を返すことができます。多くの場合、こちらの方法をお勧めします。
  • 参照ローカル変数を宣言する場合は初期化が必要です。これには、関数からの ref 戻り値か、変数への参照をローカル変数に代入する必要があります。
ref string text;  // Error
  • C# 7.0 では参照ローカル変数を宣言できますが、ref 型のフィールドを宣言することはできません。
class Thing { ref string _Text;  /* Error */ }
  • 自動実装プロパティ用に参照による型を宣言することはできません。
class Thing { ref string Text { get;set; }  /* Error */ }
  • 参照を返すプロパティは使用できます。
class Thing { string _Text = "Inigo Montoya"; 
  ref string Text { get { return ref _Text; } } }
  • 参照ローカル変数は値 (null や定数など) で初期化することはできません。参照ローカル変数は、参照を返すメンバーまたはローカル変数/フィールドから割り当てる必要があります。 
ref int number = null; ref int number = 42;  // ERROR

out 変数

C# の最初のリリース以降、out パラメーターを含むメソッドの呼び出しでは、そのメソッド呼び出す前に、必ず out 引数の識別子を事前に宣言する必要がありました。しかし、C# 7.0 ではこの特異性がなくなり、メソッドの呼び出しと共に out 引数をインラインで宣言できるようになります。図 10 に例を示します。

図 10 out 引数のインライン宣言

public long DivideWithRemainder(
  long numerator, long denominator, out long remainder)
{
  remainder = numerator % denominator;
  return (numerator / denominator);
}
[TestMethod]
public void DivideTest()
{
  Assert.AreEqual<long>(21,
    DivideWithRemainder(42, 2, out long remainder));
  Assert.AreEqual<long>(0, remainder);
}

DivideTest メソッドにおいて、テスト内部からの DivideWithRemainder の呼び出しでは、out 修飾子の後に型指定子が含まれているのがわかります。そのうえ、引き続き remainder は自動的にメソッドのスコープの対象になります。これは、2 番目の Assert.AreEqual 呼び出しからもわかります。すばらしいですね。

リテラルの改善

以前のバージョンとは違い、C# 7.0 には数値バイナリ リテラル形式が含まれています。これを、次の例で示します。

long LargestSquareNumberUsingAllDigits =
  0b0010_0100_1000_1111_0110_1101_1100_0010_0100;  // 9,814,072,356
long MaxInt64 { get; } =
  9_223_372_036_854_775_807;  // Equivalent to long.MaxValue

アンダースコア "_" を桁区切り記号としてサポートしていることにも注目してください。この記号は単に読みやすくする目的で使用され、2 進数、10 進数、または 16 進数という数値の桁の間ならどこでも使用できます。

一般化される async 戻り値の型

場合によっては、async メソッドを実装すると、同期を取って結果を返せることで実行時間の長い処理が省略されます。結果がほぼ即座に返されたり、既にわかっている場合があるためです。たとえば、ディレクトリ内のファイルの合計サイズを判断する async メソッドについて考えます (bit.ly/2dExeDG)。実際、ディレクトリにファイルがなければ、メソッドは実行時間の長い処理を行わずに即座に結果を返すことができます。C# 7.0 以前の async 構文の要件では、このようなメソッドからの戻り値は Task<long> である必要があるため、Task インスタンスを必要としない場合でも、Task のインスタンスを作成しなければなりませんでした (これを行うための一般的なパターンは Task.FromResult<T> から結果を返すことです)。

C# 7.0 では、コンパイラは async メソッドの戻り値を void、Task、または Task<T> に限定しなくなります。.NET Core Framework によって提供される System.Threading.Tasks.ValueTask<T> 構造体など、async メソッドと互換性を持つカスタム型を定義できるようになっています。詳細については、itl.tc/GeneralizedAsyncReturnTypes (英語) を参照してください。

他の式形式のメンバー

C# 6.0 では、式形式のメンバーが関数とプロパティに導入され、簡単なメソッドやプロパティを実装するための構文が合理化されています。C# 7.0 では式形式の実装が、コンストラクター、アクセサー (get プロパティと set プロパティの実装)、およびファイナライザーにまで追加されます (図 11 参照)。

図 11 アセクサーおよびコンストラクターでの式形式のメンバーの使用

class TemporaryFile  // Full IDisposible implementation
                     // left off for elucidation.
{
  public TemporaryFile(string fileName) =>
    File = new FileInfo(fileName);
  ~TemporaryFile() => Dispose();
  Fileinfo _File;
  public FileInfo File
  {
    get => _File;
    private set => _File = value;
  }
  void Dispose() => File?.Delete();
}

式形式のメンバーを使用することは特にファイナライザーで一般的になると考えています。それは、図に示すように、最もよく見られる実装の形が、Dispose メソッドを呼び出すことだからです。

式形式メンバーのさらなるサポートが、マイクロソフト C# チームではなく、C# コミュニティによって実装されたことにぜひとも注目してください。オープン ソースのおかげと言えるでしょう。

注意: この機能は Visual Studio 2017 RC には実装されていません。

throw 式

図 11 の Temporary クラスは、式形式のメンバー内にパラメーター検証を含むように拡張できます。したがって、コンストラクターを次のように更新できます。

public TemporaryFile(string fileName) =>
  File = new FileInfo(filename ?? throw new ArgumentNullException());

throw 式を使用しなければ、C# で式形式のメンバーをサポートしても、パラメーターを検証することはできません。ただし、C# 7.0 では throw をステートメントだけではなく、式としてサポートしており、より多くを内包する式の内側でエラーをインラインで報告できるようになっています。

注意: この機能は Visual Studio 2017 RC には実装されていません。

まとめ

正直に言うと、このコラムを書き始めたときにはもっと短い内容になると考えていました。しかし、機能のプログラミングとテストに多くの時間を費やしていくうちに、C# 7.0 には、機能の名前を確認したり、C# の開発に従って気が付くことよりも、ずっと多くの要素があることを発見しました。ほとんどの場合、out 変数、バイナリ リテラル、throw 式の宣言など、C# 7.0 の機能を理解して使用するうえで欠かせないことはそれほど多くありません。しかし、参照による戻り値、デコンストラクター、タプルのように、最初の予測よりもはるかに多くのことを覚えなければならない場合もあります。後者の場合では、構文だけではなく、機能を使用する適切な状況も把握する必要があります。

C# 7.0 では特異性 (事前に宣言される out 識別子や throw 式の欠如) が急速に減っており、そうした特異性の一覧は今後も少しずつ削り落とされていくことになります。しかし、同時に拡張によって、言語レベルでは以前利用できなかった機能 (タプルやパターン マッチング) のサポートも追加されていきます。

今回のコラムを読んだ皆さんがこれを参考に C# 7.0 プログラミングに着手するようになることを願っています。このコラムに従った C# 7.0 開発の詳細については、intellitect.com/csharp7 (英語) と、筆者の著書『Essential C# 7.0』 (Visual Studio 2017 の製品版のリリース直後に出版される予定です) を参照してください。


Mark Michaelis は、IntelliTect の創設者で、同社でチーフ テクニカル アーキテクト兼トレーナーを務めています。彼は約 20 年間 Microsoft MVP に認定され、2007 年から Microsoft Regional Director を務めています。Michaelis は、C#、Microsoft Azure、SharePoint、Visual Studio ALM など、マイクロソフト ソフトウェアの設計レビュー チームにも所属しています。開発者を対象としたカンファレンスで講演を行い、多数の書籍を執筆しています。最近では、『Essential C# 6.0 (5th Edition)』(Addison-Wesley Professional、2015 年) を執筆しました (itl.tc/EssentialCSharp、英語)。連絡先は、Facebook (facebook.com/Mark.Michaelis、英語)、ブログ (IntelliTect.com/Mark、英語)、Twitter (@markmichaelis、英語)、または電子メール mark@IntelliTect.com (英語のみ) です。

この記事のレビューに協力してくれた技術スタッフの Kevin Bost (IntelliTect)、Mads Torgersen (マイクロソフト) および Bill Wagner (マイクロソフト) に心より感謝いたします。


この記事について MSDN マガジン フォーラムで議論する