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


164

Existe-t-il un moyen 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 (similaire à 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 veux cela parce que je Foofais une réflexion là où il faut un type d'interface ... Je pourrais le passer en paramètre normal et faire le contrôle nécessaire dans la fonction elle-même, mais cela semblait beaucoup plus sûr (et je supposons un peu plus performant, puisque toutes les vérifications sont effectuées à la compilation).


4
En fait, votre IBase dea est le meilleur que j'ai vu jusqu'à présent. Malheureusement, vous ne pouvez pas l'utiliser pour des interfaces que vous ne possédez pas. Tout ce que C # aurait à faire est que toutes les interfaces héritent de IOjbect, tout comme toutes les classes héritent d'Object.
Rhyous

1
Remarque: c'est une idée assez courante. Les interfaces vides comme IBase- utilisées de cette manière - sont appelées interfaces marqueurs . Ils permettent des comportements spéciaux pour les types «marqués».
pius le

Réponses:


132

Le plus proche que vous puissiez faire (à l'exception de votre approche d'interface de base) est " where T : class", qui signifie type de référence. Il n'y a pas de syntaxe pour signifier "n'importe quelle interface".

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


7
belle réponse, mais avez-vous une idée de la raison pour laquelle cette syntaxe n'existe pas? On dirait que ce serait une fonctionnalité intéressante.
Stephen Holt

@StephenHolt: Je pense que les créateurs de .NET, en décidant des contraintes à autoriser, se sont concentrés sur celles qui permettraient aux classes et méthodes génériques de faire des choses avec des types génériques qu'elles ne pourraient autrement pas, plutôt que de les empêcher d'être utilisées dans manières absurdes. Cela dit, une interfacecontrainte sur Tdevrait permettre des comparaisons de références entre Tet tout autre type de référence, car les comparaisons de référence sont autorisées entre toute interface et presque tout autre type de référence, et autoriser des comparaisons même dans ce cas ne poserait aucun problème.
supercat du

1
@supercat une autre application utile d'une telle contrainte hypothétique serait de créer en toute sécurité un proxy pour les instances du type. Pour l'interface, il est garanti d'être sûr, tandis que pour les classes scellées, il échouerait, comme pour les classes avec des méthodes non virtuelles.
Ivan Danilov

@IvanDanilov: Il existe un certain nombre de contraintes imaginables qui, si elles étaient autorisées, bloqueraient utilement certaines constructions absurdes. Je suis d'accord qu'une contrainte pour "n'importe quel type d'interface" serait bien, mais je ne vois pas que cela permettrait de faire quoi que ce soit qui ne pourrait pas être fait sans elle, à l'exception de la génération de squawks au moment de la compilation lorsque des tentatives sont faites pour faire choses qui pourraient autrement échouer à l'exécution.
supercat

113

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

11
+1 pour être la seule réponse à le souligner. Je viens d'ajouter une réponse avec une approche pour améliorer les performances en vérifiant chaque type une seule fois plutôt qu'à chaque fois que la méthode est appelée.
phoog

9
L'idée générale des génériques en C # est d'avoir une sécurité au moment de la compilation. Ce que vous suggérez peut tout aussi bien être effectué avec une méthode non générique Foo(Type type).
Jacek Gorgoń

J'aime le contrôle d'exécution. Merci.
Tarık Özgün Güner

Au moment de l'exécution, vous pouvez également if (new T() is IMyInterface) { }vérifier si une interface est implémentée par la classe T. Ce n'est peut-être pas le plus efficace, mais cela fonctionne.
tkerwood

26

Non, en fait, si vous pensez classet structmoyennes classes et structs, vous avez tort. classsignifie tout type de référence (par exemple, inclut également les interfaces) et structsignifie tout type de valeur (par exemple struct, enum).


1
N'est-ce pas la définition de la différence entre une classe et une structure: que chaque classe est un type de référence (et vice versa) et idem pour les types de structure / valeur
Matthew Scharley

Matthew: Il y a plus de types valeur que de structs C #. Les énumérations, par exemple, sont des types de valeur et une where T : structcontrainte de correspondance .
Mehrdad Afshari

Il convient de noter qu'un type d'interface utilisé dans des contraintes n'implique pas class, mais déclarer un emplacement de stockage d'un type d'interface déclare réellement l'emplacement de stockage comme une référence de classe qui implémente ce type.
supercat

4
Pour être encore plus précis, where T : structcorrespond à NotNullableValueTypeConstraint, donc cela signifie qu'il doit s'agir d'un type valeur différent de Nullable<>. (Il en Nullable<>va de même pour un type struct qui ne satisfait pas la where T : structcontrainte.)
Jeppe Stig Nielsen

19

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 d'exécution une seule fois par type:

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 note é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>();

La Actualclasse, après tout, satisfait la IBasecontrainte.


Un staticconstructeur ne peut pas l'être public, donc cela devrait donner une erreur de compilation. De plus, votre staticclasse contient une méthode d'instance, c'est également une erreur de compilation.
Jeppe Stig Nielsen

Remerciements tardifs à nawfal pour avoir corrigé les erreurs notées par @JeppeStigNielsen
phoog

10

Depuis un certain temps, je réfléchis aux contraintes de quasi-temps de compilation, c'est donc une opportunité parfaite pour lancer le concept.

L'idée de base est que si vous ne pouvez pas effectuer une vérification 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 satisfaites. Malheureusement, ce n'est pas possible dans l'implémentation actuelle de C #.

La meilleure chose suivante est que le programme plante au moment où il a démarré.

La dernière option est que le programme plante au moment où le code est frappé. Il s'agit du comportement par défaut de .NET. Pour moi, c'est totalement inacceptable.

Pré-requis

Nous avons besoin d'un mécanisme de contrainte, donc faute 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 donnons une vilaine erreur.

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

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

(J'ai gardé where T:classici, car je préfère toujours les contrôles à la compilation aux contrôles à l'exécution)

Donc, cela ne nous laisse qu'un seul 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.

Le déclenchement d'une contrainte vous oblige à effectuer l'une des opérations suivantes:

  1. À la compilation, lors de l'utilisation d'un type dans un type (héritage, contrainte générique, membre de classe)
  2. À la compilation, lors de l'utilisation d'un type dans le corps d'une 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. Exécution, lors de l'utilisation de la réflexion pour construire quelque chose basé sur RTTI.

À ce stade, je tiens à préciser que vous devez toujours éviter de faire (4) dans tout programme IMO. Quoi qu'il en soit, ces contrôles ne le soutiendront pas, car cela signifierait effectivement résoudre le problème en suspens.

Cas 1: utilisation d'un type

Exemple:

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

Exemple 2:

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

Fondamentalement, cela implique l'analyse de tous les types, l'héritage, les membres, les paramètres, etc., etc., etc. Si un type est un type générique et 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 cassera 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; quand même, 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 (sauf pour des choses comme celle-ci :-), donc cela ne devrait pas donner vous 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 qui sont utilisés et si l'un d'entre eux est de 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 du cas (2), mais sa mise en œuvre est beaucoup plus difficile (vous devez vérifier si elle MakeGenericTypeest appelée dans un chemin de code). Je n'entrerai pas dans les détails ici ...

Cas 4: Réflexion, RTTI d'exécution

Exemple:

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

C'est le pire des cas et comme je l'ai expliqué avant généralement une mauvaise idée à mon humble avis. Quoi qu'il en soit, il n'y a aucun moyen pratique de comprendre cela à l'aide de chèques.

Tester le lot

Créer un programme qui teste les cas (1) et (2) se traduira par quelque chose comme ceci:

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

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

8

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


6

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 passées en tant que paramètre générique, pas n'importe laquelle.

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

Dans la source, cela ressemble à ceci:

  • Toute interface que vous souhaitez passer 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 souhaitez mettre la contrainte de type:

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

Cela n'accomplit pas grand-chose. Votre Tn'est pas contraint aux interfaces, il est contraint à tout ce qui implémente IInterface- ce que n'importe quel type peut faire s'il le veut, par exemple struct Foo : IInterfacepuisque votre IInterfaceest probablement public (sinon tout ce qui l'accepte devrait être interne).
AnorZaken

Si vous contrôlez tous les types que vous souhaitez accepter de toute façon, vous pouvez utiliser la génération de code pour créer toutes les surcharges appropriées, qui ne font que rediriger vers une méthode privée générique.
AnorZaken le

2

Ce que vous avez choisi est le mieux que vous puissiez faire:

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

2

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

Voici une telle structure:

public struct InterfaceType {private Type _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 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 renvoyer des 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 :-)


0

Solution A: Cette combinaison de contraintes doit garantir qu'il TInterfaces'agit d'une interface:

class example<TInterface, TStruct>
    where TStruct : struct, TInterface
    where TInterface : class
{ }

Il nécessite une structure unique en TStructtant que témoin pour prouver qu'il TInterfaces'agit d'une structure.

Vous pouvez utiliser une structure unique comme témoin pour tous vos types non génériques:

struct InterfaceWitness : IA, IB, IC 
{
    public int DoA() => throw new InvalidOperationException();
    //...
}

Solution B: Si vous ne souhaitez pas créer de structures en tant que témoins, vous pouvez créer une interface

interface ISInterface<T>
    where T : ISInterface<T>
{ }

et utilisez une contrainte:

class example<TInterface>
    where TInterface : ISInterface<TInterface>
{ }

Implémentation pour les interfaces:

interface IA :ISInterface<IA>{ }

Cela résout certains des problèmes, mais nécessite la confiance que personne n'implémente ISInterface<T>pour les types non-interface, mais c'est assez difficile à faire accidentellement.


-4

Utilisez plutôt une classe abstraite. Donc, vous auriez quelque chose comme:

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

10
Vous ne pouvez pas toujours remplacer une interface par une classe abstraite car C # ne prend pas en charge l'héritage multiple.
Sam
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.