Checking if an object meets a Generic Parameter constraint
I have an interface similar to the one below:
public interface IInterface<T>
where T : IInterface<T>
{
}
And now I need to create a type representing this interface using reflection,开发者_开发百科 e.g.
typeof(IInterface<>).MakeGenericType(someType);
However, I don't actually know what type 'someType' will be until runtime, and it's possible that the type won't be valid as a type argument for the generic interface, so MakeGenericType fails.
The question is, how can I check that 'someType' is valid for the generic constraint?
To be honest, the simplest approach would be to just call MakeGenericType
and catch the ArgumentException
that will be thrown if any type argument is wrong (or if you've got the wrong number of type parameters).
While you could use Type.GetGenericParameterConstraints
to find the constraints and then work out what each of them means, it's going to be ugly and bug-prone code.
I don't usually like suggesting "just try it and catch" but in this case I think it's going to be the most reliable approach. Otherwise you're just reimplementing the checks that the CLR is going to perform anyway - and what are the chances you'll reimplement them perfectly? :)
This is possible. Given a constraint, you use Type.GenericParameterAttributes
and the masks
GenericParameterAttributes.ReferenceTypeConstraint
GenericParameterAttributes.NotNullableValueTypeConstraint
GenericParameterAttributes.DefaultConstructorConstraint
to check for the presence of class
, struct
or new()
constraints. You can easily check if a given type satisfies these constraints (the first is easy to implement (use Type.IsClass
), the second is slightly tricky but you can do it using reflection, and the third has a little gotcha that your unit testing will detect (Type.GetConstructor(new Type[0])
doesn't return the default constructor for value types but you know those have a default constructor anyway).
After this, you use Type.GetGenericParameterConstraints
to get the type hierarchy constraints (the where T : Base, IInterface
like constraints) and run through them to check that the given type satisfies them.
Here's my implementation of 3 extension methods:
bool CanMakeGenericTypeVia(this Type openConstructedType, Type closedConstructedType)
Type MakeGenericTypeVia(this Type openConstructedType, Type closedConstructedType)
MethodInfo MakeGenericMethodVia(this MethodInfo openConstructedMethod, params Type[] closedConstructedParameterTypes)
The first allows you to check if a closed-constructed type matches an open-constructed type definition. If so, the second can infer all the required type arguments to return a closed-constructed from a given closed-constructed type. Finally, the third method can resolve all this automatically for methods.
Note that these methods will not fail or return false if you pass another open-constructed type as the "closed-constructed" type argument, as long as this second type respects all the type constraints of the initial open-constructed type. They will instead resolve as much type information as possible from the given types. Therefore, if you want to make sure the resolution gave a fully closed-constructed type, you should check that the result's ContainsGenericParameters
returns false. This matches the behaviour of .NET's MakeGenericType
or MakeGenericMethod
.
Also note that I'm not very well informed on co- and contravariance, so these implementations might not be correct in that regard.
Example usage:
public static void GenericMethod<T0, T1>(T0 direct, IEnumerable<T1> generic)
where T0 : struct
where T1 : class, new(), IInterface
{ }
public interface IInterface { }
public class CandidateA : IInterface { private CandidateA(); }
public struct CandidateB : IInterface { }
public class CandidateC { public CandidateC(); }
public class CandidateD : IInterface { public CandidateD(); }
var method = GetMethod("GenericMethod");
var type0 = method.GetParameters()[0].ParameterType;
var type1 = method.GetParameters()[1].ParameterType;
// Results:
type0.CanMakeGenericTypeVia(typeof(int)) // true
type0.CanMakeGenericTypeVia(typeof(IList)) // false, fails struct
type1.CanMakeGenericTypeVia(typeof(IEnumerable<CandidateA>))
// false, fails new()
type1.CanMakeGenericTypeVia(typeof(IEnumerable<CandidateB>))
// false, fails class
type1.CanMakeGenericTypeVia(typeof(IEnumerable<CandidateC>))
// false, fails : IInterface
type1.CanMakeGenericTypeVia(typeof(IEnumerable<CandidateD>))
// true
type0.MakeGenericTypeVia(typeof(int))
// typeof(int)
type1.MakeGenericTypeVia(typeof(List<CandidateD>))
// IEnumerable<CandidateD>
method.MakeGenericMethodVia(123.GetType(), (new CandidateD[0]).GetType())
// GenericMethod(int, IEnumerable<CandidateD>)
method.MakeGenericMethodVia(123.GetType(), type1)
// GenericMethod<T1>(int, IEnumerable<T1>)
// (partial resolution)
Implementation:
public static bool CanMakeGenericTypeVia(this Type openConstructedType, Type closedConstructedType)
{
if (openConstructedType == null)
{
throw new ArgumentNullException("openConstructedType");
}
if (closedConstructedType == null)
{
throw new ArgumentNullException("closedConstructedType");
}
if (openConstructedType.IsGenericParameter) // e.g.: T
{
// The open-constructed type is a generic parameter.
// First, check if all special attribute constraints are respected.
var constraintAttributes = openConstructedType.GenericParameterAttributes;
if (constraintAttributes != GenericParameterAttributes.None)
{
// e.g.: where T : struct
if (constraintAttributes.HasFlag(GenericParameterAttributes.NotNullableValueTypeConstraint) &&
!closedConstructedType.IsValueType)
{
return false;
}
// e.g.: where T : class
if (constraintAttributes.HasFlag(GenericParameterAttributes.ReferenceTypeConstraint) &&
closedConstructedType.IsValueType)
{
return false;
}
// e.g.: where T : new()
if (constraintAttributes.HasFlag(GenericParameterAttributes.DefaultConstructorConstraint) &&
closedConstructedType.GetConstructor(Type.EmptyTypes) == null)
{
return false;
}
// TODO: Covariance and contravariance?
}
// Then, check if all type constraints are respected.
// e.g.: where T : BaseType, IInterface1, IInterface2
foreach (var constraint in openConstructedType.GetGenericParameterConstraints())
{
if (!constraint.IsAssignableFrom(closedConstructedType))
{
return false;
}
}
return true;
}
else if (openConstructedType.ContainsGenericParameters)
{
// The open-constructed type is not a generic parameter but contains generic parameters.
// It could be either a generic type or an array.
if (openConstructedType.IsGenericType) // e.g. Generic<T1, int, T2>
{
// The open-constructed type is a generic type.
var openConstructedGenericDefinition = openConstructedType.GetGenericTypeDefinition(); // e.g.: Generic<,,>
var openConstructedGenericArguments = openConstructedType.GetGenericArguments(); // e.g.: { T1, int, T2 }
// Check a list of possible candidate closed-constructed types:
// - the closed-constructed type itself
// - its base type, if any (i.e.: if the closed-constructed type is not object)
// - its implemented interfaces
var inheritedClosedConstructedTypes = new List<Type>();
inheritedClosedConstructedTypes.Add(closedConstructedType);
if (closedConstructedType.BaseType != null)
{
inheritedClosedConstructedTypes.Add(closedConstructedType.BaseType);
}
inheritedClosedConstructedTypes.AddRange(closedConstructedType.GetInterfaces());
foreach (var inheritedClosedConstructedType in inheritedClosedConstructedTypes)
{
if (inheritedClosedConstructedType.IsGenericType &&
inheritedClosedConstructedType.GetGenericTypeDefinition() == openConstructedGenericDefinition)
{
// The inherited closed-constructed type and the open-constructed type share the same generic definition.
var inheritedClosedConstructedGenericArguments = inheritedClosedConstructedType.GetGenericArguments(); // e.g.: { float, int, string }
// For each open-constructed generic argument, recursively check if it
// can be made into a closed-constructed type via the closed-constructed generic argument.
for (int i = 0; i < openConstructedGenericArguments.Length; i++)
{
if (!openConstructedGenericArguments[i].CanMakeGenericTypeVia(inheritedClosedConstructedGenericArguments[i])) // !T1.IsAssignableFromGeneric(float)
{
return false;
}
}
// The inherited closed-constructed type matches the generic definition of
// the open-constructed type and each of its type arguments are assignable to each equivalent type
// argument of the constraint.
return true;
}
}
// The open-constructed type contains generic parameters, but no
// inherited closed-constructed type has a matching generic definition.
return false;
}
else if (openConstructedType.IsArray) // e.g. T[]
{
// The open-constructed type is an array.
if (!closedConstructedType.IsArray ||
closedConstructedType.GetArrayRank() != openConstructedType.GetArrayRank())
{
// Fail if the closed-constructed type isn't an array of the same rank.
return false;
}
var openConstructedElementType = openConstructedType.GetElementType();
var closedConstructedElementType = closedConstructedType.GetElementType();
return openConstructedElementType.CanMakeGenericTypeVia(closedConstructedElementType);
}
else
{
// I don't believe this can ever happen.
throw new NotImplementedException("Open-constructed type contains generic parameters, but is neither an array nor a generic type.");
}
}
else
{
// The open-constructed type does not contain generic parameters,
// we can proceed to a regular closed-type check.
return openConstructedType.IsAssignableFrom(closedConstructedType);
}
}
public static Type MakeGenericTypeVia(this Type openConstructedType, Type closedConstructedType, Dictionary<Type, Type> resolvedGenericParameters, bool safe = true)
{
if (openConstructedType == null)
{
throw new ArgumentNullException("openConstructedType");
}
if (closedConstructedType == null)
{
throw new ArgumentNullException("closedConstructedType");
}
if (resolvedGenericParameters == null)
{
throw new ArgumentNullException("resolvedGenericParameters");
}
if (safe && !openConstructedType.CanMakeGenericTypeVia(closedConstructedType))
{
throw new InvalidOperationException("Open-constructed type is not assignable from closed-constructed type.");
}
if (openConstructedType.IsGenericParameter) // e.g.: T
{
// The open-constructed type is a generic parameter.
// We can directly map it to the closed-constructed type.
// Because this is the lowest possible level of type resolution,
// we will add this entry to our list of resolved generic parameters
// in case we need it later (e.g. for resolving generic methods).
// Note that we allow an open-constructed type to "make" another
// open-constructed type, as long as the former respects all of
// the latter's constraints. Therefore, we will only add the resolved
// parameter to our dictionary if it actually is resolved.
if (!closedConstructedType.ContainsGenericParameters)
{
if (resolvedGenericParameters.ContainsKey(openConstructedType))
{
if (resolvedGenericParameters[openConstructedType] != closedConstructedType)
{
throw new InvalidOperationException("Nested generic parameters resolve to different values.");
}
}
else
{
resolvedGenericParameters.Add(openConstructedType, closedConstructedType);
}
}
return closedConstructedType;
}
else if (openConstructedType.ContainsGenericParameters) // e.g.: Generic<T1, int, T2>
{
// The open-constructed type is not a generic parameter but contains generic parameters.
// It could be either a generic type or an array.
if (openConstructedType.IsGenericType) // e.g. Generic<T1, int, T2>
{
// The open-constructed type is a generic type.
var openConstructedGenericDefinition = openConstructedType.GetGenericTypeDefinition(); // e.g.: Generic<,,>
var openConstructedGenericArguments = openConstructedType.GetGenericArguments(); // e.g.: { T1, int, T2 }
// Check a list of possible candidate closed-constructed types:
// - the closed-constructed type itself
// - its base type, if any (i.e.: if the closed-constructed type is not object)
// - its implemented interfaces
var inheritedCloseConstructedTypes = new List<Type>();
inheritedCloseConstructedTypes.Add(closedConstructedType);
if (closedConstructedType.BaseType != null)
{
inheritedCloseConstructedTypes.Add(closedConstructedType.BaseType);
}
inheritedCloseConstructedTypes.AddRange(closedConstructedType.GetInterfaces());
foreach (var inheritedCloseConstructedType in inheritedCloseConstructedTypes)
{
if (inheritedCloseConstructedType.IsGenericType &&
inheritedCloseConstructedType.GetGenericTypeDefinition() == openConstructedGenericDefinition)
{
// The inherited closed-constructed type and the open-constructed type share the same generic definition.
var inheritedClosedConstructedGenericArguments = inheritedCloseConstructedType.GetGenericArguments(); // e.g.: { float, int, string }
// For each inherited open-constructed type generic argument, recursively resolve it
// via the equivalent closed-constructed type generic argument.
var closedConstructedGenericArguments = new Type[openConstructedGenericArguments.Length];
for (int j = 0; j < openConstructedGenericArguments.Length; j++)
{
closedConstructedGenericArguments[j] = MakeGenericTypeVia
(
openConstructedGenericArguments[j],
inheritedClosedConstructedGenericArguments[j],
resolvedGenericParameters,
safe: false // We recursively checked before, no need to do it again
);
// e.g.: Resolve(T1, float)
}
// Construct the final closed-constructed type from the resolved arguments
return openConstructedGenericDefinition.MakeGenericType(closedConstructedGenericArguments);
}
}
// The open-constructed type contains generic parameters, but no
// inherited closed-constructed type has a matching generic definition.
// This cannot happen in safe mode, but could in unsafe mode.
throw new InvalidOperationException("Open-constructed type is not assignable from closed-constructed type.");
}
else if (openConstructedType.IsArray) // e.g. T[]
{
var arrayRank = openConstructedType.GetArrayRank();
// The open-constructed type is an array.
if (!closedConstructedType.IsArray ||
closedConstructedType.GetArrayRank() != arrayRank)
{
// Fail if the closed-constructed type isn't an array of the same rank.
// This cannot happen in safe mode, but could in unsafe mode.
throw new InvalidOperationException("Open-constructed type is not assignable from closed-constructed type.");
}
var openConstructedElementType = openConstructedType.GetElementType();
var closedConstructedElementType = closedConstructedType.GetElementType();
return openConstructedElementType.MakeGenericTypeVia
(
closedConstructedElementType,
resolvedGenericParameters,
safe: false
).MakeArrayType(arrayRank);
}
else
{
// I don't believe this can ever happen.
throw new NotImplementedException("Open-constructed type contains generic parameters, but is neither an array nor a generic type.");
}
}
else
{
// The open-constructed type does not contain generic parameters,
// it is by definition already resolved.
return openConstructedType;
}
}
public static MethodInfo MakeGenericMethodVia(this MethodInfo openConstructedMethod, params Type[] closedConstructedParameterTypes)
{
if (openConstructedMethod == null)
{
throw new ArgumentNullException("openConstructedMethod");
}
if (closedConstructedParameterTypes == null)
{
throw new ArgumentNullException("closedConstructedParameterTypes");
}
if (!openConstructedMethod.ContainsGenericParameters)
{
// The method contains no generic parameters,
// it is by definition already resolved.
return openConstructedMethod;
}
var openConstructedParameterTypes = openConstructedMethod.GetParameters().Select(p => p.ParameterType).ToArray();
if (openConstructedParameterTypes.Length != closedConstructedParameterTypes.Length)
{
throw new ArgumentOutOfRangeException("closedConstructedParameterTypes");
}
var resolvedGenericParameters = new Dictionary<Type, Type>();
for (int i = 0; i < openConstructedParameterTypes.Length; i++)
{
// Resolve each open-constructed parameter type via the equivalent
// closed-constructed parameter type.
var openConstructedParameterType = openConstructedParameterTypes[i];
var closedConstructedParameterType = closedConstructedParameterTypes[i];
openConstructedParameterType.MakeGenericTypeVia(closedConstructedParameterType, resolvedGenericParameters);
}
// Construct the final closed-constructed method from the resolved arguments
var openConstructedGenericArguments = openConstructedMethod.GetGenericArguments();
var closedConstructedGenericArguments = openConstructedGenericArguments.Select(openConstructedGenericArgument =>
{
// If the generic argument has been successfully resolved, use it;
// otherwise, leave the open-constructe argument in place.
if (resolvedGenericParameters.ContainsKey(openConstructedGenericArgument))
{
return resolvedGenericParameters[openConstructedGenericArgument];
}
else
{
return openConstructedGenericArgument;
}
}).ToArray();
return openConstructedMethod.MakeGenericMethod(closedConstructedGenericArguments);
}
Looking a little bit online for something like this, I found this article by Scott Hanselman. After reading it (it's short), and already thinking along the lines of the extension method from @Jon Skeet's answer, I threw this little tidbit together and gave it a quick run:
public static class Extensions
{
public static bool IsImplementationOf(this System.Type objectType, System.Type interfaceType)
{
return (objectType.GetInterface(interfaceType.FullName) != null);
}
}
It actually worked for the few tests that I put it to. It returned true when I used it on a type that DID implement an interface I passed it, and it failed when I passed it a type that didn't implement the interface. I even removed the interface declaration from the successful type and tried it again and it failed. I used it like this:
if (myType.IsImplementationOf(typeof(IFormWithWorker)))
{
//Do Something
MessageBox.Show(myType.GetInterface(typeof(DocumentDistributor.Library.IFormWithWorker).FullName).FullName);
}
else
{
MessageBox.Show("It IS null");
}
I'll probably play around with it but I may end up posting it to: What are your favorite extension methods for C#? (codeplex.com/extensionoverflow)
精彩评论