C#

C# 6.0 によるコードの単純化、明確化、および簡略化

Mark Michaelis

C# 6.0 は、C# のプログラミングを根本から変えるわけではなく、ジェネリック (C# 2.0)、LlNQ による画期的方法でのコレクションのプログラミング (C# 3.0)、非同期プログラミング パターンの単純化 (C# 5.0) などの導入時のように、開発作業が一変することはありません。とは言え、C# 6.0 によって、特定のシナリオにおける C# コードのコーディングが変わります。非常に効率の高い特徴を備えているため、おそらく別のコーディング方法があったことを忘れてしまうでしょう。C# 6.0 では、構文の新しい簡略化手段が導入されるため、形式的なコードの量が減少し、最終的に無駄のない C# コードを記述できるようになります。今回は、これらすべてを可能にする C# 6.0 の新しい機能セットについて詳しく説明します。特に重点的に説明するのは、図 1 のマインドマップに示した項目についてです。

C# 6.0 のマインドマップ
図 1 C# 6.0 のマインドマップ

今回紹介する例の多くは、私が執筆中の新版書籍『Essential C# 6.0』 (Addison-Wesley Professional) から引用しています。

using static

C# 6.0 で提供される機能の多くは、最も基本的なコンソール プログラムで利用できます。たとえば、using static ディレクティブでは、特定のクラスに using を使用できるようになります。using static ディレクティブにより、静的メソッドを型のプレフィックスを指定しないで、グローバル スコープ内で使用できるようになります (図 2 参照)。静的クラスの System.Console を使用して、この機能を説明します。

図 2 using static ディレクティブによる不要なコードの削減

using System;
using static System.ConsoleColor;
using static System.IO.Directory;
using static System.Threading.Interlocked;
using static System.Threading.Tasks.Parallel;
public static class Program
{
  // ...
  public static void Main(string[] args)
  {
    // Parameter checking eliminated for elucidation.
    EncryptFiles(args[0], args[1]);
  }
  public static int EncryptFiles(
    string directoryPath, string searchPattern = "*")
  {
    ConsoleColor color = ForegroundColor;
    int fileCount = 0;
    try
    {
      ForegroundColor = Yellow
      WriteLine("Start encryption...");
      string[] files = GetFiles(directoryPath, searchPattern,
        System.IO.SearchOption.AllDirectories);
      ForegroundColor = White
      ForEach(files, (filename) =>
      {
        Encrypt(filename);
        WriteLine("\t'{0}' encrypted", filename);
        Increment(ref fileCount);
      });
      ForegroundColor = Yellow
      WriteLine("Encryption completed");
    }
    finally
    {
      ForegroundColor = color;
    }
    return fileCount;
  }
}

この例では、System.ConsoleColor、System.IO.Directory、System.Threading.Interlocked、および System.Threading.Tasks.Parallel に using static ディレクティブを使用しています。これにより、ForegroundColor、WriteLine、GetFiles、Increment、Yellow、White、ForEach など、多くのメソッド、プロパティ、および列挙子を直接呼び出すことができるようになります。どれを呼び出す場合でも、静的メンバーを型で修飾する必要がありません (Visual Studio 2015 Preview より前のバージョンを使用している場合は、構文の using の後に "static" キーワードを追加しないで、"using System.Console" のように指定します。また、Visual Studio 2015 Preview からは、using static ディレクティブが静的クラスだけでなく列挙子と構造体に対しても有効になります)。

型の修飾子を省略すると、ほとんどの場合にコード量が減少しますが、コードの明確さはそれほど低下しません。コンソール プログラムで WriteLine とコーディングした場合、GetFiles への呼び出しと同様、その意図は明らかです。また、System.Threading.Tasks.Parallel への using static ディレクティブの追加もその意図が明らかなため、ForEach が foreach ループの並列処理を定義する 1 つの方法であることがわかります。ForEach の構文は、C# のバージョンが新しくなるにつれ、(細かく見れば) 徐々に C# の foreach ステートメントに似てきています。

using static ディレクティブについて注意すべきことは、言うまでもなく、コードの明確さを維持することです。たとえば、図 3 で定義している Encrypt 関数について考えてみましょう。

図 3 あいまいな Exists の呼び出し (nameof 演算子を使用)

private static void Encrypt(string filename)
  {
    if (!Exists(filename)) // LOGIC ERROR: Using Directory rather than File
    {
      throw new ArgumentException("The file does not exist.", 
        nameof(filename));
    }
    // ...
  }

Exists への呼び出しは問題がないように見えますが、正確に言えば、この呼び出しは Directory.Exists になり、実際に呼び出す必要があるのは File.Exists です。つまり、コードは確かに読みやすくなりますが、誤りが生まれるため、少なくともこの場合は using static 構文を使用しない方がよいでしょう。

using static ディレクティブを System.IO.Directory と System.IO.File の両方に対して指定した場合、Exists の呼び出しでコンパイラ エラーが発生します。このあいまいさを解決するには、型のあいまいさをなくすためにプレフィックスでコードを修飾せざるを得ません。

using static ディレクティブのもう 1 つの機能は、このディレクティブを拡張メソッドに指定した場合の動作です。拡張メソッドは、静的メソッドの通常動作とは異なり、グローバル スコープに移行されません。たとえば、using static ParallelEnumerable ディレクティブは、Select メソッドをグローバル スコープにしないため、Select(files, (filename) => { ... }) のように呼び出すことはできません。この制限は仕様です。まず、拡張メソッドはオブジェクトのインスタンス メソッドとして使用する (たとえば、files.Select((filename)=> { ... })) ように設計されているため、通常は、型を指定せずに静的メソッドとして呼び出すことはありません。次に、System.Linq などのライブラリには、Select などの重複するメソッド名を持つ、Enumerable や ParallelEnumerable などの型があります。これらの型をすべてグローバル スコープに追加すると、IntelliSense に不要なデータが混入し、あいまいな呼び出しが発生する可能性があります (System.Linq ベースのクラスでは、このような現象は発生しません)。

拡張メソッドがグローバル スコープになることはありませんが、C# 6.0 では、拡張メソッドを含むクラスに using static ディレクティブを指定することはかまいません。using static ディレクティブと using (名前空間) ディレクティブの違いは、using static ディレクティブの対象が特定のクラスに限定されることです。つまり、using static を指定すると、使用できる拡張メソッドの範囲を、名前空間全体ではなく、特定のクラスに絞り込むことができます。たとえば、図 4 のスニペットについて考えてみましょう。

図 4 拡張メソッドのスコープを ParallelEnumerable に限定するコード

using static System.Linq.ParallelEnumerable;
using static System.IO.Console;
using static System.Threading.Interlocked;
// ...
    string[] files = GetFiles(directoryPath, searchPattern,
      System.IO.SearchOption.AllDirectories);
    files.AsParallel().ForAll( (filename) =>
    {
      Encrypt(filename);
      WriteLine($"\t'{filename}' encrypted");
      Increment(ref fileCount);
    });
// ...

using System.Linq ステートメントではなく、using static System.Linq.ParallelEnumerable ディレクティブが使用されているのがわかります。これにより、ParallelEnumerable の拡張メソッドのみが、拡張メソッドとしてスコープに含まれます。System.Linq.Enumerable などのクラスに含まれる拡張メソッドは、拡張メソッドとして使用できません。たとえば、files.Select(...) は、Select が文字列配列の (さらに IEnumerable<string> でも) スコープに含まれていないためコンパイル エラーになります。一方、AsParallel は System.Linq.ParallelEnumerable によってスコープに含まれるようになります。つまり、拡張メソッドを含むクラスに using static ディレクティブを指定すると、そのクラスの拡張メソッドが、拡張メソッドとしてスコープに追加されます (同じクラスに含まれる、拡張メソッド以外のメソッドは、通常グローバル スコープになります)。

一般に、System.Console や System.Math など、(Parallel とは異なり) スコープ全体にわたって繰り返し使用される一部のクラスに対しては、using static ディレクティブの使用を制限することをお勧めします。同様に、列挙子に using static を指定する場合は、型の識別子が記述されていない状態でも、列挙子の項目の内容が自明である必要があります。たとえば、これは私の好みかもしれませんが、単体テスト ファイル内に using Microsoft.VisualStudio.TestTools.UnitTesting.Assert を指定して、IsTrue、AreEqual<T>、Fail、IsNotNull などのテスト アサーションを呼び出すことができるようにします。

nameof 演算子

図 3 のコードには、C# 6.0 で導入されるもう 1 つの新機能、nameof 演算子が含まれています。この新しいコンテキスト キーワードの目的は文字リテラルを特定することです。このキーワードは、引数として指定されている識別子の非修飾名の定数を (コンパイル時に) 抽出します。図 3 の nameof(ファイル名) によって返される "ファイル名" は Encrypt メソッドのパラメータ名ですが、nameof は任意のプログラム識別子に対応します。たとえば、図 5 では nameof を使用して INotifyPropertyChanged.PropertyChanged にプロパティ名を渡しています (ちなみに、itl.tc/?p=11661 (英語) で説明されているように、PropertyChanged の呼び出しに必要なプロパティ名を取得する方法として、CallerMemberName 属性を使用することも引き続き可能です)。

図 5 INotifyPropertyChanged.PropertyChanged での nameof 演算子の使用

public class Person : INotifyPropertyChanged
{
  public event PropertyChangedEventHandler PropertyChanged;
  public Person(string name)
  {
    Name = name;
  }
  private string _Name;
  public string Name
  {
    get{ return _Name; }
    set
    {
      if (_Name != value)
      {
        _Name = value;
        PropertyChangedEventHandler propertyChanged = PropertyChanged;
        if (propertyChanged != null)
        {
          propertyChanged(this,
            new PropertyChangedEventArgs(nameof(Name)));
        }
      }
    }
  }
  // ...
}
[TestClass]
public class PersonTests
{
  [TestMethod]
  public void PropertyName()
  {
    bool called = false;
    Person person = new Person("Inigo Montoya");
    person.PropertyChanged += (sender, eventArgs) =>
    {
      AreEqual(nameof(CSharp6.Person.Name), eventArgs.PropertyName);
      called = true;
    };
    person.Name = "Princess Buttercup";
    IsTrue(called);
  }   
}

修飾されていない "Name" のみを指定した場合と、完全修飾されている識別子 CSharp6.Person.Name をテスト内で使用した場合の結果は、どちらも最後の識別子のみ (ドットで区切られた名前の最後の要素) になります。

nameof 演算子を使用すると、"魔法の" 文字列の大半を取り除くことができます。ここで言う "魔法の" 文字列とは、その文字列がスコープ内に含まれる場合のみコード識別子と見なされる文字列のことです。コンパイラはこのような魔法の文字列を識別子として検証しません。そのため、魔法の文字列のスペルミスが原因でランタイム エラーが発生する可能性があります。nameof 演算子はこのような可能性を排除するだけでなく、"Rename" (名前の変更) などのリファクタリング ツールによって、nameof 演算子の引数となっている識別子すべての名前が変更されるようになります。リファクタリング ツールを使用しないで名前を変更した場合、コンパイラはその識別子がもはや存在しないことを示すエラーを発行します。

文字列補間

図 3 のコードは、ファイルが見つからなかったことを示す例外メッセージを指定するだけでなく、ファイル名自体も表示すると、さらに改善されると考えられます。以前は、string.Format を使用してファイル名を文字列リテラルに埋め込んでいました。しかし、書式を組み合わせるコードは、あまり読みやすくありませんでした。たとえば、人物名の書式設定では、図 6 に示すメッセージの代入のように、パラメーターの順序に従ってプレースホルダーを置き換えていく必要がありました。

図 6 文字列を組み合わせる書式設定と文字列補間

[TestMethod]
public void InterpolateString()
{
  Person person = new Person("Inigo", "Montoya") { Age = 42 };
  string message =
    string.Format("Hello!  My name is {0} {1} and I am {2} years old.",
    person.FirstName, person.LastName, person.Age);
  AreEqual<string>
    ("Hello!  My name is Inigo Montoya and I am 42 years old.", message);
  string messageInterpolated =
    $"Hello!  My name is {person.FirstName} {person.LastName} and I am
    {person.Age} years old.";
  AreEqual<string>(message, messageInterpolated);
}

文字列を組み合わせる書式設定に代わる手法として、messageInterpolated への代入を使用している部分に注目してください。この例の messageInterpolated に代入する式は、"$" というプレフィックスが付加された文字列リテラルです。中括弧によって、文字列内にインラインで埋め込まれるコードが識別されます。この場合、person のプロパティが使用されているため、読みやすさが大きく向上します。さらに、文字列補間構文によって、書式文字列の順序に正しく従っていない引数や、引数を指定し忘れた場合に例外を発生する原因になるエラーが減少します (Visual Studio 2015 Preview では $ 文字が使用されないため、始めの中かっこの前にそれぞれスラッシュを記述する必要があります。Visual Studio 2015 Preview に続くリリースでは、文字列リテラル構文の前に $ を使用するように仕様が更新されています)。

文字列補間は、コンパイル時に等価な string.Format 呼び出しに変換されます。そのため、以前と同様、ローカライズのインプレース サポート (現時点では従来の書式文字列に対して) が提供されます。また、文字列を介したコードの挿入がコンパイル後に実行されることはありません。

図 7 は、文字列補間に関する例をさらに 2 つ示しています。

図 7 string.Format の代わりに使用される文字列補間

public Person(string firstName, string lastName, int? age=null)
{
  Name = $"{firstName} {lastName}";
  Age = age;
}
private static void Encrypt(string filename)
{
  if (!System.IO.File.Exists(filename))
  {
    throw new ArgumentException(
      $"The file, '{filename}', does not exist.", nameof(filename));
  }
  // ...
}

2 つ目の例の throw ステートメントでは、文字列補間と nameof 演算子の両方が使用されています。文字列補間によって、ArgumentException のメッセージにファイル名が含められます (つまり、"The file, 'c:\data\missingfile.txt' does not exist")。nameof 演算子を使用する目的は、ArgumentException コンストラクターの 2 番目の引数である、Encrypt のパラメーター名 ("filename") を特定することです。Visual Studio 2015 では、文字列補間構文が完全に認識されるため、補間する文字列内に埋め込まれたコード ブロックに対して、コードの色付けと IntelliSense の両方がサポートされます。

Null 条件演算子

図 2 では、説明をわかりやすくする目的で省略しましたが、引数を受け取るほぼすべての Main メソッドでは、Length メンバーを呼び出して渡されたパラメーターの数を特定する前に、パラメーターが Null かどうかをチェックする必要があります。より一般的に言えば、メンバーを呼び出す前に Null かどうかをチェックするパターンは、System.NullReferenceException (ほとんどの場合、プログラミング ロジックにおけるエラーを示す) の発生を回避するためによく使用されます。このパターンの使用頻度が多いことから、C# 6.0 では、Null 条件演算子として "?." 演算子が導入されます。次に例を示します。

public static void Main(string[] args)
{
  switch (args?.Length)
  {
  // ...
  }
}

Null 条件演算子によって、メソッドまたはプロパティ (この場合は Length) を呼び出す前に、オペランドが Null かどうかがチェックされます。論理的にこれと等しくなるコードを明確に示すと、次のようになります (ただし、C# 6.0 の構文では、args 値が評価されるのは 1 回のみです)。

(args != null) ? (int?)args.Length : null

Null 条件演算子が特に便利な点は、連鎖させることが可能なことです。たとえば、string[] names = person?.Name?.Split(' ') を呼び出うと、Split は、person と person.Name の両方が Null でない場合のみ呼び出されます。Null 条件演算子を連鎖させると、1 つ目のオペランドが Null の場合、式の評価が終了し、式の呼び出しチェーンに含まれるそれ以降の要素は評価されません。ただし、Null 条件演算子を追加しない場合は、その意味を理解しておく必要があります。たとえば、names = person?.Name.Split(' ') について考えてみます。person インスタンスは存在しても、Name が Null の場合、Split の呼び出し時に NullReferenceException が発生します。これは、Null 条件演算子を必ず連鎖させる必要があるということではなく、こうしたロジックになることを意識する必要があります。たとえば、Person の場合、Name は検証されるため Null にはならないので Null 条件演算子を追加する必要はないのかもしれません。

Null 条件演算子に関する重要事項の 1 つは、値型を返すメンバーに Null 条件演算子を使用した場合、常に Null 許容型が返されることです。たとえば、args?.Length は単なる int ではなく int? を返します。少し (他の演算子の動作に比べて) 変わった動作かもしれませんが、値の Null 許容型は、呼び出しチェーンの最後にのみ返されます。このため、Length のドット (".") 演算子を呼び出した場合、(int? ではなく) int メンバーの呼び出しのみが許可されます。ただし、args?.Length をかっこで囲むと、演算子の優先順位が変わり、int? の結果が強制されます。このため、int? の戻り値が呼び出され、Nullable<T> に固有のメンバー (HasValue と Value) が使用可能になります。

Null 条件演算子はそれ自体も優れた機能ですが、デリゲート呼び出しと組み合わせて使用すると、C# 1.0 から続く C# の弱点を解消できます。図 5 で注目する必要があるのは、PropertyChanged イベント ハンドラーをローカル コピー (propertyChanged) に代入してから、値が Null かどうかをチェックし、最後にイベントを起動している部分です。これは、スレッド セーフを実現しながら、Null かどうかをチェックする時点とイベントを起動する時点との間にイベント サブスクリプションの解除が発生するリスクを回避する最も簡単な方法です。残念ながら、これは直観的な方法ではなく、このパターンに沿っていないコードも数多く存在します。そのような場合、NullReferenceException には一貫性がありません。さいわい、C# 6.0 で Null 条件演算子が導入されたことによって、この問題が解決します。

たとえば、次のようなコード スニペットがあるとします。

PropertyChangedEventHandler propertyChanged = PropertyChanged;
if (propertyChanged != null)
{
  propertyChanged(this, new PropertyChangedEventArgs(nameof(Name)));
}

C# 6.0 では、これを次のように単純化できます。

PropertyChanged?.Invoke(propertyChanged(
  this, new PropertyChangedEventArgs(nameof(Name)));

また、イベントはデリゲートにすぎないため、Null 条件演算子と Invoke を使用してデリゲートを呼び出すこのパターンを常に使用できます。今後の C# コードの記述方法を変えるという意味では、この機能が、おそらく C# 6.0 の他のどの機能よりも大きな影響を与えることは確かです。一度デリゲートに対して Null 条件演算子を使用すれば、もう以前の方法に戻ってコードを記述することはなくなるでしょう (もちろん、C# 6.0 以前のバージョンから逃れられない場合は除きます)。

Null 条件演算子をインデックス演算子と組み合わせて使用することもできます。たとえば、Null 条件演算子を Newtonsoft.JObject と組み合わせて使用すると、JSON オブジェクトを走査して特定の要素を取得できます (図 8 参照)。

図 8 コンソールの色の構成例

string jsonText =
    @"{
      'ForegroundColor':  {
        'Error':  'Red',
        'Warning':  'Red',
        'Normal':  'Yellow',
        'Verbose':  'White'
      }
    }";
  JObject consoleColorConfiguration = JObject.Parse(jsonText);
  string colorText = consoleColorConfiguration[
    "ForegroundColor"]?["Normal"]?.Value<string>();
  ConsoleColor color;
  if (Enum.TryParse<ConsoleColor>(colorText, out color))
  {
    Console.ForegroundColor = colorText;
  }

ここで重要なのは、MSCORLIB 内の大半のコレクションとは異なり、JObject はインデックスが無効でも例外をスローしないことです。たとえば、ForegroundColor が存在しない場合、JObject は例外をスローしないで Null を返します。この例外をスローしないことが重要になります。IndexOutOfRangeException をスローするコレクションで Null 条件演算子を使用する必要はほとんどありません。Null 条件演算子を使用すると、安全性があると思い込まれる可能性があります。Main と args の例を示すスニペットに戻り、このことを考えてみましょう。

public static void Main(string[] args)
{
  string directoryPath = args?[0];
  string searchPattern = args?[1];
  // ...
}

この例のコードが危険だと考えられる理由は、Null 条件演算子を使用することによって、args が Null ではないとなったときに、その要素が確実に存在するという誤った安心感が与えられるためです。当然、args が Null ではなくても、その要素が存在するとは限りません。args?.Length を使用して要素数をチェックすれば、args が Null ではないことが既に検証されているため、長さをチェックした後、コレクションのインデックスを作成するときに、再度 Null 条件演算子を使用する必要はまったくありません。つまり、インデックスが存在しないときに、インデックス演算子が IndexOutOfRangeException をスローする場合は、Null 条件演算子と組み合わせて使用しないようにします。使用すると、コードの正当性が誤って認識されることになります。

構造体の既定のコンストラクター

C# 6.0 の機能としてもう 1 つ知っておきたいのが、値型に対して既定 (パラメーターなし) のコンストラクターがサポートされることです。以前は、配列を初期化するとき、型構造体のフィールドに既定値を設定するとき、または default 演算子を使用してインスタンスを初期化するときに、コンストラクターが呼び出されることはなかったため、このような既定のコンストラクターは許可されていませんでした。C# 6.0 では、既定のコンストラクターが条件付きで有効になり、new 演算子を使用して値型のインスタンスを作成する場合のみ、このコンストラクターが呼び出されます。配列の初期化と、既定値の明示的な代入 (または型構造体フィールドの暗黙の初期化) はどちらも既定のコンストラクターの影響を受けません。

既定のコンストラクターの使用方法を理解するために、図 9 に示す ConsoleConfiguration クラスの例について考えてみましょう。コンストラクターが記述され、CreateUsingNewIsInitialized メソッドに示すように、そのコンストラクターが new 演算子を使用して呼び出されていることから、構造体は完全に初期化されます。図 9 からもわかるように、コンストラクターのチェーンも完全にサポートされます。つまり、コンストラクターの宣言に続いて "this" キーワードを指定することによって、1 つのコンストラクターからもう 1 つのコンストラクターを呼び出すことができます。

図 9 値型での既定のコンストラクターの宣言

using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
public struct ConsoleConfiguration
{
  public ConsoleConfiguration() :
    this(ConsoleColor.Red, ConsoleColor.Yellow, ConsoleColor.White)
  {
    Initialize(this);
  }
  public ConsoleConfiguration(ConsoleColor foregroundColorError,
    ConsoleColor foregroundColorInformation,
    ConsoleColor foregroundColorVerbose)
  {
    // All auto-properties and fields must be set before
    // accessing expression bodied members
    ForegroundColorError = foregroundColorError;
    ForegroundColorInformation = foregroundColorInformation;
    ForegroundColorVerbose = foregroundColorVerbose;
  }
   private static void Initialize(ConsoleConfiguration configuration)
  {
    // Load configuration from App.json.config file ...
  }
  public ConsoleColor ForegroundColorVerbose { get; }
  public ConsoleColor ForegroundColorInformation { get; }
  public ConsoleColor ForegroundColorError { get; }
  // ...
  // Equality implementation excluded for elucidation
}
[TestClass]
public class ConsoleConfigurationTests
{
  [TestMethod]
  public void DefaultObjectIsNotInitialized()
  {
    ConsoleConfiguration configuration = default(ConsoleConfiguration);
    AreEqual<ConsoleColor>(0, configuration.ForegroundColorError);
    ConsoleConfiguration[] configurations = new ConsoleConfiguration[42];
    foreach(ConsoleConfiguration item in configurations)
    {
      AreEqual<ConsoleColor>(default(ConsoleColor),
        configuration.ForegroundColorError);
      AreEqual<ConsoleColor>(default(ConsoleColor),
        configuration.ForegroundColorInformation);
      AreEqual<ConsoleColor>(default(ConsoleColor),
        configuration.ForegroundColorVerbose);
    }
  }
  [TestMethod]
  public void CreateUsingNewIsInitialized()
  {
    ConsoleConfiguration configuration = new ConsoleConfiguration();
    AreEqual<ConsoleColor>(ConsoleColor.Red,
      configuration.ForegroundColorError);
    AreEqual<ConsoleColor>(ConsoleColor.Yellow,
      configuration.ForegroundColorInformation);
    AreEqual<ConsoleColor>(ConsoleColor.White,
      configuration.ForegroundColorVerbose);
  }
}

構造体について覚えておくべき重要なポイントの 1 つは、他のインスタンス メンバーを呼び出す前に、インスタンスのすべてのフィールドと自動プロパティを完全に初期化しておく必要があることです (これらのフィールドとプロパティにはバッキング フィールドが含まれているため)。このような理由から、図 9 の例では、すべてのフィールドと自動プロパティに値が代入されるまで、コンストラクターは Initialize メソッドを呼び出すことができません。さいわい、チェーンされているコンストラクターが、必要なすべての初期化に対応しており、このコンストラクターが "this" キーワードを使用して呼び出された場合、コンパイラーは、this を使用して呼び出されていないコンストラクターの本体から再度データを初期化する必要はないことを適切に検出します (図 9 参照)。

自動プロパティの機能強化

図 9 では、(明示的なフィールドが存在しない) 3 つのプロパティすべてが、getter のみを備えた (本体のない) 自動プロパティとして宣言されているのがわかります。このような getter のみを備えた自動プロパティは、読み取り専用のフィールドを (内部的に) バッキング フィールドにする読み取り専用プロパティの宣言を目的とする、C# 6.0 の機能の 1 つです。このため、このようなプロパティはコンストラクター内でのみ変更できます。

getter のみを備えた自動プロパティは、構造体の宣言でもクラスの宣言でも使用できますが、特に、不変にすることがガイドラインで推奨されている構造体にとっては重要です。C# 6.0 よりも前のバージョンでは、読み取り専用プロパティの宣言とその初期化を記述するために、6 行ほどのコードを必要としましたが、C# 6.0 では、1 行でプロパティを宣言し、コンストラクター内から値を代入するだけです。したがって、不変の構造体を宣言することは、構造体の正しいプログラミング パターンであるだけでなく、より単純なパターンにもなります。以前の構文では、コードを正しく記述するために、より多くの手間がかかっていたことを考えれば、これは歓迎すべき変更点です。

C# 6.0 で導入される、2 つ目の自動プロパティの機能は、初期化子のサポートです。たとえば、次のように、初期化子を含む静的な DefaultConfig 自動プロパティを ConsoleConfiguration に追加できます。

// Instance property initialization not allowed on structs.
static private Lazy<ConsoleConfiguration> DefaultConfig{ get; } =
  new Lazy<ConsoleConfiguration>(() => new ConsoleConfiguration());

このようなプロパティによって、既定の ConsoleConfigurtion インスタンスへのアクセスに使用できる、単一のインスタンス ファクトリ パターンが提供されます。この例では、getter のみを備えた自動プロパティにコンストラクター内で値を代入するのではなく、System.Lazy<T> を活用し、宣言時にこのクラスを初期化子としてインスタンスを作成します。その結果、一度コンストラクターの処理が完了すれば、Lazy<ConsoleConfiguration> は不変になり、DefaultConfig を呼び出したときに、常に同じ ConsoleConfiguration のインスタンスが返されます。

自動プロパティの初期化子は、構造体のインスタンス メンバーには許可されないことに注意してください (クラスに対しては確実に許可されます)。

式が本体になるメソッドと自動プロパティ

C# 6.0 で導入されるもう 1 つの機能は、式が本体になるメンバーです。この機能はプロパティとメソッドの両方に対して提供されるため、矢印演算子 (=>) を使用して、ステートメント本体ではなく式をプロパティまたはメソッドに代入できるようになります。たとえば、前の例の DefaultConfig プロパティはプライベートであり、かつ Lazy<T> 型であるため、ConsoleConfiguration の実際の既定のインスタンスを取得するには、次のように GetDefault メソッドを使用する必要があります。

static public ConsoleConfiguration GetDefault() => DefaultConfig.Value;

ただし、このスニペットには、ステートメント ブロックのようなメソッド本体は存在しません。メソッドの実装に使用されているのは、ラムダ式の矢印演算子がプレフィックスとして付けられた (ステートメントではなく) 式のみです。この目的は、メソッド シグネチャ内のパラメーターの有無にかかわらず機能する、形式的なコードがすべて取り除かれた単純な 1 行の実装を提供することです。

private static void LogExceptions(ReadOnlyCollection<Exception> innerExceptions) =>
  LogExceptionsAsync(innerExceptions).Wait();

プロパティに関して言えば、式本体は読み取り専用 (getter のみを備える) プロパティに対してのみ機能します。実際には、この構文は式が本体になるメソッドの構文とほぼ同じであり、唯一異なるのは識別子の後にかっこが付けられていない点です。先ほどの Person の例に戻ると、読み取り専用の FirstName プロパティと LastName プロパティは、次のように式本体を使用して実装できます (図 10 参照)。

図 10 式が本体になる自動プロパティ

public class Person
{
  public Person(string name)
  {
    Name = name;
  }
  public Person(string firstName, string lastName)
  {
    Name = $"{firstName} {lastName}";
    Age = age;
  }
  // Validation ommitted for elucidation
  public string Name {get; set; }
  public string FirstName => Name.Split(' ')[0];
  public string LastName => Name.Split(' ')[1];
  public override string ToString() => "\{Name}(\{Age}";
}

さらに、式が本体になるプロパティは、インデックス メンバーに対しても使用できるため、内部コレクションから項目を返すなどの処理が可能です。

ディクショナリ初期化子

ディクショナリの型コレクションは、名前と値のペアを定義するときに非常に役立ちます。残念ながら、初期化の構文は最適な形式とは言えません。次に例を示します。

{ {"First", "Value1"}, {"Second", "Value2"}, {"Third", "Value3"} }

この作業を簡略化するために、C# 6.0 では、ディクショナリの代入に関する次のような新しい型構文が導入されます。

Dictionary<string, Action<ConsoleColor>> colorMap =
  new Dictionary<string, Action<ConsoleColor>>
{
  ["Error"] =               ConsoleColor.Red,
  ["Information"] =        ConsoleColor.Yellow,
  ["Verbose"] =            ConsoleColor.White
};

構文を強化するために言語チームが導入したのは、代入演算子を使用して、参照 (名前) と値のペアまたはマップをなす項目のペアを関連付ける方法です。この参照は、宣言するディクショナリに含まれる任意のインデックス値 (およびデータ型) です。

例外の強化

他の機能に負けじと、C# 6.0 では、例外に関する言語的な変更もいくつか行われています。まず、catch ブロックと finally ブロックの両方で await 句を使用できるようになります (図 11 参照)。

図 11 catch ブロックと finally ブロックでの await の使用

public static async Task<int> EncryptFilesAsync(string directoryPath, string searchPattern = "*")
{
  ConsoleColor color = Console.ForegroundColor;
  try
  {
  // ...
  }
  catch (System.ComponentModel.Win32Exception exception)
    if (exception.NativeErrorCode == 0x00042)
  {
    // ...
  }
  catch (AggregateException exception)
  {
    await LogExceptionsAsync(exception.InnerExceptions);
  }
  finally
  {
    Console.ForegroundColor = color;
    await RemoveTemporaryFilesAsync();
  }
}

C# 5.0 で await が導入されてから、catch ブロックと finally ブロックで await をサポートしてほしいという要求が、当初の予想をはるかに上回るほど大きくなりました。特に catch ブロックまたは finally ブロックから非同期メソッドを呼び出すパターンがよく使用されるのは、例外発生時にクリーンアップまたはログ記録を実行する場合などです。C# 6.0 では、どちらのブロック内でも非同期メソッドを呼び出すことができるようになります。

例外に関する 2 つ目の機能 (Visual Basic では 1.0 から提供されています) は、特定の例外の種類に基づくフィルター処理に追加する形で使用できる、例外フィルターのサポートです。これにより、if 句を指定して、その例外を catch ブロックでキャッチするかどうかを、追加で制限できるようになります (この機能は、例外をキャッチできなかったときに、実際の例外処理を実行することなく、その例外をログに記録するなど、二次的な処理に使用されることもあります)。この機能について注意すべきことの 1 つは、アプリケーションをローカライズする可能性がある場合、例外メッセージを使用して動作する catch 条件式を記述しないようにすることです。このような例外メッセージは、ローカライズ後に変更を加えない限り、正しく機能しなくなります。

まとめ

C# 6.0 で提供されるすべての機能について、最後にお伝えしておくことが 1 つあります。それは、これらの機能を使用するためには、Visual Studio 2015 以降に同梱されている C# 6.0 コンパイラーが当然必要になりますが、Microsoft .NET Framework の最新版は不要であることです。このため、たとえばコードを .NET Framework 4 に対してコンパイルする場合でも、C# 6.0 の機能を使用できます。これが実現される理由は、すべての機能が C# 6.0 コンパイラー内に実装されており、.NET Framework に依存していないためです。

駆け足で説明してきましたが、ここで C# 6.0 に対する私の見方をまとめます。ここで取り上げなかった残りの機能は 2 つだけです。これらは、コレクション初期化子で役立つ Add カスタム拡張メソッドの定義のサポート、そして大きな変更ではありませんが、オーバーロード解決の強化です。まとめると、C# 6.0 によって、根本的にコードが変わるわけではなく、少なくとも、ジェネリックや LINQ が導入されたときのような状況にはなりません。ただし、正しいコーディング パターンを、より単純に記述できるようになることは確かです。最も顕著な例は、デリゲートの Null 条件演算子ですが、文字列補間、nameof 演算子、自動プロパティの (特に読み取り専用プロパティに対する) 強化など、他にも多くの優れた機能が提供されます。

その他の情報については、次の追加資料を参照してください。

  • Mads Torgersen による説明ビデオ「C# 6.0 の新機能」(bit.ly/CSharp6Mads、英語)
  • 本稿執筆時点 (2014 年 12 月) 以降に投稿された C# 6.0 の更新に関する Mark Michaelis の C# ブログ (itl.tc/csharp、英語)
  • C# 6.0 の言語に関するディスカッション (roslyn.codeplex.com/discussions、英語)

さらに、2015 年の第 2 四半期に発売予定で、私が執筆している書籍の新版『Essential C# 6.0』 (Addison-Wesley Professional) もご覧ください (intellitect.com/EssentialCSharp、英語)。

このコラムが公開される頃には、C# 6.0 の機能に関するディスカッションは終わっているでしょう。しかし、これからの Microsoft が、オープン ソースのベスト プラクティスを使用したクロスプラットフォーム開発への投資に注力し、開発コミュニティと協力して、優れたソフトウェアを作成しようとしていることは間違いありません。このような理由から、C# 7.0 の初期設計に関するディスカッションは、オープン ソース フォーラムで行われるため、まもなくその内容を目にすることができるでしょう。


Mark Michaelis (itl.tc/Mark、英語) は、IntelliTect の創設者で、チーフ テクニカル アーキテクトとトレーナーを務めています。1996 年以来、C#、Visual Studio Team System (VSTS)、および Windows SDK の Microsoft MVP であり、2007 年には Microsoft Regional Director に認定されました。また、C#、Connected Systems Division、VSTS など、マイクロソフト ソフトウェアの設計レビュー チームにもいくつか所属しています。Michaelis は、開発者を対象としたカンファレンスで講演を行い、多数の記事や書籍を執筆しています。現在、Essential C# シリーズ (Addison-Wesley Professional) の新版の執筆に取り組んでいます。

この記事のレビューに協力してくれたマイクロソフト技術スタッフの Mads Torgersen に心より感謝いたします。