Start Debugging

C# 14: 未バインドのジェネリック型に対する nameof のサポート

C# 14 では nameof 式が拡張され、List<> や Dictionary<,> などの未バインドのジェネリック型に対応し、ダミーの型引数が不要になりました。

C# 14 では、言語に対していくつかの小さな、しかし便利な改善が導入されています。その新機能の一つが nameof 式の拡張で、未バインドのジェネリック型 をサポートするようになりました。簡単に言えば、ジェネリック型の名前を取得するためだけにダミーの型引数を入れる必要がなくなった、ということです。この更新は C# 開発者が長年抱えてきた小さな煩わしさを解消し、nameof を使うコードをより整然と保守しやすくします。

未バインドのジェネリック型とは

C# における ジェネリック型 とは、型パラメーターを持つクラスや構造体のことです (例: List<T>Dictionary<TKey, TValue>)。未バインドのジェネリック型 とは、具体的な型引数が指定されていないジェネリック型の定義そのものです。空の山かっこ (例: List<>) や、型パラメーターの数を示す山かっこ内のカンマ (例: 2 つの型パラメーターを持つ Dictionary<,>) によって、未バインドのジェネリック型を見分けることができます。これは、TTKey/TValue が何であるかを示さずに、ジェネリック型を 一般的な形で 表します。未バインドのジェネリック型は完全には特定されていないため、直接インスタンス化することはできませんが、特定の文脈では使用できます (例えば typeof を介した reflection など)。例えば、typeof(List<>) はオープンなジェネリック型 ListSystem.Type オブジェクトを返します。

C# 14 より前は、ほとんどの式の中で未バインドのジェネリック型を使用すること できませんでした。それらは主に reflection や属性のシナリオで使われていました。コード内でジェネリック型を名前で参照したい場合、通常は具体的な型引数を指定する必要があり、その結果として クローズドな ジェネリック型になりました。例えば、List<int>Dictionary<string, int> は、すべての型パラメーターが指定されているため クローズドなジェネリック型 です。これまで C# 開発者はジェネリック型の名前そのものが欲しいだけのときでも、構文を満たすために任意の型 (例えば objectint) を選ぶことが多くありました。

C# 14 以前の nameof の挙動

nameof 式は、変数、型、またはメンバーの名前を文字列として生成するコンパイル時の機能です。識別子を文字列にハードコーディングするのを避けるため (例えば、引数の検証やプロパティ変更通知のため) に広く使われています。C# 14 より前の nameof には、ジェネリックを扱う際の制限がありました。引数として未バインドのジェネリック型を使うことは できません でした。nameof の引数はコード上の有効な式または型識別子である必要があり、つまりジェネリック型には具体的な型引数が必要だったのです。実際には、ジェネリック型の名前を取得するためにダミーの型パラメーターを与える必要がありました。

例えば、文字列 "List" (ジェネリッククラス List<T> の名前) が欲しい場合、C# 13 以前では次のように書く必要がありました。

string typeName = nameof(List<int>);  // evaluates to "List"

ここでは List<int> を使い、任意の型引数 (int) を指定していますが、結果に対して型の選択は無関係です。List<> のような未バインドの形を型引数なしで使おうとすると、コードはコンパイルできません。コンパイラーは “未バインドのジェネリック名” のようなエラーを出します。式が期待される文脈ではこの形は許可されていなかったためです。言い換えると、nameof はあくまで名前 "List" だけを気にして型引数は無視するのに、有効な式にするためだけに型パラメーターを 指定する必要があった のです。

この要件は、単に言語仕様上の癖でした。これは扱いにくく、もろいコードにつながることがありました。例えば、開発者は nameof を使うためだけに型パラメーターのプレースホルダーとして objectint を使うことがよくありました。後でジェネリック型に新しい制約 (例えば T が参照型でなければならない、特定のクラスを継承していなければならないなど) が追加されると、ダミーの型が制約を満たさなくなり、nameof の使用箇所が壊れることがありました。高度なケースでは、適切な型を見つけること自体が容易ではありませんでした (例えば T が internal なクラスや、既存のいかなる型も実装していないインターフェースに制約されていた場合、nameof を使うためだけにダミーのクラスを作る必要がありました)。これらはすべて、nameof の結果には実際には影響しないことのための余計な手間でした。

C# 14 における未バインドのジェネリック型を伴う nameof

C# 14 では、nameof 式の中で未バインドのジェネリック型を直接使用できるようにすることで、この問題を解決しました。nameof の引数として、型パラメーターを指定せずにジェネリック型定義を渡せます。結果はあなたの期待どおり、nameof はジェネリック型の名前を返します。これにより、ようやく nameof(List<>) と書いて、ダミーの型引数なしに文字列 "List" を取得できるようになりました。

変更点を分かりやすくするため、C# 14 の前後でジェネリック型の名前を取得する方法を比較してみましょう。

C# 14 より前:

// Using a closed generic type (with a type argument) to get the name:
Console.WriteLine(nameof(List<int>));    // Output: "List"

// The following was not allowed in C# 13 and earlier – it would cause a compile error:
// Console.WriteLine(nameof(List<>));    // Error: Unbound generic type not allowed

C# 14 以降:

// We can use an unbound generic type directly:
Console.WriteLine(nameof(List<>));       // Output: "List"
Console.WriteLine(nameof(Dictionary<,>)); // Output: "Dictionary"

上記のとおり、nameof(List<>)"List" に評価され、同様に nameof(Dictionary<,>)"Dictionary" を返します。ジェネリック型に対して nameof を使うためだけに偽の型引数を与える必要はなくなりました。

この改善は、型自身の名前を取得することだけにとどまりません。通常の型と同様に、未バインドのジェネリック型のメンバー名を取得するためにも使えます。例えば、nameof(List<>.Count) は C# 14 では有効な式となり、文字列 "Count" を返します。以前のバージョンでは同じ結果を得るために nameof(List<int>.Count) のように、<int> の代わりに何らかの具体的な型を指定する必要がありました。C# 14 ではそのような文脈でも型引数を省略できます。一般に、nameof(SomeGenericType<...>.MemberName) を使うあらゆる場面で、特定の型を持っていなかったり、特定の型に決め打ちしたくなかったりする場合は、ジェネリック型を未バインドのままにできます。

この機能はあくまでコードの利便性と明瞭さのためのものである点に注目する価値があります。nameof 式の出力は変わっておらず、依然として識別子の名前です。変わったのは、言語仕様が nameof に対してより広い入力を許容するようになったことです。これにより nameof は、すでにオープンなジェネリック型を許容していた typeof と歩調が揃います。本質的に C# 言語は、これらのケースで型パラメーターを指定する必要があったのは、最初から不要な要件だったと認めたわけです。

なぜこれが便利か

未バインドのジェネリック型を nameof で許可することは些細な調整に見えるかもしれませんが、実用上のメリットがいくつかあります。

あなたのコードに何が変わるか

nameof 式における未バインドのジェネリック型のサポートは、C# 14 において歓迎すべき改善で、言語を少しだけ開発者にとってフレンドリーにします。nameof(List<>) のような構文を許可することで、C# は古い煩わしさを取り除き、開発者が不要な定型コードなしに意図を表せるようにします。この変更はすべての C# ユーザーに恩恵をもたらします。初心者は nameof をジェネリックと共に使う際の混乱を避けられ、熟練開発者は将来の変更に強い、より洗練されたコードを得られます。これは C# チームが言語の “papercut” に対処し、整合性を改善した好例です。C# 14 を採用するときには、ジェネリック型の名前が必要になった場面でこの機能を思い出し、より整然として簡潔なコードを書く楽しみを味わってください。

参考資料

  1. What’s new in C# 14 | Microsoft Learn
  2. Generics and attributes – C# | Microsoft Learn
  3. The nameof expression – evaluate the text name of a symbol – C# reference | Microsoft Learn
  4. Unbound generic types in nameof – C# feature specifications (preview) | Microsoft Learn
  5. What’s new in C# 14 | StartDebugging.NET

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< 戻る