Jak używać interfejsu jako ograniczenia typu ogólnego C #?


164

Czy istnieje sposób na uzyskanie następującej deklaracji funkcji?

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

to znaczy. gdzie T to typ interfejsu (podobny do where T : classi struct).

Obecnie zadowalam się:

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

Gdzie IBase jest zdefiniowany jako pusty interfejs, który jest dziedziczony przez wszystkie moje niestandardowe interfejsy ... Nie jest to idealne, ale powinno działać ... Dlaczego nie możesz zdefiniować, że typ ogólny musi być interfejsem?

Co jest warte, chcę tego, ponieważ Foowykonuje refleksję tam, gdzie potrzebuje typu interfejsu ... Mógłbym przekazać to jako normalny parametr i przeprowadzić niezbędne sprawdzenie w samej funkcji, ale wydawało się to o wiele bardziej bezpieczne (i ja załóżmy, że jest trochę wydajniejszy, ponieważ wszystkie sprawdzenia są wykonywane w czasie kompilacji).


4
Właściwie, twoja IBase dea jest najlepsza, jaką do tej pory widziałem. Niestety, nie możesz go używać w przypadku interfejsów, których nie posiadasz. Wszystko, co C # musiałby zrobić, to mieć wszystkie interfejsy dziedziczące z IOjbect, tak jak wszystkie klasy dziedziczą po Object.
Rhyous

1
Uwaga: zdarza się, że jest to dość powszechny pomysł. Puste interfejsy, takie jak IBase- używane w ten sposób - nazywane są interfejsami znaczników . Umożliwiają specjalne zachowania dla „oznaczonych” typów.
pius

Odpowiedzi:


132

Najbliższe co możesz zrobić (poza podejściem do interfejsu podstawowego) to " where T : class", co oznacza typ referencyjny. Nie ma składni oznaczającej „dowolny interfejs”.

To („ where T : class”) jest używane na przykład w programie WCF w celu ograniczenia klientów do umów serwisowych (interfejsów).


7
dobra odpowiedź, ale czy masz pojęcie, dlaczego ta składnia nie istnieje? Wygląda na to, że byłaby to fajna funkcja.
Stephen Holt,

@StephenHolt: Myślę, że twórcy .NET, decydując na jakie ograniczenia zezwalać, skupili się na tych, które pozwoliłyby klasom i metodom robić rzeczy z typami rodzajowymi, których inaczej nie mogłyby, zamiast na zapobieganiu ich używania w bezsensowne sposoby. To powiedziawszy, interfaceograniczenie Tpowinno umożliwiać porównywanie odniesień między Tdowolnym innym typem odniesienia, ponieważ porównania odniesień są dozwolone między dowolnym interfejsem a prawie każdym innym typem odniesienia, a umożliwienie porównań nawet w tym przypadku nie byłoby problemu.
supercat

1
@supercat innym użytecznym zastosowaniem takiego hipotetycznego ograniczenia byłoby bezpieczne utworzenie serwera proxy dla wystąpień typu. W przypadku interfejsu jest on bezpieczny, podczas gdy w przypadku klas zapieczętowanych zawodzi, podobnie jak w przypadku klas z metodami niewirtualnymi.
Ivan Danilov

@IvanDanilov: Istnieje wiele możliwych ograniczeń, które, jeśli są dozwolone, pożytecznie blokują niektóre bezsensowne konstrukcje. Zgadzam się, że ograniczenie dla „dowolnego typu interfejsu” byłoby fajne, ale nie widzę, aby pozwalało na cokolwiek, czego nie można by było bez niego zrobić, z wyjątkiem generowania skrzeków w czasie kompilacji, kiedy podejmowane są próby wykonania rzeczy, które w przeciwnym razie mogłyby zawieść w czasie wykonywania.
supercat

113

Wiem, że to trochę za późno, ale dla zainteresowanych możesz skorzystać z funkcji sprawdzania czasu pracy.

typeof(T).IsInterface

11
+1 za bycie jedyną odpowiedzią, która na to wskazuje. Właśnie dodałem odpowiedź z podejściem do poprawy wydajności poprzez sprawdzanie każdego typu tylko raz, a nie za każdym razem, gdy metoda jest wywoływana.
phoog

9
C # cała idea typów ogólnych w C # polega na zapewnieniu bezpieczeństwa w czasie kompilacji. To, co sugerujesz, można równie dobrze wykonać metodą inną niż ogólna Foo(Type type).
Jacek Gorgoń

Podoba mi się sprawdzanie czasu działania. Dzięki.
Tarık Özgün Güner

Również w czasie wykonywania można użyć if (new T() is IMyInterface) { }do sprawdzenia, czy interfejs jest zaimplementowany przez klasę T. Może nie być najbardziej wydajnym, ale działa.
tkerwood

26

No, rzeczywiście, jeśli myśli classi structoznaczać classES i structS, mylisz się. classoznacza dowolny rodzaj odniesienia (np zawiera interfejsy zbyt) i structoznacza wartość dowolnego typu (na przykład struct, enum).


1
Czy to nie jest jednak definicja różnicy między klasą a strukturą: że każda klasa jest typem referencyjnym (i odwrotnie) i to samo dotyczy typów stuct / wartości
Matthew Scharley

Matthew: Typy wartości to coś więcej niż struktury C #. Na przykład wyliczenia są typami wartości i where T : structograniczeniami dopasowania .
Mehrdad Afshari

Warto zauważyć, że typy interfejsów użyte w ograniczeniach nie implikują class, ale zadeklarowanie lokalizacji pamięci typu interfejsu w rzeczywistości deklaruje lokalizację pamięci jako odwołanie do klasy, która implementuje ten typ.
supercat

4
Mówiąc jeszcze dokładniej, where T : structodpowiada NotNullableValueTypeConstraint, czyli musi być typem wartości innym niż Nullable<>. (Więc Nullable<>to typ struct, który nie spełnia where T : structograniczenie.)
Jeppe Stig Nielsen

19

Kontynuując odpowiedź Roberta, jest to jeszcze później, ale możesz użyć statycznej klasy pomocniczej, aby sprawdzić w czasie wykonywania tylko raz dla każdego typu:

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

Zwracam również uwagę, że Twoje rozwiązanie „powinno działać” w rzeczywistości nie działa. Rozważać:

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

Teraz nic nie stoi na przeszkodzie, aby zadzwonić do Foo:

Foo<Actual>();

W Actualkońcu klasa spełnia to IBaseograniczenie.


staticKonstruktor nie może być public, więc to powinno dać błąd kompilacji. Twoja staticklasa zawiera również metodę instancji, co również jest błędem w czasie kompilacji.
Jeppe Stig Nielsen,

Spóźnione podziękowania dla Nawfala za poprawienie błędów odnotowanych przez @JeppeStigNielsen
phoog

10

Od jakiegoś czasu zastanawiam się nad ograniczeniami związanymi z czasem kompilacji, więc jest to doskonała okazja do uruchomienia koncepcji.

Podstawową ideą jest to, że jeśli nie możesz sprawdzić czasu kompilacji, powinieneś to zrobić jak najwcześniej, czyli w momencie uruchomienia aplikacji. Jeśli wszystkie testy przebiegną pomyślnie, aplikacja będzie działać; jeśli sprawdzenie się nie powiedzie, aplikacja zawiedzie natychmiast.

Zachowanie

Najlepszym możliwym wynikiem jest to, że nasz program nie kompiluje się, jeśli ograniczenia nie są spełnione. Niestety nie jest to możliwe w obecnej implementacji C #.

Następną najlepszą rzeczą jest to, że program ulega awarii w momencie uruchomienia.

Ostatnią opcją jest to, że program ulegnie awarii w momencie trafienia kodu. Jest to domyślne zachowanie platformy .NET. Dla mnie jest to całkowicie nie do przyjęcia.

Wymagania wstępne

Musimy mieć mechanizm ograniczający, więc z braku czegoś lepszego ... użyjmy atrybutu. Atrybut będzie obecny nad ogólnym ograniczeniem, aby sprawdzić, czy spełnia nasze warunki. Jeśli tak się nie stanie, popełniamy brzydki błąd.

To pozwala nam robić takie rzeczy w naszym kodzie:

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

(Zachowałem where T:classtutaj, ponieważ zawsze wolę sprawdzanie czasu kompilacji od sprawdzania w czasie wykonywania)

Pozostaje więc tylko jeden problem, który polega na sprawdzeniu, czy wszystkie typy, których używamy, pasują do ograniczenia. Jak trudne może to być?

Rozbijmy to

Typy ogólne są zawsze albo w klasie (/ struct / interface), albo w metodzie.

Wyzwolenie ograniczenia wymaga wykonania jednej z następujących czynności:

  1. Czas kompilacji, w przypadku używania typu w typie (dziedziczenie, ograniczenie ogólne, element członkowski klasy)
  2. Czas kompilacji, gdy używasz typu w treści metody
  3. W czasie wykonywania, gdy używasz odbicia do konstruowania czegoś na podstawie ogólnej klasy bazowej.
  4. Czas działania, kiedy używasz refleksji do konstruowania czegoś w oparciu o RTTI.

W tym miejscu chciałbym stwierdzić, że zawsze należy unikać robienia (4) w jakimkolwiek programie IMO. Niezależnie od tego te kontrole go nie obsługują, ponieważ oznaczałoby to skuteczne rozwiązanie problemu zatrzymania.

Przypadek 1: użycie typu

Przykład:

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

Przykład 2:

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

Zasadniczo obejmuje to skanowanie wszystkich typów, dziedziczenia, składowych, parametrów itp., Itd., Itd. Jeśli typ jest typem ogólnym i zawiera ograniczenie, sprawdzamy ograniczenie; jeśli jest to tablica, sprawdzamy typ elementu.

W tym miejscu muszę dodać, że to przełamie fakt, że domyślnie .NET ładuje typy „leniwe”. Skanując wszystkie typy, zmuszamy środowisko uruchomieniowe .NET do załadowania ich wszystkich. W przypadku większości programów nie powinno to stanowić problemu; mimo to, jeśli używasz statycznych inicjatorów w swoim kodzie, możesz napotkać problemy z tym podejściem ... Mimo to nie radziłbym nikomu tego robić (z wyjątkiem takich rzeczy :-), więc nie powinien dawać masz wiele problemów.

Przypadek 2: użycie typu w metodzie

Przykład:

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

Aby to sprawdzić, mamy tylko jedną opcję: zdekompiluj klasę, sprawdź wszystkie używane tokeny składowe, a jeśli jeden z nich jest typem ogólnym - sprawdź argumenty.

Przypadek 3: Odbicie, ogólna konstrukcja środowiska wykonawczego

Przykład:

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

Przypuszczam, że teoretycznie można to sprawdzić podobnymi trikami jak w przypadku (2), ale implementacja tego jest znacznie trudniejsza (trzeba sprawdzić, czy MakeGenericTypejest wywoływana w jakiejś ścieżce kodu). Nie będę tutaj wchodził w szczegóły ...

Przypadek 4: odbicie, czas wykonania RTTI

Przykład:

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

To jest najgorszy scenariusz i jak wyjaśniłem wcześniej, ogólnie zły pomysł IMHO. Tak czy inaczej, nie ma praktycznego sposobu rozwiązania tego problemu za pomocą czeków.

Testowanie partii

Utworzenie programu testującego przypadek (1) i (2) da coś takiego:

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

Korzystanie z kodu

Cóż, to łatwa część :-)

// 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

Nie można tego zrobić w żadnej wydanej wersji C #, ani w nadchodzącym C # 4.0. Nie jest to też ograniczenie języka C # - w samym środowisku CLR nie ma ograniczenia „interfejsu”.


6

Jeśli to możliwe, wybrałem takie rozwiązanie. Działa tylko wtedy, gdy chcesz, aby kilka określonych interfejsów (np. Tych, do których masz dostęp do źródła) było przekazywanych jako parametr ogólny, a nie żaden.

  • Pozwoliłem, aby moje interfejsy, które były kwestionowane, dziedziczyły pusty interfejs IInterface.
  • Ograniczyłem ogólny parametr T do wartości IInterface

W źródle wygląda to tak:

  • Dowolny interfejs, który chcesz przekazać jako parametr ogólny:

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

    public interface IInterface
    {
        // Nothing in here, keep moving
    }
  • Klasa, dla której chcesz umieścić ograniczenie typu:

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

To niewiele daje. Twój Tnie jest ograniczony do interfejsów, jest ograniczony do wszystkiego, co implementuje IInterface- co każdy typ może zrobić, jeśli chce, np. struct Foo : IInterfacePonieważ Twój IInterfacejest najprawdopodobniej publiczny (w przeciwnym razie wszystko, co go akceptuje, musiałoby być wewnętrzne).
AnorZaken

Jeśli kontrolujesz wszystkie typy, które i tak chcesz zaakceptować, możesz użyć generowania kodu, aby utworzyć wszystkie odpowiednie przeciążenia, z których wszystkie po prostu przekierowują do ogólnej metody prywatnej.
AnorZaken

2

To, na co się zdecydowałeś, to najlepsze, co możesz zrobić:

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

2

Próbowałem zrobić coś podobnego i zastosowałem rozwiązanie obejściowe: pomyślałem o niejawnym i jawnym operatorze struktury: Pomysł polega na zawinięciu Type w strukturę, którą można niejawnie przekonwertować na Type.

Oto taka struktura:

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

}

podstawowe zastosowanie:

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

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

Musisz wyobrazić sobie swój własny mekanizm wokół tego, ale przykładem może być metoda z parametrem typu InterfaceType zamiast typu

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

Metoda do przesłonięcia, która powinna zwracać typy interfejsów:

public virtual IEnumerable<InterfaceType> GetInterfaces()

Być może są też rzeczy do zrobienia z lekami generycznymi, ale nie próbowałem

Mam nadzieję, że to pomoże lub daje pomysły :-)


0

Rozwiązanie A: Ta kombinacja ograniczeń powinna zagwarantować, że TInterfacejest to interfejs:

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

Potrzeba tylko jednej struktury TStructjako Świadka, aby to udowodnićTInterface jest strukturą.

Możesz użyć pojedynczej struktury jako świadka dla wszystkich nieogólnych typów:

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

Rozwiązanie B: Jeśli nie chcesz tworzyć struktur jako świadków, możesz utworzyć interfejs

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

i użyj ograniczenia:

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

Wdrożenie dla interfejsów:

interface IA :ISInterface<IA>{ }

To rozwiązuje niektóre problemy, ale wymaga zaufania, którego nikt nie implementuje ISInterface<T>dla typów bez interfejsów, ale jest to dość trudne do zrobienia przypadkowo.


-4

Zamiast tego użyj klasy abstrakcyjnej. Więc miałbyś coś takiego:

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

10
Nie zawsze można zastąpić interfejs klasą abstrakcyjną, ponieważ C # nie obsługuje dziedziczenia wielokrotnego.
Sam
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.