December 2015

Volume 30 Number 13

Essential .NET - C# 7 の設計

Mark Michaelis | December 2015

Mark Michaelisこのコラムが公開される頃、C# 7 設計チームが、議論、計画、実験、プログラミングを重ねて、約 1 年間が経つことになります。今回は、C# 7 設計チームが取り組んでいるアイデアの一部をいくつか紹介します。

ここで紹介するのは、C# 7 に何を取り込むかを検討するためのアイデアにすぎないことを忘れないでください。話題に上っただけのものもあれば、実験的実装の範囲でしか実現されていないものもあります。確定したものはなく、多くは日の目を見ないかもしれません。かなり前に進んだアイデアもありますが、言語をまとめる最終段階で削除されるかもしれません。

Null 許容参照型と Null 非許容参照型の宣言

C# 7 の議論の中でも最も有力なアイデアは、C# 6.0 の Null 条件演算子の延長線上にあり、Null 操作をさらに強化する考え方です。最も簡単な機能強化の 1 つは、コンパイラーまたはアナライザーによる検証です。この検証は、Null 許容型のインスタンスにアクセスして、実際にその型が Null になるかどうかを事前にチェックするというものです。

Null 値を許容できない参照型が Null 値になるのを完全に回避できるとしたらどうでしょう。これは、参照型を宣言する際に Null 値を許容する (string?) か、許容しない (string!) かを意思表示するというアイデアです。理論的には、新しいコードのすべての参照型宣言を、既定で Null 非許容型と想定することも可能です。ただし、Eric Lippert と共同で執筆した『Essential C# 6.0』(Addison-Wesley Professional、2015 年) で指摘したように、コンパイル時に参照型が絶対に Null にならないようにすることは極めて困難です (bit.ly/1Rd5ekS、英語)。たとえ可能だとしても、ある型が Null になる可能性があるのに、Null でないことをチェックしないでその型を逆参照していまうシナリオは十分考えられます。あるいは、Null を許容しないと意思表示しても、その型に Null を代入してしまうシナリオも考えられます。

チームは、もっと多くのメリットが得られるように、パラメーターで Null 非許容型宣言を利用して、自動的に Null チェックを生成する可能性を議論しています (ただし、こうした Null チェックはパフォーマンスの低下につながるため、これを回避するためにコンパイル時に明示的に選択できるようなオプトイン オプションにする必要があるかもしれません)。

(データベースから取得したデータなど、整数値に null が含まれるのを許容しなければならない状況が多々あったため、C# 2.0 では Null 許容の値型を追加しましたが、皮肉なことに、C# 7 では参照型で正反対のことをサポートしようと考えています。)

Null 非許容のもの (string! のテキストなど) を参照する型をサポートするのに問題になるのは、共通中間言語 (CIL) での実装をどうするかです。最も有力な 2 つの提案は、NonNullable<T> 型構文にマップする方法と、[Nullable] 文字列テキストのように属性を利用する方法です。現時点では、後者が有力です。

タプル

タプルは C# 7 で検討中のもう 1 つの機能です。タプルは、以前のバージョンの C# でも何度か話題になりましたが、依然として導入には至っていません。これは、型をひとまとめに宣言して、宣言に 1 つ以上の値を含めたり、メソッドから 1 つ以上の値を返したりできるようにするというアイデアです。以下のサンプル コードでこの考え方を理解してください。

public class Person
{
  public readonly (string firstName, int lastName) Names; // a tuple
  public Person((string FirstName, string LastName)) names, int Age)
  {
    Names = names;
  }
}

上記のリストに示すように、タプルのサポートにより、2 つ (またはそれ以上) の値を持つタプルとして、型を宣言できます。これは、データ型を使用できるところ (フィールド、パラメーター、変数宣言、メソッドの戻り値) であればどこでも利用できます。たとえば、以下のコード スニペットは、メソッドからタプルを返します。

public (string FirstName, string LastName) GetNames(string! fullName)
{
  string[] names = fullName.Split(" ", 2);
  return (names[0], names[1]);
}
public void Main()
{
  // ...
  (string first, string last) = GetNames("Inigo Montoya");
  // ...
}

上記のリストでは、タプルを返すメソッドと、GetNames の結果を代入する first と last の変数宣言があります。この代入は、タプル内の順序に基づいて行われます (受け取る変数の名前ではありません)。現在使用されている別のアプローチ (配列やコレクション、カスタム型、出力パラメーター) をいくつか考えてみると、タプルは魅力的なオプションです。

タプルを適切に宣言するオプションはたくさんあります。現在、以下のようなオプションを検討しています。

  • 以下に示すように、タプルは、名前付きプロパティまたは無名プロパティを持つことができます。
var name = ("Inigo", "Montoya")

および

var name = (first: "John", last: "Doe")
  • 以下に示すように、結果を無名型または明示的な変数にすることもできます。
var name = (first: "John", last: "Doe")

または

(string first, string last) = GetNames("Inigo Montoya")
  • 以下に示すように、配列をタプルに変換する可能性もあります。
var names = new[]{ "Inigo", "Montoya" }
  • 以下に示すように、タプルの個々の項目に名前でアクセスすることができます。
Console.WriteLine($”My name is { names.first } { names.last }.”);
  • データ型が明示的に指定されていない場合は、データ型を推論します (一般的な無名型と同じアプローチに従います)。

タプルは複雑ですが、大部分は C# 言語で既に十分確立されている構造に従っているため、C# 7 での導入はかなり有力です。

パターン マッチング

パターン マッチングも、C# 7 設計チームの議論でよく話題になります。パターン マッチングのわかりやすい表現の 1 つは、おそらく、拡張 switch (および if) ステートメントです。このステートメントの case ステートメントでは、単なる定数ではなく、式のパターンをサポートします (拡張 case ステートメントに対応させる場合、switch 式の型も対応する定数値の型に制限されません)。パターン マッチングにより、switch 式が特定の型かどうか、特定のメンバーを持つ型かどうか、特定の "パターン" や式に一致する型かどうかなど、switch 式をパターンに対してクエリできます。たとえば、obj が 2 以上の x 値を持つ Point 型の場合に分岐させるとします。

object obj;
// ...
switch(obj) {
  case 42:
    // ...
  case Color.Red:
    // ...
  case string s:
    // ...
  case Point(int x, 42) where (Y > 42):
    // ...
  case Point(490, 42): // fine
    // ...
  default:
    // ...
}

興味深いことに、case ステートメントに式を指定する場合は、case ステートメントに移動する引数でも式を許可する必要があります。

Point 型の case をサポートするには、パターン マッチングを処理する Point 型のメンバーの型も必要になります。この場合、必要なのは、int 型の 2 つの引数を受け取るメンバーです。たとえば、以下のようなメンバーです。

public static bool operator is (Point self out int x, out int y) {...}

where 式がなければ、case Point(490, 42) に決して到達しないため、コンパイラーはエラーまたは警告を表示します。

switch ステートメントの制限要因の 1 つは、値を返すのではなく、コード ブロックを実行するところにあります。値を返す switch 式では、以下に示すように、パターン マッチングの追加機能をサポートできる可能性があります。

string text = match (e) { pattern => expression; ... ; default => expression }

同様に、is 演算子でもパターン マッチングをサポートできる可能性があります。型チェックを許可するだけでなく、型に特定のメンバーが存在しているかどうかについて、より汎用的なクエリをサポートできる可能性があります。

レコード

C# 6.0 で検討され (最終的には却下され) た "コンストラクター" の省略宣言構文の延長線上にあり、コンストラクター宣言をクラス定義に埋め込む、「レコード」という概念のサポートがあります。たとえば、以下の宣言について考えてみます。

class Person(string Name, int Age);

この簡単なステートメントは、自動的に以下を生成することになります。

  • コンストラクター
public Person(string Name, int Age)
{
  this.Name = Name;
  this.Age = Age;
}
  • 読み取り専用プロパティ (変更不可の型を作成)
  • 等式の実装 (GetHashCode、Equals、== 演算子、!= 演算子など)
  • ToString の既定の実装
  • "is" 演算子のパターン マッチング サポート

膨大な量のコードが生成されます (たった 1 行の短いコードですべてが作成されます) が、これにより、手作業で実装する基本定型実装に対して、相応のショートカットを用意できる可能性が期待されます。さらに、すべてのコードを「既定のコード」と考え、明示的に実装したメンバーを優先し、同じメンバーの生成を除外することができます。

レコードに関連する最も難しい問題の 1 つが、シリアル化を処理する方法です。おそらく、データ転送オブジェクト (DTO) としてレコードを活用することになるでしょうが、このようなレコードのシリアル化をサポートするためにできることは (あるとしても) 明白ではありません。

レコードに関連して、with 式のサポートがあります。with 式により、既存のオブジェクトに基づいて新しいオブジェクトのインスタンスを作成できます。person オブジェクトの宣言があるとした場合、たとえば、以下のように with 式を使用して新しいインスタンスを作成できます。

Person inigo = new Person("Inigo Montoya", 42);
Person humperdink = inigo with { Name = "Prince Humperdink" };

この with 式に対応して以下のようなコードが生成されます。

Person humperdink = new Person(Name: "Prince Humperdink", Age: inigo.42 );

ただし、別の提案として、with 式のコンストラクターのシグネチャに依存するのではなく、以下のように With メソッドの呼び出しに置き換える方がよいという意見もあります。

Person humperdink = inigo.With(Name: "Prince Humperdink", Age: inigo.42);

非同期ストリーム

C# 7 での非同期サポートの強化対象は、非同期シーケンスを処理する考え方です。たとえば、Current プロパティと Task<bool> MoveNextAsync メソッドを備えた IAsyncEnumerable があるとします。この場合、foreach を使用して、IAsyncEnumerable インスタンスを反復処理でき、コンパイラーがストリームの各メンバーの非同期呼び出しを処理します (つまり、シーケンス (おそらく、チャネル) 内に処理する別の要素があるかどうかを判断するために、await を実行します)。これについては、評価すべき注意点がたくさんあります。最も小さな注意点は、IAsyncEnumerable を返す LINQ 標準クエリ演算子のすべてで発生する LINQ 肥大化の可能性です。また、CancellationToken サポートや、Task.ConfigureAwait でさえ、公開する方法が定かではありません。

コマンド ラインでの C#

Windows PowerShell は、コマンドライン インターフェイス (CLI) で Microsoft .NET Framework を利用できるようにしています。この機能の愛用者としては、検討中の機能の中で、コマンド ラインでの C# のサポートが最も気になっています。つまり、一般に Read-Evaluate-Print Loop (REPL) のサポートといわれる考え方です。REPL サポートは、C# スクリプティングに付随するもので、形式張らない簡単なシナリオでお決まりの形式 (クラス宣言など) を一切必要としないというものです。コンパイル手順がないため、REPL には、アセンブリや NuGet パッケージを参照する新しいディレクティブや、追加ファイルのインポートが必要になります。現在の提案では、以下のサポートを検討中です。

  • 追加のアセンブリや NuGet パッケージを参照するための「#r」。このバリエーションとなる「#r!」は、いくつか制約を設けたうえで、内部メンバーへのアクセスも許可します (これは、ソース コードのあるアセンブリにアクセスするシナリオを意図しています)。
  • ディレクトリ全体を含めるための「#l」(F# と同様)。
  • 追加の C# スクリプト ファイルをインポートするための「#load」。順序が重要になることを除けば、プロジェクトにスクリプト ファイルを追加する方法と同じです (C# スクリプトでは名前空間が許可されないため、.cs ファイルのインポートはサポートされない可能性があります)。
  • 実行中にパフォーマンス診断を有効にするための「#time」。

最初のバージョンの C# REPL は、(同じ機能セットをサポートする更新版の対話型ウィンドウと共に) Visual Studio 2015 Update 1 でリリースされる予定です。詳細については、Itl.tc/CSREPL (英語) と、来月のこのコラムをご覧ください。

まとめ

この 1 年間にまとめた資料は多すぎて、設計チームが取り組んできたことをすべて取り上げることはできません。今回説明したアイデアだけでも、考慮すべき詳細 (注意点やメリット) はまだまだあります。とは言え、チームが何に取り組んでいて、今でも十分強力な C# 言語をどのような観点で強化しようとしているかについて知っていただければさいわいです。C# 7 の設計メモを直接レビューしたい方、ご意見・ご感想をお寄せいただける方は、bit.ly/CSharp7DesignNotes (英語) から議論にご参加ください。


Mark Michaelis は、IntelliTect の創設者で、同社でチーフ テクニカル アーキテクト兼トレーナーを務めています。20 年にわたって Microsoft MVP に認定され、2007 年から Microsoft Regional Director を務めています。また、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 (英語のみ) です。

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