Modern C# · Lesson 40

C# 14 nameof accepts unbound generics

Distinguish a compile-time display name from reflection type identity.

External .NET 10 SDK / stable C# 14. This project does not run in SharpRune's .NET 9 playground.

Read or download the complete SDK project, then follow its setup on your computer. The separate C# companion practice below runs in SharpRune’s .NET 9 console environment.

The concept and environment

C# 14 extends nameof to accept unbound generic types such as a dictionary with unspecified type arguments. The external project demonstrates that exact syntax. The portable companion uses closed generic types inside nameof and separately uses an open generic inside typeof, which earlier language versions already support. Both programs produce the same small display names, but they exercise different compiler acceptance rules. A compile-time name is a string for readable diagnostics; it is not a complete runtime Type, a constructed generic identity, or a collision-free serialization discriminator.

Walk through the portable program

The first portable expression names Dictionary of integer and string, producing Dictionary without namespace or generic arguments. The second names List of integer and produces List. The typeof expression asks whether the open dictionary type is a generic type definition, so it prints true. Output is Dictionary, List, and True. The third line uses reflection on a real Type object; it is not derived from the preceding text. Keeping those operations separate prevents a simple display name from being mistaken for all of the information present in a generic type definition.

Compile the genuine external feature

The external Program changes only the nameof operands to the unbound forms Dictionary with two empty argument positions and List with one. It still uses typeof for the reflection observation. A net10.0 project with language version 14.0 compiles those operands as the actual stable C# feature. The result strings do not display the open argument slots, which is precisely why this feature is a naming convenience rather than a textual generic signature format. Run the supplied project externally instead of modifying the browser companion to pretend its compiler understands the new operands.

Pitfalls and limits

Do not infer that because typeof has long accepted an unbound generic, older nameof must accept it too. Each operator has its own syntax and version rules. Likewise, two types in different namespaces can share a simple name, and constructed versions of one generic type share that name as well. Renaming an application symbol can intentionally change a nameof result at compile time. A stable external protocol should define its identifiers deliberately rather than depending on a refactoring-sensitive display string that omits namespace, assembly, and generic argument information.

Practice one: isolate a contract

First add a nameof call for Dictionary with different closed argument types in the portable program and predict the same Dictionary result. Hint: the string names the generic type declaration's simple name rather than encoding its construction. Then compare the actual closed Type objects and predict that different type arguments produce distinct runtime types. Keep those identity checks separate from string equality. This exercise supplies a concrete counterexample to the assumption that matching nameof text means two generic constructions are interchangeable in every API or reflection operation.

Practice two: challenge an assumption

For another exercise, define two small types with the same simple name in different namespaces and produce their names with explicit qualification. Predict matching strings but different Type identities. Hint: qualification selects the symbol; nameof still returns its simple name. Then design a deliberate stable identifier for a wire format or plugin registry and explain why it is different from a diagnostic label. In the external project, retain unbound generic nameof only where naming the generic definition is actually the intended operation, rather than sprinkling new syntax into unrelated reflection code.

Review the resulting contract

A useful review separates symbol selection, display naming, generic construction, and runtime identity. The companion's three lines demonstrate those distinctions without requiring a compiler upgrade, while the external project proves the added operand syntax under C# 14. If the goal is readable exception messages or logging, a simple name can be appropriate. If the goal is identifying types across boundaries, choose a representation with the required stability and completeness. Matching printed names are evidence about those strings only; they do not establish generic assignability, assembly identity, or protocol compatibility.

External .NET 10 SDK / stable C# 14. This project does not run in SharpRune's .NET 9 playground.

C# 14 nameof accepts unbound generics lab

Set up the SDK project

  1. Save every listed file at its exact relative path in a fresh directory.
  2. Install a .NET 10 SDK supporting stable C# 14; check dotnet --version.
  3. Run dotnet run --project FeatureLab.csproj. Keep this external code separate from the .NET 9 playground.

Download complete SDK project ↗

Project files

Program.csDownload file ↗
using System.Collections.Generic;
Console.WriteLine(nameof(Dictionary<,>));
Console.WriteLine(nameof(List<>));
Console.WriteLine(typeof(Dictionary<,>).IsGenericTypeDefinition);
FeatureLab.csprojDownload file ↗
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <LangVersion>14.0</LangVersion>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
</Project>

Expected result

Expected console output, in this order: Dictionary List True The portable companion compares the same small rule using older syntax; it does not compile or emulate the new feature.

Use the listed SDK and dependencies for this project. Open C# practice runs the separate portable exercise.

C# companion practice

using System;
using System.Collections.Generic;
Console.WriteLine(nameof(Dictionary<int, string>));
Console.WriteLine(nameof(List<int>));
Console.WriteLine(typeof(Dictionary<,>).IsGenericTypeDefinition);
Open C# practice

C# practice output

Dictionary
List
True
Try this
Compare closed-type nameof with external open-type syntax. Older typeof support for unbound types does not imply older nameof support.

Further reading