Проверка соответствия объекта общему ограничению параметров

У меня есть интерфейс, аналогичный интерфейсу ниже:

public interface IInterface<T>
    where T : IInterface<T>
{
}

И теперь мне нужно создать тип, представляющий этот интерфейс, используя отражение, например.

typeof(IInterface<>).MakeGenericType(someType);

Однако, я действительно не знаю, какой тип "someType" будет отображаться до выполнения, и возможно, что тип не будет действительным в качестве аргумента типа для общего интерфейса, поэтому MakeGenericType завершится с ошибкой.

Вопрос в том, как я могу проверить, что 'someType' действителен для общего ограничения?

Ответ 1

Чтобы быть честным, самым простым подходом было бы просто вызвать MakeGenericType и поймать ArgumentException, который будет вызываться, если какой-либо аргумент типа неверен (или если у вас неправильное количество параметров типа).

Пока вы можете использовать Type.GetGenericParameterConstraints, чтобы найти ограничения, а затем выработать то, что каждый из них означает, это будет уродливый и подверженный ошибкам код.

Мне обычно не нравится предлагать "просто попробовать и поймать", но в этом случае я думаю, что это будет самый надежный подход. В противном случае вы просто переопределяете проверки, которые CLR будет выполнять в любом случае - и каковы шансы, что вы полностью их реализуете?:)

Ответ 2

Это возможно. Учитывая ограничение, вы используете Type.GenericParameterAttributes и маски

GenericParameterAttributes.ReferenceTypeConstraint
GenericParameterAttributes.NotNullableValueTypeConstraint
GenericParameterAttributes.DefaultConstructorConstraint

чтобы проверить наличие ограничений class, struct или new(). Вы можете легко проверить, удовлетворяет ли данный тип этим ограничениям (первый из них легко реализовать (используйте Type.IsClass), второй немного сложный, но вы можете сделать это с помощью рефлексии, а у третьего есть немного информации о том, что ваше тестирование устройства будет detect (Type.GetConstructor(new Type[0]) не возвращает конструктор по умолчанию для типов значений, но вы знаете, что у них есть конструктор по умолчанию).

После этого вы используете Type.GetGenericParameterConstraints, чтобы получить ограничения иерархии типов (where T : Base, IInterface подобные ограничения) и пропустить их, чтобы проверить, удовлетворяет ли данный тип им.

Ответ 3

В поисках чего-то подобного, я нашел эту статью Скоттом Хансеном. Прочитав его (коротко) и уже думая по линиям метода расширения от ответа @Jon Skeet, я бросил этот маленький лакомый кусочек и дал ему быстрый ход:

public static class Extensions
{
    public static bool IsImplementationOf(this System.Type objectType, System.Type interfaceType)
    {
        return (objectType.GetInterface(interfaceType.FullName) != null);
    }
}

Он действительно работал для нескольких тестов, на которые я его положил. Он вернул true, когда я использовал его для типа, который DID реализует интерфейс, который я ему передал, и он не прошел, когда я передал ему тип, который не реализовал интерфейс. Я даже удалил объявление интерфейса из успешного типа и попробовал его снова, и он не удался. Я использовал его так:

if (myType.IsImplementationOf(typeof(IFormWithWorker)))
{
    //Do Something
    MessageBox.Show(myType.GetInterface(typeof(DocumentDistributor.Library.IFormWithWorker).FullName).FullName);
}
else
{
    MessageBox.Show("It IS null");
}

Я, возможно, поиграю с ним, но я могу закончить его размещение: Каковы ваши любимые методы расширения для С#? (Codeplex.com/extensionoverflow)

Ответ 4

Здесь моя реализация 3 методов расширения:

  • bool CanMakeGenericTypeVia(this Type openConstructedType, Type closedConstructedType)
  • Type MakeGenericTypeVia(this Type openConstructedType, Type closedConstructedType)
  • MethodInfo MakeGenericMethodVia(this MethodInfo openConstructedMethod, params Type[] closedConstructedParameterTypes)

Первый позволяет проверить, соответствует ли тип закрытого типа определению открытого типа. Если это так, то второй может вывести все необходимые аргументы типа, чтобы вернуть замкнутое построение из заданного замкнутого типа. Наконец, третий метод может разрешить все это автоматически для методов.

Обратите внимание, что эти методы не сбой или возврат false, если вы передадите другой открытый тип в качестве аргумента типа "закрытый построенный", если этот второй тип учитывает все ограничения типа исходного открытого типа. Вместо этого они будут разрешать как можно больше информации о типе от данных типов. Поэтому, если вы хотите убедиться, что разрешение дало полностью закрытый тип, вы должны проверить, что результат ContainsGenericParameters возвращает false. Это соответствует поведению .NET MakeGenericType или MakeGenericMethod.

Также обратите внимание, что я не очень хорошо информирован о совместной и контравариантности, поэтому эти реализации могут быть неверными в этом отношении.

Пример использования:

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)

Реализация:

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 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);
}