Runtime: Add GenericTypeParameters to Type

Created on 2 Sep 2017  路  11Comments  路  Source: dotnet/runtime

Proposed Additions

class Type { // implementation provided by TypeInfo override public virtual Type[] GenericTypeParameters => throw new NotImplementedException() }

Rationale

Unified Metadata/Runtime Type representation

Type.GenericTypeArguments and TypeInfo.GenericTypeParameters were introduced during an effort to split Type into a runtime representation (Type) and a metadata representation (TypeInfo). In such a world, a type's generic arguments belonged on the runtime representation while a type's generic parameters belonged on the metadata representation. Hence, GenericTypeArguments was declared on Type and GenericTypeParameters on TypeInfo.

Alas, the cleaving type into runtime/metadata representations didn't take root (see https://github.com/dotnet/coreclr/issues/13714#issuecomment-326300477), TypeInfo was deprecated (to the extent possible), and Type's original unified abstraction restored for the .NET Standard 2.0 release. In this new world, the metadata and runtime abstractions live next to each other. So GenericTypeParameters, the metadata representation, belongs next to it's runtime counterpart, GenericTypeArguments.

Symmetry

The 2.0 generic API was symmetric between Type and MethodInfo (see below). If we desire to maintain that symmetry then if/when GenericMethodParameters is added to MethodBase (see https://github.com/dotnet/corefx/issues/23741) GenericTypeParameters will need to be declared on Type.

| Type | TypeInfo | MethodInfo |
| ---- | ---- | ---- |
| IsGenericType | | IsGenericMethod |
| IsGenericTypeDefinition | | IsGenericMethodDefinition |
| GetGenericArguments | | GetGenericArguments |
| GetGenericTypeDefinition | | GetGenericMethodDefinition |
| MakeGenericType | | MakeGenericMethod |
| IsConstructedGenericType | | IsConstructedGenericMethod |
| ContainsGenericParameters | | ContainsGenericParameters |
| GenericTypeArguments | | GenericMethodArguments [new] |
| GenericTypeParameters [new] | GenericTypeParameters | GenericMethodParameters [new] |

api-needs-work area-System.Reflection

Most helpful comment

If we're going to consider hoisting an api from TypeInfo to Type, we should approach it holistically rather than piecemeal.

My rationale would be:

  • TypeInfo is now a conceptual white elephant and it'd be good to eliminate reasons to sprinkle GetTypeInfo() in the code.

  • With that in mind, it's worth looking though its apis to see if there's anything useful we can hoist back up to Type. (Useful enough, that is, to justify adding members to an already crowded class.)

Here's what I'd pick and choose:


       public virtual Type[] GenericTypeParameters { get; }

Yes. I like the clarity of specifying that I'm really talking about parameters, not arguments, and I'd like to be able to do that without introducing the (admittedly small) overhead of a GetTypeInfo call.


        public virtual IEnumerable<ConstructorInfo> DeclaredConstructors { get; }
        public virtual IEnumerable<EventInfo> DeclaredEvents { get; }
        public virtual IEnumerable<FieldInfo> DeclaredFields { get; }
        public virtual IEnumerable<MemberInfo> DeclaredMembers { get; }
        public virtual IEnumerable<MethodInfo> DeclaredMethods { get; }
        public virtual IEnumerable<TypeInfo> DeclaredNestedTypes { get; }
        public virtual IEnumerable<PropertyInfo> DeclaredProperties { get; }

Yes. This the kind of core mechanism that Type should always have had and I often find myself annoyed that having to choose between the windy GetTypeInfo().DeclaredX and the even worse GetX(BindingFlags.Public|BindingFlags.NonPublic|BindingFlags.Instance|BindingFlags.Static|BindingFlags.DeclaredOnly) The DeclaredOnly bit is easy to overlook.

We'd probably want to change DeclaredNestedTypes to return IEnumerable<Type>


        public virtual EventInfo GetDeclaredEvent(string name);
        public virtual FieldInfo GetDeclaredField(string name);
        public virtual MethodInfo GetDeclaredMethod(string name);
        public virtual TypeInfo GetDeclaredNestedType(string name);
        public virtual PropertyInfo GetDeclaredProperty(string name);

No. These promote writing code that's fragile with respect to overloads (especially when called on types you don't own.) and don't provide any escape hatch that remove that fragility. Type already has a large number of member query apis that do provide that escape hatch - we don't want to duplicate that. I'm surprised these even made it onto .NETCore 1.0.


        public virtual IEnumerable<MethodInfo> GetDeclaredMethods(string name);

No. The use case is pretty limited (using it as a starting point for your own query) and is duplicated by existing apis. I don't think this meets the -100 point test.


        public virtual IEnumerable<Type> ImplementedInterfaces { get; }

No. Just duplicates GetInterfaces() and arguably violates the rules about properties being cheap to compute. This api walks a hierarchy and deduplicates a list - not exactly trivial.


        public virtual bool IsAssignableFrom(TypeInfo typeInfo);
        TypeInfo System.Reflection.IReflectableType.GetTypeInfo();
        public virtual Type AsType();

No. These serve no purpose if you're trying avoid TypeInfo.

All 11 comments

If we're going to consider hoisting an api from TypeInfo to Type, we should approach it holistically rather than piecemeal.

My rationale would be:

  • TypeInfo is now a conceptual white elephant and it'd be good to eliminate reasons to sprinkle GetTypeInfo() in the code.

  • With that in mind, it's worth looking though its apis to see if there's anything useful we can hoist back up to Type. (Useful enough, that is, to justify adding members to an already crowded class.)

Here's what I'd pick and choose:


       public virtual Type[] GenericTypeParameters { get; }

Yes. I like the clarity of specifying that I'm really talking about parameters, not arguments, and I'd like to be able to do that without introducing the (admittedly small) overhead of a GetTypeInfo call.


        public virtual IEnumerable<ConstructorInfo> DeclaredConstructors { get; }
        public virtual IEnumerable<EventInfo> DeclaredEvents { get; }
        public virtual IEnumerable<FieldInfo> DeclaredFields { get; }
        public virtual IEnumerable<MemberInfo> DeclaredMembers { get; }
        public virtual IEnumerable<MethodInfo> DeclaredMethods { get; }
        public virtual IEnumerable<TypeInfo> DeclaredNestedTypes { get; }
        public virtual IEnumerable<PropertyInfo> DeclaredProperties { get; }

Yes. This the kind of core mechanism that Type should always have had and I often find myself annoyed that having to choose between the windy GetTypeInfo().DeclaredX and the even worse GetX(BindingFlags.Public|BindingFlags.NonPublic|BindingFlags.Instance|BindingFlags.Static|BindingFlags.DeclaredOnly) The DeclaredOnly bit is easy to overlook.

We'd probably want to change DeclaredNestedTypes to return IEnumerable<Type>


        public virtual EventInfo GetDeclaredEvent(string name);
        public virtual FieldInfo GetDeclaredField(string name);
        public virtual MethodInfo GetDeclaredMethod(string name);
        public virtual TypeInfo GetDeclaredNestedType(string name);
        public virtual PropertyInfo GetDeclaredProperty(string name);

No. These promote writing code that's fragile with respect to overloads (especially when called on types you don't own.) and don't provide any escape hatch that remove that fragility. Type already has a large number of member query apis that do provide that escape hatch - we don't want to duplicate that. I'm surprised these even made it onto .NETCore 1.0.


        public virtual IEnumerable<MethodInfo> GetDeclaredMethods(string name);

No. The use case is pretty limited (using it as a starting point for your own query) and is duplicated by existing apis. I don't think this meets the -100 point test.


        public virtual IEnumerable<Type> ImplementedInterfaces { get; }

No. Just duplicates GetInterfaces() and arguably violates the rules about properties being cheap to compute. This api walks a hierarchy and deduplicates a list - not exactly trivial.


        public virtual bool IsAssignableFrom(TypeInfo typeInfo);
        TypeInfo System.Reflection.IReflectableType.GetTypeInfo();
        public virtual Type AsType();

No. These serve no purpose if you're trying avoid TypeInfo.

Woot! Love it.

Meta: I was going to open separate issues for each one of these APIs on TypeInfo just because sometimes people prefer to tackle 'em piece meal. But if the powers that be wanna do 'em all at once then I'm all for that.


        public virtual IEnumerable<ConstructorInfo> DeclaredConstructors()
        public virtual IEnumerable<EventInfo> DeclaredEvents()
        public virtual IEnumerable<FieldInfo> DeclaredFields()
        public virtual IEnumerable<MemberInfo> DeclaredMembers()
        public virtual IEnumerable<MethodInfo> DeclaredMethods()
        public virtual IEnumerable<TypeInfo> DeclaredNestedTypes()
        public virtual IEnumerable<PropertyInfo> DeclaredProperties()

As noted, there are rules about properties being cheap to compute and the plural APIs load all the infos so are certainly not cheap. So, if we move them to Type, might we consider making them methods (as above) instead of properties. The Linq to XML API provide precedent: E.g. IEnumerable<XElement> Elements() instead of IEnumerable<XElement> Elements { get; }.

And if they're methods, then they could be made extension methods and we might consider adding them to RuntimeReflectionExtensions. Of course, if we do that, then we'll (unfortunately) want to prefix them all with Get to make them symmetric with all the GetRuntimeXXX APIs.


        public virtual EventInfo GetDeclaredEvent(string name);
        public virtual FieldInfo GetDeclaredField(string name);
        public virtual MethodInfo GetDeclaredMethod(string name);
        public virtual TypeInfo GetDeclaredNestedType(string name, int arity);
        public virtual PropertyInfo GetDeclaredProperty(string name, Type[] signature);
        public virtual IEnumerable<MethodInfo> GetDeclaredMethod(string name, Type[] signature);

These methods, re-formulated by adding signature and arity so they never throw an AmbiguousMatchException, would, IMHO, make good companion APIs to the plurals above. While it's true you could start with the plurals above and fliter them to the method you want, that requires populating all the info caches first. Providing methods that bind directly save having to create infos that are just going to be tossed.

Again, these could all be be put onto RuntimeReflectionExtensions. And if we do that then we should make the Declared and Runtime method families symmetric. So if we add signature and arity to Declared then we should add them to Runtime. And if we have vestigial APIs on Runtime (e.g. GetRuntimeMethods) then we should addpreserve them on Declared.

Regarding RuntimeReflectionExtensions - I think it's okay to put these on Type or on System.Reflection.TypeExtensions. RuntimeReflectionExtensions was a landing place for apis that expose how the underlying runtime layouts vtables and overrides virtuals in arcane situations and so forth. The apis we're talking about here are completely runtime-agnostic. It's purely about the metadata description.

These methods, re-formulated by adding signature and arity so they never throw an AmbiguousMatchException, would, IMHO, make good companion APIs to the plurals above.

We'd have to add generic arity, calling convention and even return type (operator explicit is an example of a method that can and does overload by differing return type only.) Still, if we can get rid of the BindingFlags and even better, the custom binder, they could be a simpler, less error-prone set of apis than the classic ones. I'd call the prospects on this a 50-50... let's keep it on the plate, anyway.

What if I rephrase the goal as

Providing methods that allow the user to specify _a single info_ for reflection to return and in so doing reflection will only activate and cache that info.

That would necessarily require an API that allows passing all necessary metadata to disambiguate infos. Worthy goal?

And what about:

  • Get prefix so we're symmetric with existing reflection APIs returning enumerations regardless of where the methods eventually live?
  • If not Get prefix, then how about methods instead of properties, like LINQ to XML?

Right, I think we're on the same page. We want an api that lets you pass everything that guarantees a single (or zero) result.

The existing api

Type.GetMethod(string name, int genericParameterCount, BindingFlags bindingAttr, Binder binder, CallingConventions callConvention, Type[] types, ParameterModifier[] modifiers)

comes pretty close. I know of at least two things it's missing:

  1. Return type (e.g. the operator explicit case, though it's not limited to that of course.)

  2. Custom modifiers on parameter types. You can overload on them too.

The thing is, that for this to have any chance of getting approved, it really has to get it right. We've got so many overlapping apis to do the 99% job already - if it doesn't really solve something 100%, it's not going to be added.

Well, I'm all for getting it right! Seems reasonable to have a reflection API that provides all the parameters necessary to uniquely identify an info!

I have to admit I didn't know that the _runtime_ actually allowed for _binding_ on the custom modifiers. I thought they were totally ignored -- basically just goo for the compiler and reflection to play with. So, I'll look forward to seeing the test case that actually binds at runtime to one method or another based on custom modifiers.

And what about the Get prefix convention and/or method convention for items returning enumerations? Seems like it'd be nice to have a uniform convention for these things (as much as is possible give the mix that's already been shipped).

I have to admit I didn't know that the runtime actually allowed for binding on the custom modifiers. I >thought they were totally ignored

The typical example of a custom modifier is CppConst (probably not the exact name) that represents the C++ "const" modifier. You can see why overloading on that is needed.

And what about the Get prefix convention and/or method convention for items returning enumerations?

Seems reasonable, but let's nail down what the api does first - naming can be pinned down later in the process.

Hm, we're conflating issues; the "Get" prefix relates to the "Declared" properties on TypeInfo not the uber GetMethod signature.

I agree that "If we're going to consider hoisting an api from TypeInfo to Type, we should approach it holistically rather than piecemeal" but I think grouping this entire discussion into a single bug is going to (has already) become a bit unwieldy.

To that end, let's at least carve out this one-and-only-one info idea as it's not strictly related to what we do with the APIs on TypeInfo.

True - shall we agree to leave the Declared(name) methods on the "no" list as far as this issue goes?

The uber-GetMethod is a much more ambitious effort (and much harder to justify on a cost-benefit basis - Reflection has been demented with regard to custom modifiers since its inception and it doesn't seem to have mattered much in the real world.) That can wait for another day.

I made my case for the "Declared(name)" case at dotnet/corefx#23854.

Closing due to lengthy inaction.

Was this page helpful?
0 / 5 - 0 ratings