| File: System\Reflection\TypeLoading\General\Assignability.cs | Web Access |
| Project: src\runtime\src\libraries\System.Reflection.MetadataLoadContext\src\System.Reflection.MetadataLoadContext.csproj (System.Reflection.MetadataLoadContext) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; using System.Reflection.Runtime.TypeInfos; namespace System.Reflection.TypeLoading { internal static class Assignability { public static bool IsAssignableFrom(Type toTypeInfo, Type fromTypeInfo, CoreTypes coreTypes) { if (toTypeInfo == null) throw new NullReferenceException(); if (fromTypeInfo == null) return false; // It would be more appropriate to throw ArgumentNullException here, but returning "false" is the desktop-compat behavior. if (fromTypeInfo.Equals(toTypeInfo)) return true; if (toTypeInfo.IsGenericTypeDefinition) { // Asking whether something can cast to a generic type definition is arguably meaningless. The .NET Framework CLR Reflection layer converts all // generic type definitions to generic type instantiations closed over the formal generic type parameters. The .NET Native framework // keeps the two separate. Fortunately, under either interpretation, returning "false" unless the two types are identical is still a // defensible behavior. To avoid having the rest of the code deal with the differing interpretations, we'll short-circuit this now. return false; } if (fromTypeInfo.IsGenericTypeDefinition) { // The .NET Framework CLR Reflection layer converts all generic type definitions to generic type instantiations closed over the formal // generic type parameters. The .NET Native framework keeps the two separate. For the purpose of IsAssignableFrom(), // it makes sense to unify the two for the sake of backward compat. We'll just make the transform here so that the rest of code // doesn't need to know about this quirk. fromTypeInfo = fromTypeInfo.GetGenericTypeDefinition().MakeGenericType(fromTypeInfo.GetGenericTypeParameters()); } if (fromTypeInfo.CanCastTo(toTypeInfo, coreTypes)) return true; // .NET Framework compat: IsAssignableFrom() considers T as assignable to Nullable<T> (but does not check if T is a generic parameter.) if (!fromTypeInfo.IsGenericParameter) { if (toTypeInfo.IsConstructedGenericType && toTypeInfo.GetGenericTypeDefinition() == coreTypes[CoreType.NullableT]) { Type nullableUnderlyingType = toTypeInfo.GenericTypeArguments[0]; if (nullableUnderlyingType.Equals(fromTypeInfo)) return true; } } return false; } private static bool CanCastTo(this Type fromTypeInfo, Type toTypeInfo, CoreTypes coreTypes) { if (fromTypeInfo.Equals(toTypeInfo)) return true; if (fromTypeInfo.IsArray) { if (toTypeInfo.IsInterface) return fromTypeInfo.CanCastArrayToInterface(toTypeInfo, coreTypes); if (fromTypeInfo.IsSubclassOf(toTypeInfo)) return true; // T[] is castable to Array or Object. if (!toTypeInfo.IsArray) return false; int rank = fromTypeInfo.GetArrayRank(); if (rank != toTypeInfo.GetArrayRank()) return false; bool fromTypeIsSzArray = fromTypeInfo.IsSZArray; bool toTypeIsSzArray = toTypeInfo.IsSZArray; if (fromTypeIsSzArray != toTypeIsSzArray) { // T[] is assignable to T[*] but not vice-versa. if (!(rank == 1 && !toTypeIsSzArray)) { return false; // T[*] is not castable to T[] } } Type toElementTypeInfo = toTypeInfo.GetElementType()!; Type fromElementTypeInfo = fromTypeInfo.GetElementType()!; return fromElementTypeInfo.IsElementTypeCompatibleWith(toElementTypeInfo, coreTypes); } if (fromTypeInfo.IsByRef) { if (!toTypeInfo.IsByRef) return false; Type toElementTypeInfo = toTypeInfo.GetElementType()!; Type fromElementTypeInfo = fromTypeInfo.GetElementType()!; return fromElementTypeInfo.IsElementTypeCompatibleWith(toElementTypeInfo, coreTypes); } if (fromTypeInfo.IsPointer) { if (toTypeInfo.Equals(coreTypes[CoreType.Object])) return true; // T* is castable to Object. if (toTypeInfo.Equals(coreTypes[CoreType.UIntPtr])) return true; // T* is castable to UIntPtr (but not IntPtr) if (!toTypeInfo.IsPointer) return false; Type toElementTypeInfo = toTypeInfo.GetElementType()!; Type fromElementTypeInfo = fromTypeInfo.GetElementType()!; return fromElementTypeInfo.IsElementTypeCompatibleWith(toElementTypeInfo, coreTypes); } if (fromTypeInfo.IsGenericParameter) { // // A generic parameter can be cast to any of its constraints, or object, if none are specified, or ValueType if the "struct" constraint is // specified. // // This has to be coded as its own case as TypeInfo.BaseType on a generic parameter doesn't always return what you'd expect. // if (toTypeInfo.Equals(coreTypes[CoreType.Object])) return true; if (toTypeInfo.Equals(coreTypes[CoreType.ValueType])) { GenericParameterAttributes attributes = fromTypeInfo.GenericParameterAttributes; if ((attributes & GenericParameterAttributes.NotNullableValueTypeConstraint) != 0) return true; } foreach (Type constraintType in fromTypeInfo.GetGenericParameterConstraints()) { if (constraintType.CanCastTo(toTypeInfo, coreTypes)) return true; } return false; } if (toTypeInfo.IsArray || toTypeInfo.IsByRef || toTypeInfo.IsPointer || toTypeInfo.IsGenericParameter) return false; if (fromTypeInfo.MatchesWithVariance(toTypeInfo, coreTypes)) return true; if (toTypeInfo.IsInterface) { foreach (Type ifc in fromTypeInfo.GetInterfaces()) { if (ifc.MatchesWithVariance(toTypeInfo, coreTypes)) return true; } return false; } else { // Interfaces are always castable to System.Object. The code below will not catch this as interfaces report their BaseType as null. if (toTypeInfo.Equals(coreTypes[CoreType.Object]) && fromTypeInfo.IsInterface) return true; Type walk = fromTypeInfo; while (true) { Type? baseType = walk.BaseType; if (baseType == null) return false; walk = baseType; if (walk.MatchesWithVariance(toTypeInfo, coreTypes)) return true; } } } // // Check a base type or implemented interface type for equivalence (taking into account variance for generic instantiations.) // Does not check ancestors recursively. // private static bool MatchesWithVariance(this Type fromTypeInfo, Type toTypeInfo, CoreTypes coreTypes) { Debug.Assert(!(fromTypeInfo.IsArray || fromTypeInfo.IsByRef || fromTypeInfo.IsPointer || fromTypeInfo.IsGenericParameter)); Debug.Assert(!(toTypeInfo.IsArray || toTypeInfo.IsByRef || toTypeInfo.IsPointer || toTypeInfo.IsGenericParameter)); if (fromTypeInfo.Equals(toTypeInfo)) return true; if (!(fromTypeInfo.IsConstructedGenericType && toTypeInfo.IsConstructedGenericType)) return false; Type genericTypeDefinition = fromTypeInfo.GetGenericTypeDefinition(); if (!genericTypeDefinition.Equals(toTypeInfo.GetGenericTypeDefinition())) return false; Type[] fromTypeArguments = fromTypeInfo.GenericTypeArguments; Type[] toTypeArguments = toTypeInfo.GenericTypeArguments; Type[] genericTypeParameters = genericTypeDefinition.GetGenericTypeParameters(); for (int i = 0; i < genericTypeParameters.Length; i++) { Type fromTypeArgumentInfo = fromTypeArguments[i]; Type toTypeArgumentInfo = toTypeArguments[i]; GenericParameterAttributes attributes = genericTypeParameters[i].GenericParameterAttributes; switch (attributes & GenericParameterAttributes.VarianceMask) { case GenericParameterAttributes.Covariant: if (!(fromTypeArgumentInfo.IsGcReferenceTypeAndCastableTo(toTypeArgumentInfo, coreTypes))) return false; break; case GenericParameterAttributes.Contravariant: if (!(toTypeArgumentInfo.IsGcReferenceTypeAndCastableTo(fromTypeArgumentInfo, coreTypes))) return false; break; case GenericParameterAttributes.None: if (!(fromTypeArgumentInfo.Equals(toTypeArgumentInfo))) return false; break; default: throw new BadImageFormatException(); // Unexpected variance value in metadata. } } return true; } // // A[] can cast to B[] if one of the following are true: // // A can cast to B under variance rules. // // A and B are both integers or enums and have the same reduced type (i.e. represent the same-sized integer, ignoring signed/unsigned differences.) // "char" is not interchangeable with short/ushort. "bool" is not interchangeable with byte/sbyte. // // For .NET Framework compat, A& and A* follow the same rules. // private static bool IsElementTypeCompatibleWith(this Type fromTypeInfo, Type toTypeInfo, CoreTypes coreTypes) { if (fromTypeInfo.IsGcReferenceTypeAndCastableTo(toTypeInfo, coreTypes)) return true; Type reducedFromType = fromTypeInfo.ReducedType(coreTypes); Type reducedToType = toTypeInfo.ReducedType(coreTypes); if (reducedFromType.Equals(reducedToType)) return true; return false; } private static Type ReducedType(this Type t, CoreTypes coreTypes) { if (t.IsEnum) t = t.GetEnumUnderlyingType(); if (t.Equals(coreTypes[CoreType.Byte])) return coreTypes[CoreType.SByte] ?? throw new TypeLoadException(SR.Format(SR.CoreTypeNotFound, "System.SByte")); if (t.Equals(coreTypes[CoreType.UInt16])) return coreTypes[CoreType.Int16] ?? throw new TypeLoadException(SR.Format(SR.CoreTypeNotFound, "System.Int16")); if (t.Equals(coreTypes[CoreType.UInt32])) return coreTypes[CoreType.Int32] ?? throw new TypeLoadException(SR.Format(SR.CoreTypeNotFound, "System.Int32")); if (t.Equals(coreTypes[CoreType.UInt64])) return coreTypes[CoreType.Int64] ?? throw new TypeLoadException(SR.Format(SR.CoreTypeNotFound, "System.Int64")); return t; } // // Contra/CoVariance. // // IEnumerable<D> can cast to IEnumerable<B> if D can cast to B and if there's no possibility that D is a value type. // private static bool IsGcReferenceTypeAndCastableTo(this Type fromTypeInfo, Type toTypeInfo, CoreTypes coreTypes) { if (fromTypeInfo.Equals(toTypeInfo)) return true; if (fromTypeInfo.ProvablyAGcReferenceType(coreTypes)) return fromTypeInfo.CanCastTo(toTypeInfo, coreTypes); return false; } // // A true result indicates that a type can never be a value type. This is important when testing variance-compatibility. // private static bool ProvablyAGcReferenceType(this Type t, CoreTypes coreTypes) { if (t.IsGenericParameter) { GenericParameterAttributes attributes = t.GenericParameterAttributes; if ((attributes & GenericParameterAttributes.ReferenceTypeConstraint) != 0) return true; // generic parameter with a "class" constraint. } return t.ProvablyAGcReferenceTypeHelper(coreTypes); } private static bool ProvablyAGcReferenceTypeHelper(this Type t, CoreTypes coreTypes) { if (t.IsArray) return true; if (t.IsByRef || t.IsPointer) return false; if (t.IsGenericParameter) { // We intentionally do not check for a "class" constraint on generic parameter ancestors. // That's because this property does not propagate up the constraining hierarchy. // (e.g. "class A<S, T> where S : T, where T : class" does not guarantee that S is a class.) foreach (Type constraintType in t.GetGenericParameterConstraints()) { if (constraintType.ProvablyAGcReferenceTypeHelper(coreTypes)) return true; } return false; } return t.IsClass && !t.Equals(coreTypes[CoreType.Object]) && !t.Equals(coreTypes[CoreType.ValueType]) && !t.Equals(coreTypes[CoreType.Enum]); } // // T[] casts to IList<T>. This could be handled by the normal ancestor-walking code // but for one complication: T[] also casts to IList<U> if T[] casts to U[]. // private static bool CanCastArrayToInterface(this Type fromTypeInfo, Type toTypeInfo, CoreTypes coreTypes) { Debug.Assert(fromTypeInfo.IsArray); Debug.Assert(toTypeInfo.IsInterface); if (toTypeInfo.IsConstructedGenericType) { Type[] toTypeGenericTypeArguments = toTypeInfo.GenericTypeArguments; if (toTypeGenericTypeArguments.Length != 1) return false; Type toElementTypeInfo = toTypeGenericTypeArguments[0]; Type toTypeGenericTypeDefinition = toTypeInfo.GetGenericTypeDefinition(); Type? fromElementTypeInfo = fromTypeInfo.GetElementType(); foreach (Type ifc in fromTypeInfo.GetInterfaces()) { if (ifc.IsConstructedGenericType) { Type ifcGenericTypeDefinition = ifc.GetGenericTypeDefinition(); if (ifcGenericTypeDefinition.Equals(toTypeGenericTypeDefinition)) { if (fromElementTypeInfo!.IsElementTypeCompatibleWith(toElementTypeInfo, coreTypes)) return true; } } } return false; } else { foreach (Type ifc in fromTypeInfo.GetInterfaces()) { if (ifc.Equals(toTypeInfo)) return true; } return false; } } } }