Why can nameof not be used with alias-qualified types at the root level?
Asked Answered
L

2

32

Imagine a type at the root namespace level (could be in the default global space, or could potentially be an extern alias).

It appears that this type cannot be referred to via nameof(), when using the alias prefix. It works fine with typeof, and via using aliases (although nameof on a using alias yields the alias name, not the type name). The compiler objects with CS8083, "An alias-qualified name is not an expression."

But: is there a reason for this? is this trying to prevent some obscure problem scenario? or meet some specification minutae? or is it perhaps a compiler bug? I'm also fully content to note that we shouldn't usually declare types in the namespace root - CA1050 is very right about this; but that's not the point here :)

Full example follows; note that in this example uses two projects for the using alias check, but that for simplicity everything involving C can just be ignored for a simple investigation.

extern alias foo;
using System;
using X = global::A;
using Y = global::FunWithNamespaces.B;
using Z = foo::C;

public class A { }

namespace FunWithNamespaces
{
    public class B { }
    public class Program
    {
        static void Main()
        {
            // oddness is on the lines marked ## CS8083

            // relative-qualified using typeof
            Console.WriteLine(typeof(X).Name); // A, expected
            Console.WriteLine(typeof(Y).Name); // B, expected
            Console.WriteLine(typeof(Z).Name); // C, expected
            Console.WriteLine(typeof(A).Name); // A
            Console.WriteLine(typeof(B).Name); // B
            // note: can't talk about C without using an alias qualifier or a using-alias
            Console.WriteLine(typeof(Console).Name); // Console

            // relative-qualified things using nameof
            Console.WriteLine(nameof(X)); // X; I'm on the fence about X vs A, but... whatever
            Console.WriteLine(nameof(Y)); // Y; I'm on the fence about Y vs B, but... whatever
            Console.WriteLine(nameof(Z)); // Z; I'm on the fence about Z vs C, but... whatever
            Console.WriteLine(nameof(A)); // A
            Console.WriteLine(nameof(B)); // B
            // note: can't talk about C without using an alias qualifier or a using-alias
            Console.WriteLine(nameof(Console)); // Console

            // alias-qualified things using typeof
            Console.WriteLine(typeof(global::A).Name); // A
            Console.WriteLine(typeof(global::FunWithNamespaces.B).Name); // B
            Console.WriteLine(typeof(foo::C).Name); // C
            Console.WriteLine(typeof(global::System.Console).Name); // Console

            // alias-qualified things using nameof
            // ??? Console.WriteLine(nameof(global::A)); // ## CS8083 An alias-qualified name is not an expression
            Console.WriteLine(nameof(global::FunWithNamespaces.B)); // B
            // ??? Console.WriteLine(nameof(foo::C)); // ## CS8083 An alias-qualified name is not an expression
            Console.WriteLine(nameof(global::System.Console)); // Console
        }
        
    }
}

where C is defined in a separate assembly and referenced with aliases specified as foo, and is simply:

public class C { }

Edit: in terms of the specification, this comes down to https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/expressions#nameof-expressions, where a nameof_expression must be either a simple_name (which it isn't) or a named_entity_target '.' identifier type_argument_list? - so: for all cases that aren't a simple name, there must be a .something - but I guess the underlying question here is why must there be a .something, vs some other construction that permits global::Foo? For example:

nameof_expression
    : 'nameof' '(' named_entity ')'
    ;

named_entity
    : simple_name
    | named_entity_target '.' identifier type_argument_list?
    | qualified_alias_member type_argument_list?
    ;

named_entity_target
    : 'this'
    | 'base'
    | named_entity 
    | predefined_type 
    | qualified_alias_member
    ;
Lactic answered 9/9, 2021 at 10:34 Comment(10)
There seems to be some related discussion here: github.com/dotnet/csharplang/discussions/911Dempstor
I think this is worthy of a github issue. It seems strange to say the least and potentially an obscure design decision worthy of an authoritive explanation. Edit ^^Makings
Honestly, if anyone could answer that I'd expected Marc to do :DEmanation
@GyörgyKőszeg that has been asserted but not demonstrated. It was certainly a perfectly valid expression a few lines above :) the fact that the Microsoft.Whatever example passes means nothing: that could also resolve to be a namespace rather than a typeLactic
This specific issue is also mentioned on GitHub. A reply here says: it's not a valid expression syntactically (added before Marc's comment). Update: Maybe it will be fixed along with this proposal filed by Jon Skeet, which seems to be the restriction of the same expression syntax limitation. But I can't tell why global::C is valid though. Maybe add a comment to the GitHub issue.Hugely
@GyörgyKőszeg I don't think that proposal of Jon Skeet's affects this?Kirstiekirstin
@GyörgyKőszeg in terms of it being a valid nameof_expression - the real question is "why is that rule like it is?" - not "why is the compiler following the rule?"; I've clarified with citationsLactic
@canton7: this comment suggests that they are related. I cannot confirm it though.Hugely
@MatthewWatson thanks; opined :)Lactic
also linking for context: github.com/dotnet/roslyn/issues/1590Lactic
L
1

Answer to underlying question

if you came here for compile-time desision - there is no way at this moment. It is an expected behavior from compilator side as result of expression compilation.

Quick runtime solution

(typeof(f::SomeCustomClassName)).Name instead of nameof(f::SomeCustomClassName)

Explanation

Now lets see again to nameof_expression (link)

nameof_expression = 'nameof' '(' named_entity ')'

where named_entity is

named_entity = named_entity_target ('.' identifier type_argument_list?)*

And lets look again to calling class name. In common variant without aliases we got: nameof(SomeCustomClassName) that unfolds to nameof(ConsoleApp1.SomeCustomClassName)

That matches named_entity_target.identifier.

But the alias nameof(f::SomeCustomClassName)...

From specification

named_entity_target
    : simple_name
    | 'this'
    | 'base'
    | predefined_type 
    | qualified_alias_member
    ;

Is qualified_alias_member so named_entity_target = qualified_alias_member

So nameof_expression unfolds to

qualified_alias_member ('.' identifier type_argument_list?)*

Lets have a look to qualified_alias_member (link):

qualified_alias_member
    : identifier '::' identifier type_argument_list?
    ;

According to all of this we have

identifier '::' identifier ('.' identifier type_argument_list?)*

That does not match to nameof(f::SomeCustomClassName) Thats why it is incorrect for compilator. Thats why it need for dot nameof(f::SomeCustomClassName.SomeCustomClassName2)

it is not a bug. It is miscalculation from expression creators or porposive limitation.

Lutz answered 17/3, 2022 at 6:13 Comment(3)
Yes, I mentioned the named_entity_target etc in the question; I guess the real question is (per your last line): "is this an intentional limitation? If so, what intent? If not, can the limitation be removed?"Lactic
Only if microsoft decides it matters. But current state of work with aliases is unfortunate and most of coders doesn't use them because this reason too. If IDE's developers will support better approach to work with aliases, only then something can change when more people pay attention to this problem. In .net core issue tracker no info about changes with aliases so not in the coming years.Lutz
UPD. I mean that changes on compiler side no issue opened. But a lot of issues of work with aliases from IDE. So all as I thought.Lutz
O
-1

The resolution of the expression is done during compilation, hence the CSxxx compilation errors. So the operator nameof() has no effect during runtime.

Plus, it might use .Net Reflection to get the name of the value that you gave as parameter. Therefore to resolve a type via reflection, the compiler needs the "full path" of the member (i.e from the namespace to it).

Oquendo answered 3/2, 2022 at 0:40 Comment(1)
Nobody said anything about runtime; and an alias-qualified name is a "full path"Lactic

© 2022 - 2024 — McMap. All rights reserved.