C#

強化された新しい C# 6.0

Mark Michaelis

C# 6.0 はまだ完成版ではありませんが、機能はほぼ完成に近いところまできています。2014 年 5 月のコラム「C# 6.0 言語プレビュー」(msdn.microsoft.com/magazine/dn683793.aspx) で紹介して以降、次期バージョンの Visual Studioの CTP3 リリース (コードネーム "14") に含まれる C# 6.0 には多くの変更や改良が加えられています。

今回は、新しい機能を紹介し、5 月に説明した機能の最新情報を示します。また、C# 6.0 の各機能の更新内容を説明した包括的な最新のブログも紹介しておきます。ブログについては itl.tc/csharp6 (英語) を確認してください。ブログで紹介している例の多くは、私が執筆中の『Essential C# 6.0』 (Addison-Wesley Professional) の新版から引用しています。

Null 条件演算子

.NET 開発の初心者でも、おそらく NullReferenceException についてはよくご存じでしょう。これは、開発者が事前に十分な null チェックを行わなかったために (null) オブジェクトを渡してメンバーを呼び出した場合に発生する例外で、ほとんどの場合はバグを表します。次の例を考えてみます。

public static string Truncate(string value, int length)
{
  string result = value;
  if (value != null) // Skip empty string check for elucidation
  {
    result = value.Substring(0, Math.Min(value.Length, length));
  }
  return result;
}

null をチェックしていなければ、メソッドは NullReferenceException をスローすることになります。これは単純ですが、文字列パラメーターが null かどうかを確認しなければならないのはやや面倒です。多くの場合、比較の頻度を考えれば無駄な処理です。C# 6.0 では、このようなチェックをより簡潔に記述できる null 条件演算子が新しく導入されました。

public static string Truncate(string value, int length)
{          
  return value?.Substring(0, Math.Min(value.Length, length));
}
[TestMethod]
public void Truncate_WithNull_ReturnsNull()
{
  Assert.AreEqual<string>(null, Truncate(null, 42));
}

Truncate_WithNull_ReturnsNull メソッドが示すように、オブジェクトの値が実際に null であれば、null 条件演算子によって null が返されます。このことから、次の例に示すように null 条件演算子が呼び出しチェーンに含まれていたらどうなるか、という疑問が生じます。

public static string AdjustWidth(string value, int length)
{
  return value?.Substring(0, Math.Min(value.Length, length)).PadRight(length);
}
[TestMethod]
public void AdjustWidth_GivenInigoMontoya42_ReturnsInigoMontoyaExtended()
{
  Assert.AreEqual<int>(42, AdjustWidth("Inigo Montoya", 42).Length);
}

この場合、Substring を null 条件演算子を使って呼び出しても、value?.Substring で null と評価されて null が返され、その null 値で PadRight が呼び出されるように見えます。しかし、この場合は言語の動作によって適切な処理が行われます。つまり、PadRight の呼び出しは行われず、即座に null が返されます。そのため、この条件演算子がなければ NullReferenceException が発生するプログラミング エラーが回避されます。この考え方を null 値の反映と呼びます。

null 条件演算子は、呼び出しチェーンの中でターゲットになるメソッドとその関連メソッドを呼び出す前に、条件として null かどうかをチェックします。場合によっては、text?.Length.GetType ステートメントのように、驚くべき処理が行われることもあります。

ターゲットの呼び出しが null のときは null 条件演算子は null を返します。そのとき、返された値を受け取る呼び出し側メンバーのデータ型が null を許容しない値型だとすると、結果のデータ型はどうなるでしょう。たとえば、value?.Length から返されるデータ型を単純に int にすることはできません。もちろん、答えは null 許容型 (int?) です。実際、結果を単に int に代入しようとすると、コンパイル エラーが発生します。

int length = text?.Length; // Compile Error: Cannot implicitly convert type 'int?' to 'int'

null 条件演算子には、2 つの構文形式があります。1 つは、疑問符の後にドット演算子 (?.) という形式です。もう 1 つは、疑問符をインデックス演算子と組み合わせて使用する形式です。たとえば、コレクションがあるとします。コレクションのインデックスを指定する前に明示的に null 値かどうかチェックするのではなく、次のように null 条件演算子を使用してこのチェックを行うことができます。

public static IEnumerable<T> GetValueTypeItems<T>(
  IList<T> collection, params int[] indexes)
  where T : struct
{
  foreach (int index in indexes)
  {
    T? item = collection?[index];
    if (item != null) yield return (T)item;
  }
}

上記の例では、null 条件付きインデックス形式の演算子 ?[...] を使用しているため、コレクションが null でない場合のみ、コレクションへのインデックス指定が行われます。この形式の null 条件演算子を使用した場合の T? item = collection?[index] ステートメントの動作は、次のステートメントと同じになります。

T? item = (collection != null) ? collection[index] : null.

null 条件演算子で可能なのは項目の取得のみです。項目の代入は機能しません。null コレクションを考えた場合、このことにはどういう意味があるのでしょう。

参照型で ?[...] を使用すると、あいまいになります。参照型は null にできるので、?[...] 演算子の結果が null の場合、コレクション自体が null だったのか、指定した要素自体が null だったのかがはっきりしません。

null 条件演算子を特に役立つように適用すると、C# 1.0 以降に存在していた C# の特異性、つまり、デリゲート呼び出し前の null チェックが解決されます。図 1 の C# 2.0 コードについて考えてみます。

図 1 デリゲート呼び出し前の null チェック

class Theremostat
{
  event EventHandler<float> OnTemperatureChanged;
  private int _Temperature;
  public int Temperature
  {
    get
    {
      return _Temperature;
    }
    set
    {
      // If there are any subscribers, then
      // notify them of changes in temperature
      EventHandler<float> localOnChanged =
        OnTemperatureChanged;
      if (localOnChanged != null)
      {
        _Temperature = value;
        // Call subscribers
        localOnChanged(this, value);
      }
    }
  }
}

null 条件演算子を使用すると、set の実装全体がシンプルに次のようにまとめられます。

OnTemperatureChanged?.Invoke(this, value)

null 条件演算子を前に付けて Invoke を呼び出すだけです。スレッド セーフにするためにローカル変数にデリゲート インスタンスを代入したり、デリゲートの呼び出し前に値が null かどうかを明示的にチェックする必要はなくなります。

C# の開発者は、これまでの 4 回のリリースでこれが改善されなかったことを不思議に思ってきました。ようやく実現された感じがします。この機能だけで、デリゲートの呼び出し方法が変わります。

もう 1 つ null 条件演算子がよく使われるようになると考えられるのは、合体演算子と組み合わせるパターンです。Length を呼び出す前に linesOfCode の null をチェックする代わりに、項目カウントのアルゴリズムを次のように記述できます。

List<string> linesOfCode = ParseSourceCodeFile("Program.cs");
return linesOfCode?.Count ?? 0;

この場合、任意の空のコレクション (項目なし) と null コレクションはどちらも、同じカウントを返すように正規化されます。つまり、null 条件演算子は次の処理を実行します。

  • オペランドが null の場合は null を返します。
  • オペランドが null の場合は、呼び出しチェーン内の他の呼び出しを行いません。
  • ターゲットのメンバーが値型を返す場合は、null 許容型 (System.Nullable<T>) を返します。
  • デリゲートの呼び出しをスレッド セーフ方式でサポートします。
  • メンバー演算子 (?.) とインデックス演算子 (?[...]) のどちらとしても使用できます。

自動プロパティの初期化子

これまで構造体を正しく実装してきた .NET 開発者は、型を不変にするために使用する構文の多さに間違いなくうんざりしています (.NET の標準が型を不変にすべきだと示しています)。問題なのは、読み取り専用プロパティには以下のものが必要だという点です。

  1. 読み取り専用で定義したバッキング フィールド
  2. コンストラクター内からのバッキング フィールドの初期化
  3. プロパティの明示的な実装 (自動プロパティを使用しない)
  4. バッキング フィールドを返す明示的な getter の実装

これらはすべて、不変プロパティを "正しく" 実装するためだけに必要です。この動作を、その型のすべてのプロパティに対して繰り返します。したがって、正しいことを行うには、かなり多くの労力が必要になります。C# 6.0 では、自動プロパティの初期化子という新機能が導入されます (CTP3 では、初期化式のサポートも導入されます)。自動プロパティの初期化子により、プロパティへの代入がプロパティの宣言内で直接行えるようになります。読み取り専用プロパティの場合、プロパティを確実に不変にするために必要なことがすべて処理されます。たとえば、次の FingerPrint クラスを考えてみます。

public class FingerPrint
{
  public DateTime TimeStamp { get; } = DateTime.UtcNow;
  public string User { get; } =
    System.Security.Principal.WindowsPrincipal.Current.Identity.Name;
  public string Process { get; } =
    System.Diagnostics.Process.GetCurrentProcess().ProcessName;
}

コードに示すように、プロパティの初期化子により、プロパティ宣言の一部として初期値を代入できます。プロパティは、読み取り専用プロパティ (getter のみ) または読み取り/書き込みプロパティ (setter と getter の両方) のいずれかになります。読み取り専用の場合、基になるバッキング フィールドは自動的に read-only 修飾子を使って宣言されます。これにより、初期化後は不変になります。

初期化子は、任意の式を指定できます。たとえば、条件演算子を使用すると、初期化値を既定値に設定できます。

public string Config { get; } = string.IsNullOrWhiteSpace(
  string connectionString =
    (string)Properties.Settings.Default.Context?["connectionString"])?
  connectionString : "<none>";

この例では、前回のコラムで説明したように、宣言式 (itl.tc/?p=4040、英語) を使用しているのがわかります。単なる式では表現できない場合は、初期化を静的メソッドにリファクタリングして、それを呼び出します。

Nameof 式

CTP3 リリースで導入されたもう 1 つの追加機能は、nameof 式のサポートです。コード内で「鍵になる文字列」を使用することがよくあります。ここで言う「鍵になる文字列」とは、コード内のプログラム要素にマップされる標準の C# 文字列です。たとえば、ArgumentNullException をスローするときに、例外の原因になった無効なパラメーター名を表す文字列を使用します。残念ながら、このパラメーター名のような「鍵になる文字列」はコンパイル時に検証されることはないので、(パラメーターの名前変更などによって) プログラム要素が変わっても「鍵になる文字列」が自動的に更新されることはありません。その結果、コンパイラが検出することはない不整合が生じます。

OnPropertyChanged イベントを発生させる場合などは、名前を抽出するツリー式を利用して「鍵になる文字列」の使用を避けることができる場合があります。プログラム要素の名前を特定するだけという操作の単純性を考えると、これでもやや面倒です。どちらの場合も、解決策は理想的とは言えません。

この特殊な場面に対処するため、C# 6.0 は "プログラム要素" の名前にアクセスできるようにします。プログラム要素は、クラス名、メソッド名、パラメーター名、特定の属性名 (おそらくリフレクションを使用する場合) のいずれでもかまいません。たとえば、図 2 のコードは、nameof 式を使用してパラメーターの名前を抽出しています。

図 2 Nameof 式を使用したパラメーター名の抽出

void ThrowArgumentNullExceptionUsingNameOf(string param1)
{
  throw new ArgumentNullException(nameof(param1));
}
[TestMethod]
public void NameOf_UsingNameofExpressionInArgumentNullException()
{
  try
  {
    ThrowArgumentNullExceptionUsingNameOf("data");
    Assert.Fail("This code should not be reached");
  }
  catch (ArgumentNullException exception)
  {
    Assert.AreEqual<string>("param1", exception.ParamName);
}

[TestMethod] に示すように、ArgumentNullException の ParamName プロパティには、値 param1 が含まれます。これは、メソッドの nameof(param1) 式を使用して設定されます。nameof 式を使えるのは、パラメーターに限りません。図 3 に示すように、どのようなプログラミング要素でも取得できます。

図 3 他のプログラム要素の取得

namespace CSharp6.Tests
{
  [TestClass]
  public class NameofTests
  {
    [TestMethod]
    public void Nameof_ExtractsName()
    {
      Assert.AreEqual<string>("NameofTests", nameof(NameofTests));
      Assert.AreEqual<string>("TestMethodAttribute",
        nameof(TestMethodAttribute));
      Assert.AreEqual<string>("TestMethodAttribute",
        nameof(
         Microsoft.VisualStudio.TestTools.UnitTesting.TestMethodAttribute));
      Assert.AreEqual<string>("Nameof_ExtractsName",
        string.Format("{0}", nameof(Nameof_ExtractsName)));
      Assert.AreEqual<string>("Nameof_ExtractsName",
        string.Format("{0}", nameof(
        CSharp6.Tests.NameofTests.Nameof_ExtractsName)));
    }
  }
}

より明示的なドット形式の名前を使用している場合でも、nameof 式が取得するのは最後の識別子のみです。属性の場合、"Attribute" というサフィックスは示されません。ただし、コンパイルには必要です。これにより、乱雑なコードを整理できるようになります。

プライマリ コンストラクター

自動プロパティの初期化子は、プライマリ コンストラクターと組み合わせると特に便利です。プライマリ コンストラクターは、共通のオブジェクト パターンに関する定型表現を減らします。この機能は、5 月のコラム以降、大幅に強化されています。更新された部分は次のとおりです。

  1. プライマリ コンストラクターの実装本体 (省略可能): プライマリ コンストラクターのパラメーターの検証や初期化など、以前はサポートされていなかった操作が可能になりました。
  2. フィールド パラメーターの除去: プライマリ コンストラクターのパラメーターを使ったフィールドの宣言 (以前の C# とは異なり特定の名前付け規則が強制されなくなるので、この機能が定義された当時はこの機能に前向きに取り組まないというのは正しい判断でした)。
  3. 式形式の関数とプロパティのサポート (後ほど説明)。

Web サービス、多層アプリケーション、データ サービス、Web API、JSON などのテクノロジが普及したことで、データ転送オブジェクト (DTO) ようなクラスがクラスの共通形式の 1 つになります。通常、DTO には多くの動作は実装せず、データの保存を単純にすることに力が注がれています。単純さに注力することで、プライマリ コンストラクターが魅力的になっています。たとえば、次の例に示す不変の Pair データ構造を考えてみます。

struct Pair<T>(T first, T second)
{
  public T First { get; } = first;
  public T Second { get; } = second;
  // Equality operator ...
}

コンストラクターの定義 Pair(string first, string second) は、クラスの宣言に含まれています。つまり、コンストラクターのパラメーターは first と second (それぞれ型 T) です。この 2 つのパラメーターは、プロパティの初期化子でも参照され、それぞれ対応するプロパティに代入されています。クラス定義、不変性サポート、必須コンストラクター (すべてのプロパティ/フィールドの初期化子) の単純さを見れば、単純さが正しいコーディングにいかに役立つかがわかります。こうした単純さは、以前不要な冗長さがあったパターンの大幅な改善につながります。

プライマリ コンストラクターの本体は、プライマリ コンストラクターの動作を指定します。これにより、通常のコンストラクターと同等の機能をプライマリ コンストラクターに実装できるようになります。たとえば、Pair<T> データ構造の信頼性を向上する手順の 1 つとして、プロパティを検証する場合があります。このような検証では、たとえば Pair.First では null 値が無効であることを確認します。CTP3 では、プライマリ コンストラクター本体 (つまり、宣言を含めないコンストラクター本体) を含められるようになります (図 4 参照)。

図 4 プライマリ コンストラクター本体の実装

struct Pair<T>(T first, T second)
{
  {
    if (first == null) throw new ArgumentNullException("first");
    First = first; // NOTE: Not working in CTP3
  }     
  public T First { get; }; // NOTE: Results in compile error for CTP3
  public T Second { get; } = second;
  public int CompareTo(T first, T second)
  {
    return first.CompareTo(First) + second.CompareTo(Second);
  }
// Equality operator ...
}

わかりやすくするために、ここではプライマリ コンストラクター本体をクラスの最初のメンバーにしていますが、C# の要件ではありません。プライマリ コンストラクター本体は、他のクラス メンバーと同列に任意の順序で指定できます。

CTP3 では機能しませんが、読み取り専用プロパティのもう 1 つの機能として、コンストラクター内から直接そのプロパティに代入できます (First = first など)。これはプライマリ コンストラクターだけでなく、すべてのコンストラクター メンバーでも可能です。

自動プロパティの初期化子をサポートすることで、以前のバージョンでは必要だった明示的なフィールド宣言の多くが必要なくなりました。明確な理由で削除できないフィールド宣言は、setter での検証が必要なシナリオです。これに対して、読み取り専用フィールドを宣言する必要はまったくなくなります。読み取り専用フィールドを宣言するときは、そのレベルのカプセル化が必要であれば、常に、読み取り専用の自動プロパティをプライベートとして宣言できるようになります。

上記の CompareTo メソッドにはパラーメーター first と second があり、見たところ、プライマリ コンストラクターのパラメーター名と重複しています。プライマリ コンストラクター名は自動プロパティの初期化子のスコープ内に含まれているため、first と second のスコープがあいまいに感じます。さいわい、これは同じスコープにはなりません。スコープを設定する規則はこれまでの C# とは異なります。

C# 6.0 より前のスコープは、常に、コード内の変数宣言の位置によって決まっていました。パラメーターは、そのパラメーターが宣言されたメソッド内にスコープが限定され、フィールドはクラス内に限定されます。if ステートメント内で宣言された変数は、if ステートメントの条件式本体内部の限定されます。

これとは対照的に、プライマリ コンストラクターのパラメーターは時間の制限を受けます。プライマリ コンストラクターのパラメーターは、プライマリ コンストラクターの実行中のみ "有効" です。プライマリ コンストラクター本体の場合はこの時間という考え方が明白ですが、自動プロパティの初期化子の場合はそれほど明白ではありません。

C# 1.0 以降では、フィールドの初期化子はクラス初期化の一環として実行されるステートメントに変換されます。自動プロパティの初期化子は、このフィールドの初期化子と同様の方法で実装されます。つまり、プライマリ コンストラクターのパラメーターのスコープは、クラス初期化子とプライマリ コンストラクター本体の有効期間に限定されます。自動プロパティの初期化子またはプライマリ コンストラクター本体の外部からプライマリ コンストラクターのプロパティを参照すると、コンパイル エラーになります。

プライマリ コンストラクターに関して覚えておきたい考え方が、ほかにもいくつかあります。基本コンストラクターを呼び出すことができるのは、プライマリ コンストラクターのみです。基本コンストラクターを呼び出すには、プライマリ コンストラクターの宣言の後に base (コンテキスト) キーワードを使用します。

class UsbConnectionException(
  string message, Exception innerException, HidDeviceInfo hidDeviceInfo):
    Exception  (message, innerException)
{
  public HidDeviceInfo HidDeviceInfo { get;  } = hidDeviceInfo;
}

コンストラクターを追加指定する場合、コンストラクターの呼び出しチェーンの最後にプライマリ コンストラクターを呼び出す必要があります。つまり、プライマリ コンストラクターでは this 初期化子を使用できません。プライマリ コンストラクターが既定のコンストラクターでもないとすると、他のすべてのコンストラクターには追加したコンストラクターを含める必要があります。

public class Patent(string title, string yearOfPublication)
{
  public Patent(string title, string yearOfPublication,
    IEnumerable<string> inventors)
    ...this(title, yearOfPublication)
  {
    Inventors.AddRange(inventors);
  }
}

これらの例は、プライマリ コンストラクターによって C# がシンプルになることを示しています。プライマリ コンストラクターを使用すると、単純な操作を複雑な方法ではなく、単純な方法で行うことができます。コードを読みにくくする複数のコンストラクターや呼び出しチェーンをクラスに含める必要がある場合もあります。プライマリ コンストラクター構文によってコードがシンプルにならず、見た目が複雑になるような場合はプライマリ コンストラクターを使用しません。C# 6.0 のすべての機能強化については、好ましくない機能がある場合やコードが読みにくくなる場合は使用しないようにします。

式形式の関数とプロパティ

式形式の関数は、C# 6.0 で構文が簡単になったもう 1 つの例です。式形式の関数とは、本体にステートメントを含まない関数です。式形式の関数では、関数宣言の後に式を使って本体を実装します。

たとえば、次の式は ToString のオーバーライドの 1 つを Pair<T> クラスに追加しています。

public override string ToString() => string.Format("{0}, {1}", First, Second);

式形式の関数に関しては、特に新しいことはありません。C# 6.0 に含まれる機能のほとんどと同様、実装がシンプルになるように簡略化した構文を提供することが目的です。当然、式の戻り値の型は、関数宣言で指定した戻り値の型と一致する必要があります。上記の場合、関数を実装する式と同様、ToString は文字列を返します。void や Task を返すメソッドには、何も返さない式を実装します。

式形式による単純化は、関数に限りません。読み取り専用 (getter のみ) のプロパティも式を使用して実装できます。つまり、式形式のプロパティです。たとえば、FingerPrint クラスに Text メンバーを次のように追加できます。

public string Text =>
  string.Format("{0}: {1} - {2} ({3})", TimeStamp, Process, Config, User);

その他の機能

C# 6.0 では廃止予定の機能がいくつかあります。

  • インデックス付きプロパティ演算子 ($) は使用できなくなり、C# 6.0 では想定されません。
  • インデックス メンバー構文は CTP3 では機能しません。ただし、C# 6.0 の今後のリリースでは再度使用できるようになる予定です。
var cppHelloWorldProgram = new Dictionary<int, string>
{
[10] = "main() {",
[20] = "    printf(\"hello, world\")",
[30] = "}"
};
  • プライマリ コンストラクター内のフィールドの引数は C# 6.0 には含まれなくなります。
  • 現在、バイナリ数値リテラルと、数値リテラル内の数値区切り記号 ('_') は両方とも、製品版に間に合うかどうかわかりません。

5 月のコラムで既に説明したので、ここで取り上げていない機能がたくさんありますが、静的 using ステートメント (itl.tc/?p=4038、英語)、宣言式 (itl.tc/?p=4040、英語)、および例外処理の強化 (itl.tc/?p=4042、英語) は、安定した状態の機能です。

まとめ

明らかに、開発者は C# に熱心に取り組んでおり、その成果を維持したいと考えています。言語チームは、開発者からのフィードバックを真剣に受け取り、その意見を参考に言語に変更を加えています。どうぞ遠慮しないで、roslyn.codeplex.com (英語) にアクセスし、読者の皆さんのお考えをチームにお知らせください。リリースまでの C# 6.0 の更新情報について、itl.tc/csharp6 (英語) も忘れずに確認してください。


Mark Michaelis は、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 に心より感謝いたします。