Open jnm2 opened 1 month ago
Finally! As of today I use a dummy internal type named _
so I can have expressions like
nameof(A<_>)
nameof(Dictionary<_,_>.Count)
@koszeggy How do you use discards then, e.g. expr switch { Something => 1, _ => 2 }
?
From LDM notes:
We have sympathy. It feels like a corner that's cut. But it's quite expensive to implement, and has semantic dark corners (List<>.First.Foo).
Is this still the case and the LDT is now willing to take on the cost?
Would implementing this feature make it cheaper to do a similar feature in typeof
? typeof(List<>.Count)
? Saving additional runtime reflection by making typeof work statically on members would be really nice.
@MgSam I don't think the view is that this is expensive anymore, and my view is that it's straightforward to address the mentioned corners. Back at the time, the corners were coming as an afterthought, but they're priced into my proposal and the implementation ends up as 40 lines of compiler code.
I can't speak to the comparative cost of adding member lookup to typeof
, but this nameof
work doesn't feel like it leads up to that. Unlike typeof
, nameof
already does member lookup, and it's being loosened to allow unbound generic types.
🎉 LDM approved the proposal! A backup plan was also pre-approved. If preferable for implementation, we could do a version of this which did not provide lookup through generic type constraints.
Summary
Allows unbound generic types to be used with
nameof
, as innameof(List<>)
to obtain the string"List"
, rather than having to specify an unused generic type argument in order to obtain the same string.Motivation
This is a small feature that removes a common frustration: why do I have to pick a generic type argument when the choice has no effect on the evaluation of the expression? It's very odd to require something to be specified within an operand when it has no impact on the result. Notably,
typeof
does not suffer from this limitation.It's not just about code that better expresses itself. Once some arbitrary type argument has been chosen in a
nameof
expression, such asobject?
, changing a constraint on a type parameter can break uses ofnameof
unnecessarily. Insult becomes added to injury in this scenario. Satisfying the type parameter can sometimes require declaring a dummy class to implement an interface which is constraining the type parameter. Now there's unused metadata and a strange name invented, all for the purpose of adding a type argument to thenameof
expression, a type argument whichnameof
will ultimately ignore even though it requires it.In some rarer cases, with a generic class constraint, it's not even possible to use
nameof
because it's not possible to inherit from a base class which is used as a generic constraint, due to the base class having an internal constructor or internal abstract member.There's a lot of bang for the buck in fixing this one. When you hit this, it feels like a paper cut. It's gotten steady attention from the community, including an initial proposal by Jon Skeet. Implementation complexity is very low. A language design member and a community member have each implemented this feature in the compiler for purposes of investigation.
Description
Unbound type names become available for use with
nameof
:nameof(A<>)
evaluates to"A"
nameof(Dictionary<,>)
evaluates to"Dictionary"
Additionally, chains of members will be able to be accessed on unbound types, just like on bound types:
nameof(A<>.B)
evaluates to"B"
nameof(A<>.B.Count)
evaluates to"Count"
Even members of generic type parameters can be accessed, consistent with how
nameof
already works when the type is not unbound. Since the type is unbound, there is no type information on these members beyond whatSystem.Object
or any additional generic constraints provide.nameof(A<,>.B)
evaluates to"B"
nameof(A<,>.B.Count)
evaluates to"Count"
.Not supported
Support is not included for nesting an unbound type as a type argument to another generic type, such as
A<B<>>
orA<B<>>.C.D
. Even though this could be logically implemented, such expressions have no precedent in the language, and there is not sufficient motivation to introduce it:A<B<>>
provides no benefits overA<>
, andA<B<>>.C
provides no benefits overA<>.C
.If
C
returns theT
ofA<T>
,A<B<>>.C.D
can be written more directly asB<>.D
. If it returns some other type, thenA<B<>>.C.D
provides no benefits overA<>.C.D
.Support is not included for partially unbound types, such as
Dictionary<int,>
. Similarly, such expressions have no precedent in the language, and there is not sufficient motivation. That form provides no benefits overDictionary<,>
, and accessing members ofT
-returning members can be written more directly without wrapping in a partially unbound type.Detailed design
Member lookup on an unbound type expression will be performed the same way as for a
this
expression within that type declaration.No change is needed for the cases listed in the Not supported section. They already provide the same errors for
nameof
expressions as they do fortypeof
.No change is needed when the syntax
nameof
binds to a method named 'nameof' rather than being the contextual keywordnameof
. Passing any type expression to a method results in "CS0119: '...' is a type, which is not valid in the given context." This already covers unbound generic type expressions.Design meetings
https://github.com/dotnet/csharplang/blob/main/meetings/2017/LDM-2017-11-06.md#roslyn-20450 https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-10-16.md#unbound-generic-types-in-nameof