nameof with Generics

Daniel A. White picture Daniel A. White · Apr 26, 2015 · Viewed 12.9k times · Source

I was experimenting with nameof with generics. I didn't get the result I was expecting. I'm not sure if this is part of the spec or not.

class MainClass
{
    public static void Main (string[] args)
    {
        Console.WriteLine ($"Hello { nameof(FooBar<string>)! }");
    }
}

class FooBar<T> { }

The output I get is

Hello FooBar!

I would expect some details about the type parameters.

I tried it with a method and that fails with a compiler error:

class MainClass
{
    public static void Main (string[] args)
    {
        Console.WriteLine ($"Hello { nameof(Do<string>) }");
    }

    public static T Do<T>() {}
}

Error CS8084: An argument to nameof operator cannot be method group with type arguments (CS8084) (foo)

Is this because nameof is a compile-time construct and generics are types initialized at runtime? Or is there some other limitation?

Answer

Yuval Itzchakov picture Yuval Itzchakov · Apr 26, 2015

I would expect some details about the type parameters

From the design docs:

Result of nameof. The result of nameof depends on the symbols that its argument bound to:

One or more members: if all members have the same metadata name then the result of nameof is that name; otherwise it is an error "This argument refers to multiple elements with different names". The metadata name of a member IorI< isA1...AK>` is simply "I" after standard identifier transformations have been applied.

The <T> parameter is removed due to standard identifier transformations (section §2.4.2 in the C# specification) which doesn't permit <> as valid identifiers. First any leading @ is removed, then Unicode escape sequences are transformed, and then any formatting characters are removed. This of course still happens at compile-time. You can also see this when you try to print out the name of a generic type:

typeof(List<string>).Name;

Will result in:

List`1

Is this because nameof is a compile-time construct and generics are types initialized at runtime? Or is there some other limitation?

The second error is specified as invalid by design to avoid overload resolution complications inside nameof:

Allow generic type arguments? Presumably 'yes' when naming a type since that's how expression binding already works. And presumably 'no.' when naming a method-group since type arguments are used/inferred during overload resolution, and it would be confusing also to have to deal with that in nameof.

We can see that clearly in the roslyn codebase:

private BoundExpression BindNameofOperatorInternal(InvocationExpressionSyntax node, 
                                                   DiagnosticBag diagnostics)
{
    CheckFeatureAvailability(node.GetLocation(), MessageID.IDS_FeatureNameof, diagnostics);

    var argument = node.ArgumentList.Arguments[0].Expression;
    string name = "";

    // We relax the instance-vs-static requirement for top-level member access expressions by creating a NameofBinder binder.
    var nameofBinder = new NameofBinder(argument, this);
    var boundArgument = nameofBinder.BindExpression(argument, diagnostics);

    if (!boundArgument.HasAnyErrors && CheckSyntaxForNameofArgument(argument, out name, diagnostics) && boundArgument.Kind == BoundKind.MethodGroup)
    {
        var methodGroup = (BoundMethodGroup)boundArgument;
        if (!methodGroup.TypeArgumentsOpt.IsDefaultOrEmpty)
        {
            // method group with type parameters not allowed
            diagnostics.Add(ErrorCode.ERR_NameofMethodGroupWithTypeParameters, argument.Location);
        }
        else
        {
            nameofBinder.EnsureNameofExpressionSymbols(methodGroup, diagnostics);
        }
    }

    return new BoundNameOfOperator(node, boundArgument, ConstantValue.Create(name), Compilation.GetSpecialType(SpecialType.System_String));
}

For the full specification, see: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/expressions