web-dev-qa-db-fra.com

Comment utiliser l'interface comme contrainte de type générique C #?

Est-il possible d'obtenir la déclaration de fonction suivante?

public bool Foo<T>() where T : interface;

c'est à dire. où T est un type d’interface (semblable à where T : class et struct).

Actuellement, je me suis installé pour:

public bool Foo<T>() where T : IBase;

Où IBase est défini comme une interface vide héritée de toutes mes interfaces personnalisées ... Pas idéal, mais cela devrait fonctionner ... Pourquoi ne pouvez-vous pas définir qu'un type générique doit être une interface?

Pour ce que ça vaut, je le veux parce que Foo fait une réflexion là où il a besoin d’un type d’interface ... Je pourrais le passer comme paramètre normal et faire la vérification nécessaire dans la fonction elle-même, beaucoup plus de typesafe (et je suppose un peu plus performant, puisque toutes les vérifications sont effectuées à compiletime).

153
Matthew Scharley

Le plus proche que vous puissiez faire (sauf pour votre approche d’interface de base) est "where T : class ", signifiant type de référence. Il n’existe pas de syntaxe pour signifier" toute interface ".

Cette ("where T : class ") est utilisé, par exemple, dans WCF pour limiter les clients aux contrats de service (interfaces).

120
Marc Gravell

Je sais que c'est un peu tard, mais pour ceux qui sont intéressés, vous pouvez utiliser une vérification d'exécution.

typeof(T).IsInterface
104
Robert

Non, en fait, si vous pensez class et struct signifie classes et structs, vous vous trompez. class signifie tout type de référence (inclut également les interfaces) et struct signifie tout type de valeur (par exemple, struct, enum).

25
Mehrdad Afshari

Pour faire suite à la réponse de Robert, c'est encore plus tard, mais vous pouvez utiliser une classe d'assistance statique pour effectuer la vérification à l'exécution une fois par type uniquement:

public bool Foo<T>() where T : class
{
    FooHelper<T>.Foo();
}

private static class FooHelper<TInterface> where TInterface : class
{
    static FooHelper()
    {
        if (!typeof(TInterface).IsInterface)
            throw // ... some exception
    }
    public static void Foo() { /*...*/ }
}

Je remarque également que votre solution "devrait fonctionner" ne fonctionne pas, en fait. Considérer:

public bool Foo<T>() where T : IBase;
public interface IBase { }
public interface IActual : IBase { string S { get; } }
public class Actual : IActual { public string S { get; set; } }

Maintenant, rien ne vous empêche d'appeler Foo ainsi:

Foo<Actual>();

Après tout, la classe Actual satisfait à la contrainte IBase.

19
phoog

Depuis quelque temps, je réfléchis aux contraintes de temps de compilation, c’est donc l’occasion idéale pour lancer le concept.

L'idée de base est que si vous ne pouvez pas effectuer de vérification d'une heure de compilation, vous devez le faire le plus tôt possible, c'est-à-dire au moment où l'application démarre. Si toutes les vérifications sont correctes, l'application s'exécutera. Si une vérification échoue, l'application échouera instantanément.

comportement

Le meilleur résultat possible est que notre programme ne compile pas si les contraintes ne sont pas respectées. Malheureusement, ce n'est pas possible dans l'implémentation actuelle de C #.

La meilleure chose à faire est que le programme se bloque au moment où il est lancé.

La dernière option est que le programme va planter au moment où le code est frappé. C'est le comportement par défaut de .NET. Pour moi, c'est complètement inacceptable.

Prérequis

Nous avons besoin d'un mécanisme de contrainte, donc pour l'absence de mieux, utilisons un attribut. L'attribut sera présent au-dessus d'une contrainte générique pour vérifier s'il correspond à nos conditions. Si ce n'est pas le cas, nous commettons une erreur laide.

Cela nous permet de faire des choses comme ça dans notre code:

public class Clas<[IsInterface] T> where T : class

(J'ai gardé le where T:class ici, car je préfère toujours les contrôles au moment de la compilation aux contrôles au moment de l'exécution)

Donc, cela ne nous laisse que 1 problème, qui consiste à vérifier si tous les types que nous utilisons correspondent à la contrainte. À quel point cela peut-il être dur?

Brisons-le

Les types génériques sont toujours soit sur une classe (/ struct/interface), soit sur une méthode.

Pour déclencher une contrainte, vous devez effectuer l'une des opérations suivantes:

  1. Au moment de la compilation, lors de l'utilisation d'un type dans un type (héritage, contrainte générique, membre de classe)
  2. Compile-time, lors de l'utilisation d'un type dans un corps de méthode
  3. Au moment de l'exécution, lors de l'utilisation de la réflexion pour construire quelque chose basé sur la classe de base générique.
  4. À l'exécution, lors de l'utilisation de la réflexion pour construire quelque chose basé sur RTTI.

À ce stade, je voudrais dire que vous devriez toujours éviter de faire (4) dans n’importe quel programme OMI. Quoi qu’il en soit, ces vérifications ne le prendront pas en charge, car cela signifierait effectivement résoudre le problème de blocage.

Cas 1: en utilisant un type

Exemple:

public class TestClass : SomeClass<IMyInterface> { ... } 

Exemple 2:

public class TestClass 
{ 
    SomeClass<IMyInterface> myMember; // or a property, method, etc.
} 

En gros, cela implique d'analyser tous les types, héritage, membres, paramètres, etc., etc. Si un type est un type générique et qu'il a une contrainte, nous vérifions la contrainte; s'il s'agit d'un tableau, nous vérifions le type d'élément.

À ce stade, je dois ajouter que cela résoudra le fait que, par défaut, .NET charge les types "paresseux". En analysant tous les types, nous forçons le runtime .NET à tous les charger. Pour la plupart des programmes, cela ne devrait pas être un problème. Néanmoins, si vous utilisez des initialiseurs statiques dans votre code, vous pourriez rencontrer des problèmes avec cette approche ... Cela dit, je ne conseillerais à personne de le faire de toute façon (à l'exception de choses comme celle-ci :-), cela ne devrait donc pas donner vous avez beaucoup de problèmes.

Cas 2: utilisation d'un type dans une méthode

Exemple:

void Test() {
    new SomeClass<ISomeInterface>();
}

Pour vérifier cela, nous n'avons qu'une seule option: décompiler la classe, vérifier tous les jetons membres utilisés et si l'un d'entre eux est le type générique, vérifier les arguments.

cas 3: réflexion, construction générique d'exécution

Exemple:

typeof(CtorTest<>).MakeGenericType(typeof(IMyInterface))

Je suppose qu'il est théoriquement possible de vérifier cela avec des astuces similaires à celles de case (2), mais son implémentation est beaucoup plus difficile (vous devez vérifier si MakeGenericType est appelé dans un chemin de code). Je ne vais pas entrer dans les détails ici ...

Cas 4: Reflection, RTTI d'exécution

Exemple:

Type t = Type.GetType("CtorTest`1[IMyInterface]");

C’est le pire des cas et, comme je l’ai déjà expliqué, une mauvaise idée à mon humble avis. Quoi qu'il en soit, il n'y a aucun moyen pratique de résoudre ce problème en utilisant des chèques.

Test du lot

Créer un programme qui teste les cas (1) et (2) donnera comme résultat:

[AttributeUsage(AttributeTargets.GenericParameter)]
public class IsInterface : ConstraintAttribute
{
    public override bool Check(Type genericType)
    {
        return genericType.IsInterface;
    }

    public override string ToString()
    {
        return "Generic type is not an interface";
    }
}

public abstract class ConstraintAttribute : Attribute
{
    public ConstraintAttribute() {}

    public abstract bool Check(Type generic);
}

internal class BigEndianByteReader
{
    public BigEndianByteReader(byte[] data)
    {
        this.data = data;
        this.position = 0;
    }

    private byte[] data;
    private int position;

    public int Position
    {
        get { return position; }
    }

    public bool Eof
    {
        get { return position >= data.Length; }
    }

    public sbyte ReadSByte()
    {
        return (sbyte)data[position++];
    }

    public byte ReadByte()
    {
        return (byte)data[position++];
    }

    public int ReadInt16()
    {
        return ((data[position++] | (data[position++] << 8)));
    }

    public ushort ReadUInt16()
    {
        return (ushort)((data[position++] | (data[position++] << 8)));
    }

    public int ReadInt32()
    {
        return (((data[position++] | (data[position++] << 8)) | (data[position++] << 0x10)) | (data[position++] << 0x18));
    }

    public ulong ReadInt64()
    {
        return (ulong)(((data[position++] | (data[position++] << 8)) | (data[position++] << 0x10)) | (data[position++] << 0x18) | 
                        (data[position++] << 0x20) | (data[position++] << 0x28) | (data[position++] << 0x30) | (data[position++] << 0x38));
    }

    public double ReadDouble()
    {
        var result = BitConverter.ToDouble(data, position);
        position += 8;
        return result;
    }

    public float ReadSingle()
    {
        var result = BitConverter.ToSingle(data, position);
        position += 4;
        return result;
    }
}

internal class ILDecompiler
{
    static ILDecompiler()
    {
        // Initialize our cheat tables
        singleByteOpcodes = new OpCode[0x100];
        multiByteOpcodes = new OpCode[0x100];

        FieldInfo[] infoArray1 = typeof(OpCodes).GetFields();
        for (int num1 = 0; num1 < infoArray1.Length; num1++)
        {
            FieldInfo info1 = infoArray1[num1];
            if (info1.FieldType == typeof(OpCode))
            {
                OpCode code1 = (OpCode)info1.GetValue(null);
                ushort num2 = (ushort)code1.Value;
                if (num2 < 0x100)
                {
                    singleByteOpcodes[(int)num2] = code1;
                }
                else
                {
                    if ((num2 & 0xff00) != 0xfe00)
                    {
                        throw new Exception("Invalid opcode: " + num2.ToString());
                    }
                    multiByteOpcodes[num2 & 0xff] = code1;
                }
            }
        }
    }

    private ILDecompiler() { }

    private static OpCode[] singleByteOpcodes;
    private static OpCode[] multiByteOpcodes;

    public static IEnumerable<ILInstruction> Decompile(MethodBase mi, byte[] ildata)
    {
        Module module = mi.Module;

        BigEndianByteReader reader = new BigEndianByteReader(ildata);
        while (!reader.Eof)
        {
            OpCode code = OpCodes.Nop;

            int offset = reader.Position;
            ushort b = reader.ReadByte();
            if (b != 0xfe)
            {
                code = singleByteOpcodes[b];
            }
            else
            {
                b = reader.ReadByte();
                code = multiByteOpcodes[b];
                b |= (ushort)(0xfe00);
            }

            object operand = null;
            switch (code.OperandType)
            {
                case OperandType.InlineBrTarget:
                    operand = reader.ReadInt32() + reader.Position;
                    break;
                case OperandType.InlineField:
                    if (mi is ConstructorInfo)
                    {
                        operand = module.ResolveField(reader.ReadInt32(), mi.DeclaringType.GetGenericArguments(), Type.EmptyTypes);
                    }
                    else
                    {
                        operand = module.ResolveField(reader.ReadInt32(), mi.DeclaringType.GetGenericArguments(), mi.GetGenericArguments());
                    }
                    break;
                case OperandType.InlineI:
                    operand = reader.ReadInt32();
                    break;
                case OperandType.InlineI8:
                    operand = reader.ReadInt64();
                    break;
                case OperandType.InlineMethod:
                    try
                    {
                        if (mi is ConstructorInfo)
                        {
                            operand = module.ResolveMember(reader.ReadInt32(), mi.DeclaringType.GetGenericArguments(), Type.EmptyTypes);
                        }
                        else
                        {
                            operand = module.ResolveMember(reader.ReadInt32(), mi.DeclaringType.GetGenericArguments(), mi.GetGenericArguments());
                        }
                    }
                    catch
                    {
                        operand = null;
                    }
                    break;
                case OperandType.InlineNone:
                    break;
                case OperandType.InlineR:
                    operand = reader.ReadDouble();
                    break;
                case OperandType.InlineSig:
                    operand = module.ResolveSignature(reader.ReadInt32());
                    break;
                case OperandType.InlineString:
                    operand = module.ResolveString(reader.ReadInt32());
                    break;
                case OperandType.InlineSwitch:
                    int count = reader.ReadInt32();
                    int[] targetOffsets = new int[count];
                    for (int i = 0; i < count; ++i)
                    {
                        targetOffsets[i] = reader.ReadInt32();
                    }
                    int pos = reader.Position;
                    for (int i = 0; i < count; ++i)
                    {
                        targetOffsets[i] += pos;
                    }
                    operand = targetOffsets;
                    break;
                case OperandType.InlineTok:
                case OperandType.InlineType:
                    try
                    {
                        if (mi is ConstructorInfo)
                        {
                            operand = module.ResolveMember(reader.ReadInt32(), mi.DeclaringType.GetGenericArguments(), Type.EmptyTypes);
                        }
                        else
                        {
                            operand = module.ResolveMember(reader.ReadInt32(), mi.DeclaringType.GetGenericArguments(), mi.GetGenericArguments());
                        }
                    }
                    catch
                    {
                        operand = null;
                    }
                    break;
                case OperandType.InlineVar:
                    operand = reader.ReadUInt16();
                    break;
                case OperandType.ShortInlineBrTarget:
                    operand = reader.ReadSByte() + reader.Position;
                    break;
                case OperandType.ShortInlineI:
                    operand = reader.ReadSByte();
                    break;
                case OperandType.ShortInlineR:
                    operand = reader.ReadSingle();
                    break;
                case OperandType.ShortInlineVar:
                    operand = reader.ReadByte();
                    break;

                default:
                    throw new Exception("Unknown instruction operand; cannot continue. Operand type: " + code.OperandType);
            }

            yield return new ILInstruction(offset, code, operand);
        }
    }
}

public class ILInstruction
{
    public ILInstruction(int offset, OpCode code, object operand)
    {
        this.Offset = offset;
        this.Code = code;
        this.Operand = operand;
    }

    public int Offset { get; private set; }
    public OpCode Code { get; private set; }
    public object Operand { get; private set; }
}

public class IncorrectConstraintException : Exception
{
    public IncorrectConstraintException(string msg, params object[] arg) : base(string.Format(msg, arg)) { }
}

public class ConstraintFailedException : Exception
{
    public ConstraintFailedException(string msg) : base(msg) { }
    public ConstraintFailedException(string msg, params object[] arg) : base(string.Format(msg, arg)) { }
}

public class NCTChecks
{
    public NCTChecks(Type startpoint)
        : this(startpoint.Assembly)
    { }

    public NCTChecks(params Assembly[] ass)
    {
        foreach (var Assembly in ass)
        {
            assemblies.Add(Assembly);

            foreach (var type in Assembly.GetTypes())
            {
                EnsureType(type);
            }
        }

        while (typesToCheck.Count > 0)
        {
            var t = typesToCheck.Pop();
            GatherTypesFrom(t);

            PerformRuntimeCheck(t);
        }
    }

    private HashSet<Assembly> assemblies = new HashSet<Assembly>();

    private Stack<Type> typesToCheck = new Stack<Type>();
    private HashSet<Type> typesKnown = new HashSet<Type>();

    private void EnsureType(Type t)
    {
        // Don't check for Assembly here; we can pass f.ex. System.Lazy<Our.T<MyClass>>
        if (t != null && !t.IsGenericTypeDefinition && typesKnown.Add(t))
        {
            typesToCheck.Push(t);

            if (t.IsGenericType)
            {
                foreach (var par in t.GetGenericArguments())
                {
                    EnsureType(par);
                }
            }

            if (t.IsArray)
            {
                EnsureType(t.GetElementType());
            }
        }

    }

    private void PerformRuntimeCheck(Type t)
    {
        if (t.IsGenericType && !t.IsGenericTypeDefinition)
        {
            // Only check the assemblies we explicitly asked for:
            if (this.assemblies.Contains(t.Assembly))
            {
                // Gather the generics data:
                var def = t.GetGenericTypeDefinition();
                var par = def.GetGenericArguments();
                var args = t.GetGenericArguments();

                // Perform checks:
                for (int i = 0; i < args.Length; ++i)
                {
                    foreach (var check in par[i].GetCustomAttributes(typeof(ConstraintAttribute), true).Cast<ConstraintAttribute>())
                    {
                        if (!check.Check(args[i]))
                        {
                            string error = "Runtime type check failed for type " + t.ToString() + ": " + check.ToString();

                            Debugger.Break();
                            throw new ConstraintFailedException(error);
                        }
                    }
                }
            }
        }
    }

    // Phase 1: all types that are referenced in some way
    private void GatherTypesFrom(Type t)
    {
        EnsureType(t.BaseType);

        foreach (var intf in t.GetInterfaces())
        {
            EnsureType(intf);
        }

        foreach (var nested in t.GetNestedTypes())
        {
            EnsureType(nested);
        }

        var all = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance;
        foreach (var field in t.GetFields(all))
        {
            EnsureType(field.FieldType);
        }
        foreach (var property in t.GetProperties(all))
        {
            EnsureType(property.PropertyType);
        }
        foreach (var evt in t.GetEvents(all))
        {
            EnsureType(evt.EventHandlerType);
        }
        foreach (var ctor in t.GetConstructors(all))
        {
            foreach (var par in ctor.GetParameters())
            {
                EnsureType(par.ParameterType);
            }

            // Phase 2: all types that are used in a body
            GatherTypesFrom(ctor);
        }
        foreach (var method in t.GetMethods(all))
        {
            if (method.ReturnType != typeof(void))
            {
                EnsureType(method.ReturnType);
            }

            foreach (var par in method.GetParameters())
            {
                EnsureType(par.ParameterType);
            }

            // Phase 2: all types that are used in a body
            GatherTypesFrom(method);
        }
    }

    private void GatherTypesFrom(MethodBase method)
    {
        if (this.assemblies.Contains(method.DeclaringType.Assembly)) // only consider methods we've build ourselves
        {
            MethodBody methodBody = method.GetMethodBody();
            if (methodBody != null)
            {
                // Handle local variables
                foreach (var local in methodBody.LocalVariables)
                {
                    EnsureType(local.LocalType);
                }

                // Handle method body
                var il = methodBody.GetILAsByteArray();
                if (il != null)
                {
                    foreach (var oper in ILDecompiler.Decompile(method, il))
                    {
                        if (oper.Operand is MemberInfo)
                        {
                            foreach (var type in HandleMember((MemberInfo)oper.Operand))
                            {
                                EnsureType(type);
                            }

                        }
                    }
                }
            }
        }
    }

    private static IEnumerable<Type> HandleMember(MemberInfo info)
    {
        // Event, Field, Method, Constructor or Property.
        yield return info.DeclaringType;
        if (info is EventInfo)
        {
            yield return ((EventInfo)info).EventHandlerType;
        }
        else if (info is FieldInfo)
        {
            yield return ((FieldInfo)info).FieldType;
        }
        else if (info is PropertyInfo)
        {
            yield return ((PropertyInfo)info).PropertyType;
        }
        else if (info is ConstructorInfo)
        {
            foreach (var par in ((ConstructorInfo)info).GetParameters())
            {
                yield return par.ParameterType;
            }
        }
        else if (info is MethodInfo)
        {
            foreach (var par in ((MethodInfo)info).GetParameters())
            {
                yield return par.ParameterType;
            }
        }
        else if (info is Type)
        {
            yield return (Type)info;
        }
        else
        {
            throw new NotSupportedException("Incorrect unsupported member type: " + info.GetType().Name);
        }
    }
}

en utilisant le code

Eh bien, c'est la partie facile :-)

// Create something illegal
public class Bar2 : IMyInterface
{
    public void Execute()
    {
        throw new NotImplementedException();
    }
}

// Our fancy check
public class Foo<[IsInterface] T>
{
}

class Program
{
    static Program()
    {
        // Perform all runtime checks
        new NCTChecks(typeof(Program));
    }

    static void Main(string[] args)
    {
        // Normal operation
        Console.WriteLine("Foo");
        Console.ReadLine();
    }
}
10
atlaste

Vous ne pouvez le faire dans aucune version publiée de C #, ni dans la prochaine version de C # 4.0. Ce n'est pas non plus une limitation en C # - il n'y a pas de contrainte "d'interface" dans le CLR lui-même.

8
Pavel Minaev

Si possible, je suis allé avec une solution comme celle-ci. Cela ne fonctionne que si vous voulez que plusieurs interfaces spécifiques (par exemple, celles auxquelles vous avez accès à la source) soient transmises en tant que paramètre générique, pas n'importe lequel.

  • J'ai laissé mes interfaces, qui ont été mises en cause, hériter d'une interface vide IInterface.
  • J'ai contraint le paramètre générique T à être de IInterface

En source, cela ressemble à ceci:

  • Toute interface à transmettre en tant que paramètre générique:

    public interface IWhatever : IInterface
    {
        // IWhatever specific declarations
    }
    
  • IInterface:

    public interface IInterface
    {
        // Nothing in here, keep moving
    }
    
  • La classe sur laquelle vous voulez mettre la contrainte de type:

    public class WorldPeaceGenerator<T> where T : IInterface
    {
        // Actual world peace generating code
    }
    
6
Ray

Ce que vous avez décidé de faire est le mieux que vous puissiez faire:

public bool Foo<T>() where T : IBase;
2
KevinDeus

J'ai essayé de faire quelque chose de similaire et utilisé une solution de contournement: j'ai pensé à l'opérateur implicite et explicite sur la structure: L'idée est d'envelopper le Type dans une structure qui peut être convertie de manière implicite.

Voici une telle structure:

public struct InterfaceType {type privé _type;

public InterfaceType(Type type)
{
    CheckType(type);
    _type = type;
}

public static explicit operator Type(InterfaceType value)
{
    return value._type;
}

public static implicit operator InterfaceType(Type type)
{
    return new InterfaceType(type);
}

private static void CheckType(Type type)
{
    if (type == null) throw new NullReferenceException("The type cannot be null");
    if (!type.IsInterface) throw new NotSupportedException(string.Format("The given type {0} is not an interface, thus is not supported", type.Name));
}

}

utilisation de base:

// OK
InterfaceType type1 = typeof(System.ComponentModel.INotifyPropertyChanged);

// Throws an exception
InterfaceType type2 = typeof(WeakReference);

Vous devez imaginer votre propre mécanisme autour de cela, mais un exemple pourrait être une méthode prenant un paramètre InterfaceType en paramètre au lieu d'un type

this.MyMethod(typeof(IMyType)) // works
this.MyMethod(typeof(MyType)) // throws exception

Une méthode à remplacer qui devrait retourner les types d'interface:

public virtual IEnumerable<InterfaceType> GetInterfaces()

Il y a peut-être des choses à faire avec les génériques aussi, mais je n'ai pas essayé

J'espère que cela peut aider ou donner des idées :-)

1
Charles HETIER