Come posso usare l'interfaccia come vincolo di tipo generico C #?


164

C'è un modo per ottenere la seguente dichiarazione di funzione?

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

vale a dire. dove T è un tipo di interfaccia (simile a where T : class, e struct).

Attualmente ho optato per:

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

Dove IBase è definito come un'interfaccia vuota ereditata da tutte le mie interfacce personalizzate ... Non ideale, ma dovrebbe funzionare ... Perché non riesci a definire che un tipo generico deve essere un'interfaccia?

Per quello che vale, lo voglio perché Foosta facendo una riflessione dove ha bisogno di un tipo di interfaccia ... Potrei passarlo come un normale parametro e fare il controllo necessario nella funzione stessa, ma questo sembra molto più tipico (e io supponiamo che sia un po 'più performante, dal momento che tutti i controlli vengono eseguiti in fase di compilazione).


4
In realtà, il tuo IBase dea è il migliore che abbia mai visto finora. Sfortunatamente, non puoi usarlo per interfacce che non possiedi. Tutto ciò che C # dovrebbe fare è avere tutte le interfacce ereditate da IOjbect proprio come tutte le classi ereditano da Object.
Rhyous,

1
Nota: questa sembra essere un'idea piuttosto comune. Interfacce vuote come IBase- utilizzate in questo modo - sono chiamate interfacce marker . Consentono comportamenti speciali per tipi "marcati".
Pio

Risposte:


132

Il più vicino che puoi fare (tranne per il tuo approccio di interfaccia di base) è " where T : class", che significa tipo di riferimento. Non esiste sintassi che significhi "qualsiasi interfaccia".

Questo (" where T : class") viene utilizzato, ad esempio, in WCF per limitare i client ai contratti di assistenza (interfacce).


7
bella risposta, ma hai idea del perché questa sintassi non esiste? Sembra che sarebbe una caratteristica piacevole da avere.
Stephen Holt,

@StephenHolt: Penso che i creatori di .NET, nel decidere quali vincoli consentire, si siano concentrati su quelli che avrebbero permesso alle classi e ai metodi generici di fare cose con tipi generici che altrimenti non avrebbero potuto, piuttosto che impedirne l'uso modi insensati. Detto questo, un interfacevincolo Tdovrebbe consentire confronti di riferimento tra Te qualsiasi altro tipo di riferimento, poiché i confronti di riferimento sono consentiti tra qualsiasi interfaccia e quasi qualsiasi altro tipo di riferimento e consentire confronti anche in quel caso non costituirebbe alcun problema.
supercat

1
@supercat un'altra utile applicazione di tale ipotetico vincolo sarebbe quella di creare in modo sicuro un proxy per istanze del tipo. Per l'interfaccia è garantito che sia sicuro, mentre per le classi sigillate fallirebbe, come per le classi con metodi non virtuali.
Ivan Danilov,

@IvanDanilov: Esistono una serie di vincoli concepibili che, se consentiti, bloccherebbero utilmente alcuni costrutti insensati. Concordo che un vincolo per "qualsiasi tipo di interfaccia" sarebbe carino, ma non vedo che consentirebbe qualsiasi cosa che non si possa fare senza di essa, salvo la generazione di squawks in fase di compilazione quando si tenta di fare cose che potrebbero altrimenti fallire in fase di esecuzione.
supercat,

113

So che è un po 'tardi, ma per coloro che sono interessati è possibile utilizzare un controllo di runtime.

typeof(T).IsInterface

11
+1 per essere l'unica risposta a segnalarlo. Ho appena aggiunto una risposta con un approccio per migliorare le prestazioni controllando ogni tipo solo una volta anziché ogni volta che viene chiamato il metodo.
phoog

9
L'idea generale dei generici in C # è quella di avere una sicurezza in fase di compilazione. Quello che stai suggerendo può anche essere eseguito con un metodo non generico Foo(Type type).
Jacek Gorgoń,

Mi piace il controllo di runtime. Grazie.
Tarık Özgün Güner,

Inoltre in fase di esecuzione è possibile utilizzare if (new T() is IMyInterface) { }per verificare se un'interfaccia è implementata dalla classe T. Potrebbe non essere il più efficiente, ma funziona.
Tkerwood,

26

No, in realtà, se stai pensando classe structmedi classES e structs, ti sbagli. classindica qualsiasi tipo di riferimento (ad esempio include anche le interfacce) e structindica qualsiasi tipo di valore (ad esempio struct, enum).


1
Non è questa la definizione della differenza tra una classe e una struttura: ogni classe è un tipo di riferimento (e viceversa) e idem per tipi di stuct / value
Matthew Scharley

Matthew: Esistono più tipi di valore rispetto alle strutture C #. Gli enumeratori, ad esempio, sono tipi di valore e where T : structvincolo di corrispondenza .
Mehrdad Afshari,

Vale la pena notare che i tipi di interfaccia utilizzati nei vincoli non implicano class, ma dichiarare una posizione di memoria di un tipo di interfaccia dichiara davvero che la posizione di memoria è un riferimento di classe che implementa quel tipo.
supercat,

4
Per essere ancora più preciso, where T : structcorrisponde a NotNullableValueTypeConstraint, quindi significa che deve essere un tipo di valore diverso da Nullable<>. (Quindi Nullable<>è un tipo di struttura che non soddisfa il where T : structvincolo.)
Jeppe Stig Nielsen,

19

Per dare seguito alla risposta di Robert, questo è ancora più tardi, ma è possibile utilizzare una classe di supporto statica per eseguire il controllo del runtime una sola volta per tipo:

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() { /*...*/ }
}

Noto anche che la tua soluzione "dovrebbe funzionare" in effetti non funziona. Tener conto di:

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

Ora non c'è niente che ti impedisce di chiamare Foo così:

Foo<Actual>();

La Actualclasse, dopo tutto, soddisfa il IBasevincolo.


Un staticcostruttore non può essere public, quindi questo dovrebbe dare un errore di compilazione. Inoltre la tua staticclasse contiene un metodo di istanza, che è anche un errore di compilazione.
Jeppe Stig Nielsen,

In ritardo grazie a nawfal per la correzione degli errori rilevati da @JeppeStigNielsen
phoog

10

Da qualche tempo sto pensando a vincoli di tempo quasi di compilazione, quindi questa è un'opportunità perfetta per lanciare il concetto.

L'idea di base è che se non è possibile eseguire un tempo di compilazione del controllo, è necessario farlo al più presto possibile, che è sostanzialmente il momento in cui l'applicazione viene avviata. Se tutti i controlli sono corretti, l'applicazione verrà eseguita; se un controllo fallisce, l'applicazione fallirà immediatamente.

Comportamento

Il miglior risultato possibile è che il nostro programma non viene compilato se i vincoli non vengono rispettati. Purtroppo questo non è possibile nell'attuale implementazione C #.

La prossima cosa migliore è che il programma si blocca nel momento in cui è stato avviato.

L'ultima opzione è che il programma si arresta in modo anomalo nel momento in cui il codice viene colpito. Questo è il comportamento predefinito di .NET. Per me questo è assolutamente inaccettabile.

Prerequisiti

Dobbiamo avere un meccanismo di vincolo, quindi per mancanza di qualcosa di meglio ... usiamo un attributo. L'attributo sarà presente sopra un vincolo generico per verificare se corrisponde alle nostre condizioni. In caso contrario, diamo un brutto errore.

Questo ci consente di fare cose del genere nel nostro codice:

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

(Ho tenuto il where T:classqui, perché preferisco sempre i controlli in fase di compilazione rispetto ai controlli in fase di esecuzione)

Quindi, questo ci lascia solo con 1 problema, che sta controllando se tutti i tipi che usiamo corrispondono al vincolo. Quanto può essere difficile?

Smettiamolo

I tipi generici si trovano sempre su una classe (/ struct / interface) o su un metodo.

L'attivazione di un vincolo richiede di eseguire una delle seguenti operazioni:

  1. Tempo di compilazione, quando si utilizza un tipo in un tipo (eredità, vincolo generico, membro della classe)
  2. Tempo di compilazione, quando si utilizza un tipo in un corpo del metodo
  3. Runtime, quando si utilizza la riflessione per costruire qualcosa basato sulla classe base generica.
  4. Runtime, quando si utilizza la riflessione per costruire qualcosa basato su RTTI.

A questo punto, vorrei affermare che dovresti sempre evitare di fare (4) in qualsiasi programma IMO. Indipendentemente da ciò, questi controlli non lo supporteranno, dal momento che significherebbe effettivamente risolvere il problema di arresto.

Caso 1: utilizzo di un tipo

Esempio:

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

Esempio 2:

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

Fondamentalmente ciò implica la scansione di tutti i tipi, ereditarietà, membri, parametri, ecc. Ecc. Se un tipo è un tipo generico e ha un vincolo, controlliamo il vincolo; se è un array, controlliamo il tipo di elemento.

A questo punto devo aggiungere che ciò interromperà il fatto che per impostazione predefinita .NET carica i tipi 'pigro'. Analizzando tutti i tipi, forziamo il runtime .NET a caricarli tutti. Per la maggior parte dei programmi questo non dovrebbe essere un problema; tuttavia, se usi gli inizializzatori statici nel tuo codice, potresti riscontrare problemi con questo approccio ... Detto questo, non consiglierei a nessuno di farlo comunque (tranne che per cose come questa :-), quindi non dovrebbe dare hai molti problemi.

Caso 2: utilizzo di un tipo in un metodo

Esempio:

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

Per verificarlo abbiamo solo 1 opzione: decompilare la classe, controllare tutti i token membri utilizzati e se uno di questi è di tipo generico - controlla gli argomenti.

Caso 3: riflessione, costruzione generica di runtime

Esempio:

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

Suppongo che sia teoricamente possibile verificarlo con trucchi simili a case (2), ma l'implementazione è molto più difficile (è necessario verificare se MakeGenericTypeviene chiamato in un percorso di codice). Non entrerò nei dettagli qui ...

Caso 4: riflessione, runtime RTTI

Esempio:

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

Questo è lo scenario peggiore e come ho spiegato prima generalmente una cattiva idea IMHO. Ad ogni modo, non esiste un modo pratico per capirlo usando i controlli.

Test del lotto

La creazione di un programma che testa case (1) e (2) si tradurrà in qualcosa del genere:

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

Usando il codice

Bene, questa è la parte 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

Non puoi farlo in nessuna versione rilasciata di C #, né nel prossimo C # 4.0. Non è nemmeno una limitazione C # - non c'è alcun vincolo "interfaccia" nel CLR stesso.


6

Se possibile, sono andato con una soluzione come questa. Funziona solo se si desidera che diverse interfacce specifiche (ad esempio quelle a cui si ha accesso al codice sorgente) vengano passate come parametro generico, non alcuna.

  • Ho lasciato che le mie interfacce, che erano in questione, ereditassero un'interfaccia vuota IInterface.
  • Ho vincolato il parametro T generico per essere di IInterface

In origine, si presenta così:

  • Qualsiasi interfaccia che si desidera passare come parametro generico:

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

    public interface IInterface
    {
        // Nothing in here, keep moving
    }
  • La classe in cui si desidera inserire il vincolo di tipo:

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

Questo non ottiene molto. Il tuo Tnon è vincolato alle interfacce, è limitato a tutto ciò che implementa IInterface, cosa che qualsiasi tipo può fare se lo desidera, ad esempio struct Foo : IInterfaceperché il tuo IInterfaceè molto probabilmente pubblico (altrimenti tutto ciò che lo accetta dovrebbe essere interno).
AnorZaken

Se controlli comunque tutti i tipi che desideri accettare, puoi utilizzare la generazione del codice per creare tutti i sovraccarichi adeguati, che reindirizzano a un metodo privato generico.
AnorZaken

2

Quello per cui ti sei accontentato è il meglio che puoi fare:

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

2

Ho provato a fare qualcosa di simile e ho usato una soluzione alternativa: ho pensato all'operatore implicito ed esplicito sulla struttura: l'idea è di avvolgere il Tipo in una struttura che può essere convertita in Tipo implicitamente.

Ecco una struttura del genere:

public struct InterfaceType {tipo privato _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));
}

}

utilizzo di base:

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

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

Devi immaginare il tuo mecanismo attorno a questo, ma un esempio potrebbe essere un metodo preso un InterfaceType in parametro anziché un tipo

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

Un metodo per sovrascrivere che dovrebbe restituire i tipi di interfaccia:

public virtual IEnumerable<InterfaceType> GetInterfaces()

Forse ci sono anche cose da fare con i generici, ma non ci ho provato

Spero che questo possa aiutare o dare idee :-)


0

Soluzione A: Questa combinazione di vincoli dovrebbe garantire che TInterfacesia un'interfaccia:

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

Richiede una singola struttura TStructcome Testimone per provare che TInterfaceè una struttura.

Puoi usare single struct come testimone per tutti i tuoi tipi non generici:

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

Soluzione B: se non si desidera creare strutture come testimoni, è possibile creare un'interfaccia

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

e usa un vincolo:

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

Implementazione per interfacce:

interface IA :ISInterface<IA>{ }

Questo risolve alcuni dei problemi, ma richiede la fiducia che nessuno implementa ISInterface<T>per i tipi non di interfaccia, ma è piuttosto difficile da fare accidentalmente.


-4

Utilizzare invece una classe astratta. Quindi, avresti qualcosa del tipo:

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

10
Non è sempre possibile sostituire un'interfaccia con una classe astratta poiché C # non supporta l'ereditarietà multipla.
Sam,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.