-
Notifications
You must be signed in to change notification settings - Fork 1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Proposal]: typeof
string constants
#8505
Comments
I like it. If we do this, i'd like to do all the stirng ones at the same time. So Name/Namespace/FullName. We'll def see people wanting help with namespaces, so this will be nice to kill two birds with one stone. |
I'm concerned about this. I'm not a huge fan of presuming what the implementation of
|
Note: all those cases where the runtime guarantees a constant, and we have exactly that information statically, you would get hte same string. only7 in the cases where the name is not a constant (it actually may vary at runtime) would you be disallowed from doing this. so, if you say |
I also want to point out that these are the same strings the compiler already emits into the binary when the same
This may require some work on the runtime side to document guarantees that the runtime is willing to make. I'd be happy to help with that, like I did with the Module Initializers feature. |
I do not see anywhere in the ECMA 335 standard that specifies that reflection's |
@333fred What you might be worried about is that there is no longer a check the strongly named assembly here. Another possible bad thing is: suppose .NET Team decides to change the format of |
I think a more robust solution would be for the runtime to allow "runtime constants" to be used where currently only compile-time constants are allowed. Related dotnet/runtime#108416. Though, that would preclude older runtimes. |
I'm worried about precisely what I said: I don't feel comfortable presuming how the implementation of FullName formats things. No more, no less. |
For this to not depend too much on the runtime format, I think this could be treated as a constant only in constant context (where it's not allowed today), anywhere else However, I think nullability can still apply anywhere since these are now well-known members. |
@jnm2 It might be nicer if we could have more control so for example imagine we could quote part of what exists inside |
If this were to be implemented, I would expect I do think @alrz has a good point about cases where the type name at compile time might not match the type name at runtime. You could put it on the obfuscator to identify and replace string constants that contain type names, but that might be a bridge too far? |
@alrz That's a good point about post-compilation rewriters. Even as a constant, in some cases, it's not always clear if So, two things. First, this is already an issue if you're moving or mangling types. Trimming has this same issue. Qualified type names can come from many places, including config files, and post-compilation renaming already breaks scenarios like that. This would just be another example among many. Existing mitigations should work, such as Second, the way to perfectly avoid this issue is for the post-compilation step to have access to the source code and Roslyn semantics for the strings that it's seeing. That way post-compilation can tell the difference between |
@iam3yal I'd be interested in hearing concrete use cases. Anything we build will have to contend with the split between metadata names, which use backticks for generics and |
@jnm2 I might misunderstanding you but the use cases are exactly the same use cases as in the OP. My point was to enhance The backticks in my suggestions can be used only and only in the context of Just to hopefully make things crystal clear, It doesn't have to be backticks so they are really not that important to the point I'm trying to make. It's just a different take that aims to achieve the same thing. :) |
I expect we would use backtics for something much larger in the language. |
Instead of treating the existing expression as some kind of constexpr, which may or may not have the desired effects, how about considering a small number of special methods that would be unimplemented and which the compiler would recognize and replace with the constant values at compile-time? That would give the C# compiler full control over what the methods are expected to return. // replaced at compile-time
var fullName = Constants.FullNameOf(typeof(Foo));
var fullClrName = Constants.FullClrNameOf(typeof(Foo));
// compiler errors
var x = Constants.FullNameOf(typeof(T)); // generics
var y = Constants.FullNameOf(t); // Type variable |
I would think sooner of keyword-first syntax in the line of |
@CyrusNajmabadi I understand. Although it can be anything else like double quotes iirc it's invalid inside |
I think this is yet another use case for deterministic/constant functions. HaloFour's idea makes sense to me. Instead of tying this to the attribute argument type encoding (which we apparently cannot do), we should have the language and the actual Yes, this means this feature will not be available for a while, but I think it's important to get it right and the need for this specific use case is not that dire to justify making it an exceptional case. |
Keywords are expensive. It's new syntax and that has a substantial cost associated with it. At the minimum, it leads to the "do we take a breaking change or
At that point it would be more of a compiler feature than a language one. Essentially a set of intrinsics the compiler could define in the form of methods. There would be a significant cost to design this out and model them in the compiler (there is no concept of constant methods today and that would take work to introduce). After that though it should be more iterative to add new ones. Where else would we use this? If it were just for full name it's probably not worth the cost. But if we had a host of problems we could solve this way it may be an idea worth exploring and see where it goes. |
LDM rejected as likely never, with sympathy for the scenarios but worry about dark corners. There is interest in a more ambitious solution. One such example could be a form of constexpr with interception, where source generator plugins can decide how to provide constants. There's also a workaround today if you're willing to get up to some ceremony. This comes in the form of source generators generating constants for types that you ask for. So if you find yourself in a corner needing a constant and not wanting to hardcode a string, you could write a source generator. |
One potential workaround is a code fixer:
I've never written a code fixer, so I can't guarantee it's a workable approach - just throwing an idea out there in case somebody finds it useful. |
@TahirAhmadov That's a cool idea. However if you perform a rename or Find All References on |
Hahaha, there may be a solution for those issues: string s = "Namespace.Foo" + GetFullName<Foo>();
// defined somewhere globally
string GetFullName<T>() => ""; Or something along those lines, hopefully in a way that it gets optimized away. |
string s = GetFullName<Foo>("Namespace.Foo");
// defined somewhere globally
string GetFullName<T>(string v)
{
#if DEBUG
if(typeof(T).IsGeneric) throw new Exception("Invalid type"); // list all the criteria here
if(v != typeof(T).FullName) throw new Exception("Literal incorrect, use code fixer to update");
#endif
return v;
} |
@TahirAhmadov Places where a method can be called, there would be no reason not to use |
You're right, of course. For some reason I approached it from the perspective of performance improvement and totally forgot about constant context. Oh well. I think constant functions can really solve this and many other scenarios nicely. I hope it's the next big thing that the language gets. |
(Now if we did your first idea, it would have to be called |
Adding const string s = constexpr(typeof(int).FullName); Just 2 cents. |
@hrumhurum Adding |
@iam3yal See #8505 (comment) for what this is referring to. I'm interested in constexpr which is driven by interception via constant instead of interception via method. |
@jnm2 Can you briefly clarify what you expect to happen from something like this?
I mean, do you expect the compiler to stop on |
It would be exactly like what interception is today. Rather than source-generating a method with an interception attribute, you'd source-generate a constant with a similar interception attribute. The compiler would require something to intercept the constexpr expression or it would error. |
Summary
For certain kinds of types,
typeof(
...).FullName
is considered a constant value. It is allowed anywhere a string constant is allowed, such as in an attribute argument or a constant interpolated string.Motivation
The community has requested this feature often. Several duplicates and many variants have been filed, garnering a decent number of upvotes. This would be easy to implement, and it would bring multiple benefits.
First, this change would allow
typeof(
...).FullName
to be used in places where it is the obvious thing to use, but where it is currently disallowed. Today you'll be faced with writing workarounds such as:With this proposal, this would be able to be written naturally as
[UseThisLogger(typeof(Logger<>).FullName)]
.typeof
expressions are already given special treatment by the language as attribute arguments, and this special treatment is extended a little further to make[Attr1(typeof(Xyz).FullName)]
work alongside[Attr2(typeof(Xyz))]
.Second, this change would bring performance improvements in existing code where
typeof(
...).FullName
is used to build strings. The entire string which includes the type name could become constant, rather than interpolating or concatenating as a runtime operation.Detailed design
When
.FullName
is used immediately on atypeof
expression, and the referenced type is a supported kind of type, thetypeof(
...).FullName
expression is a string constant. Types are supported when their resultingFullName
strings are known at compile time and cannot change at runtime.Such an expressions will be a constant everywhere, not just in places where a constant is required. The value of the constant is the same as the value the expression would have if evaluated at runtime.
Supported type kinds
Unbound generics are supported.
typeof(List<>).FullName
would produce the constant string"System.Collections.Generic.List`1"
.Nested types are supported.
typeof(List<>.Enumerator).FullName
would produce the constant string"System.Collections.Generic.List`1+Enumerator"
.Primitive type keywords are supported.
typeof(nint).FullName
would produce the constant string"System.IntPtr"
. This includestypeof(void).FullName
, which produces"System.Void"
.Aliases are supported. The constant string resolves the actual type, the same way the expression would evaluate at runtime.
Array types and pointer types are supported, both individually and when composed over each other.
typeof(int[])
ortypeof(int*)
ortypeof(int[,][]**)
all have constantFullName
s. When these composable types are used, their element types must also be supported types.Unsupported type kinds
Type parameters are not supported.
typeof(T)
is a type which is not known at compile time.Bound generics are not supported.
typeof(List<int>).FullName
contains parts known only at runtime. For example:"System.Collections.Generic.List`1[[System.Int32, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]]"
. The compiler may see only a reference assembly with a different name, or an assembly may load with a different version at runtime than at compile time.Nullable value types and tuple types are special cases of bound generics and are not supported.
Nullability
The
FullName
property onSystem.Type
is annotated as nullable, but whentypeof(Xyz).FullName
is a constant, it is never null. When this expression is constant, the compiler's nullability analysis should report it as not-null. This avoids the inevitable need for!
each time this feature is used with an attribute, since it's not likely that the attribute's type name argument is an optional one.This will also be a benefit in places that use
typeof(Xyz).FullName!
today.Possible extensions
typeof(Xyz).Name
andtypeof(Xyz).Namespace
are components of the full name, and could be supported in the same way. UnlikeFullName
andName
,Namespace
could evaluate to a constant null string.typeof(Xyz<>).Name
differs fromnameof
. Whereasnameof
returns the C# language name of the type or alias,typeof(Xyz<>).Name
returns the name in metadata, such as"Xyz`1"
.typeof(Xyz).Namespace
would make it less of a hassle to refer to a full namespace. Currently, the only resort is something like:Alternatives
Some of the variants of this request are for a
fullnameof
orpathof
operator. The output would diverge, sincenameof
today returns the C# name of the type or alias being used whiletypeof(
...).Name
returns the metadata name, suitable for reflection. Metadata names use backticks in generic type names and the+
separator between containing types and nested types. This same distinction would presumably be maintained betweenfullnameof
andtypeof(
...).FullName
.fullnameof
could reference namespaces without requiring the naming of a type within the namespace.fullnameof
would also be extendable to reference a member, and this is sometimes requested. However, referencing a member along with its containing type name and full namespace seems like specialized use, producing strings which are not usable with any core .NET API. This may be more of a job for aninfoof
operator, providing a reference to the member and letting it be examined at runtime to produce policy-specific formatting suited to the use case.Design meetings
https://github.com/dotnet/csharplang/blob/main/meetings/2024/LDM-2024-10-16.md#typeof-string-constants
The text was updated successfully, but these errors were encountered: