Как использовать интерфейс как ограничение типа С#?

Есть ли способ получить следующее объявление функции?

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

т. где T - тип интерфейса (аналогично where T : class и struct).

В настоящее время я решил:

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

Где IBase определяется как пустой интерфейс, который наследуется всеми моими интерфейсами... Не идеально, но он должен работать... Почему вы не можете определить, что общий тип должен быть интерфейсом?

Для чего я хочу это, потому что Foo делает отражение, где ему нужен тип интерфейса... Я мог бы передать его как обычный параметр и выполнить необходимую проверку самой функции, но это казалось много более типичных (и я полагаю, немного более результативным, так как все проверки выполняются в compiletime).

Ответ 1

Ближе всего вы можете сделать (за исключением подхода с базовым интерфейсом) "where T : class", что означает ссылочный тип. Синтаксис не означает "любой интерфейс".

Это ( "where T : class" ) используется, например, в WCF для ограничения клиентов на контракты на обслуживание (интерфейсы).

Ответ 2

Я знаю, что это немного поздно, но для тех, кого это интересует, вы можете использовать проверку времени выполнения.

typeof(T).IsInterface

Ответ 3

Нет, на самом деле, если вы думаете, что class и struct означают class es и struct s, вы ошибаетесь. class означает любой ссылочный тип (например, включает в себя интерфейсы), а struct означает любой тип значения (например, struct, enum).

Ответ 4

Чтобы ответить на ответ Роберта, это еще позже, но вы можете использовать статический вспомогательный класс, чтобы сделать проверку времени выполнения только для каждого типа:

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

Я также отмечаю, что ваше решение "должно работать" на самом деле не работает. Рассмотрим:

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

Теперь вам ничего не мешает вам вызвать Foo:

Foo<Actual>();

Класс Actual, в конце концов, удовлетворяет ограничению IBase.

Ответ 5

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

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

Поведение

Наилучшим возможным результатом является то, что наша программа не компилируется, если ограничения не выполняются. К сожалению, это невозможно в текущей реализации С#.

Следующее лучшее, что программа вылетает с момента ее запуска.

Последняя опция заключается в том, что программа выйдет из строя с момента нажатия на код. Это поведение по умолчанию .NET. Для меня это совершенно неприемлемо.

Prerequirements

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

Это позволяет нам делать такие вещи в нашем коде:

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

(Я сохранил where T:class здесь, потому что я всегда предпочитаю проверки времени компиляции для проверки времени выполнения)

Итак, это оставляет нам только одну проблему, которая проверяет, соответствуют ли все типы, которые мы используем, ограничениям. Как трудно это быть?

Позвольте разбить его

Общие типы всегда либо относятся к классу (/struct/interface), либо к методу.

Запуск ограничения требует выполнения одной из следующих задач:

  • Время компиляции при использовании типа в типе (наследование, общее ограничение, член класса)
  • Время компиляции при использовании типа в теле метода
  • Время выполнения при использовании отражения для построения чего-либо, основанного на базовом базовом классе.
  • Время выполнения при использовании отражения для построения чего-то, основанного на RTTI.

В этот момент я хотел бы заявить, что вы всегда должны избегать выполнения (4) в любой программе IMO. Несмотря на это, эти проверки не поддержат его, так как это будет эффективно означать решение проблемы прекращения.

Случай 1: использование типа

Пример:

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

Пример 2:

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

В основном это касается сканирования всех типов, наследования, элементов, параметров и т.д. и т.д. Если тип является общим типом и имеет ограничение, мы проверяем ограничение; если это массив, мы проверяем тип элемента.

В этот момент я должен добавить, что это нарушит тот факт, что по умолчанию .NET загружает типы "ленивые". Сканируя все типы, мы вынуждаем среду выполнения .NET загружать их все. Для большинства программ это не должно быть проблемой; все же, если вы используете статические инициализаторы в своем коде, вы можете столкнуться с проблемами с этим подходом... Тем не менее, я бы не советовал кому-либо делать это в любом случае (за исключением таких вещей:-), поэтому он не должен давать у вас много проблем.

Случай 2: использование типа в методе

Пример:

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

Чтобы проверить это, у нас есть только один вариант: декомпилировать класс, проверить все токены-участники, которые используются, и если один из них является общим типом - проверьте аргументы.

Случай 3: Отражение, общая конструкция времени выполнения

Пример:

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

Я полагаю, что теоретически можно проверить это с помощью подобных трюков как на случай (2), но его реализация намного сложнее (вам нужно проверить, вызывается ли MakeGenericType в некотором кодовом пути). Я не буду вдаваться в подробности здесь...

Случай 4: отражение, RTTI RTTI

Пример:

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

Это худший сценарий, и, как я уже объяснял ранее, это была плохая идея ИМХО. В любом случае, нет практического способа понять это с помощью проверок.

Тестирование партии

Создание программы, которая проверяет регистр (1) и (2), приведет к чему-то вроде этого:

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

Используя код

Хорошо, что легкая часть: -)

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

Ответ 6

Вы не можете сделать это в любой выпущенной версии С# или в предстоящем С# 4.0. Это не ограничение С#: либо нет ограничений интерфейса в самой CLR.

Ответ 7

Если возможно, я пошел с таким решением. Он работает только в том случае, если вы хотите, чтобы несколько конкретных интерфейсов (например, те, к которым у вас есть доступ к источнику), были переданы как общий параметр, а не какой-либо.

  • Я позволю своим интерфейсам, которые поставили под вопрос, наследовать пустой интерфейс IInterface.
  • Я ограничил общий параметр T равным IInterface

В источнике это выглядит так:

  • Любой интерфейс, который вы хотите передать в качестве общего параметра:

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

    public interface IInterface
    {
        // Nothing in here, keep moving
    }
    
  • Класс, на который вы хотите поместить ограничение типа:

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

Ответ 8

То, что вы задумали, - это лучшее, что вы можете сделать:

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

Ответ 9

Я попытался сделать что-то подобное и использовал обходное решение: я думал о неявном и явном операторе по структуре: идея состоит в том, чтобы обернуть тип в структуру, которая может быть преобразована в тип неявно.

Вот такая структура:

public struct InterfaceType {   private Тип _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));
}

}

базовое использование:

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

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

Вы должны представить себе свой собственный mecanism вокруг этого, но примером может быть метод, который принимает параметр InterfaceType в параметре вместо типа

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

Метод переопределения, который должен возвращать типы интерфейсов:

public virtual IEnumerable<InterfaceType> GetInterfaces()

Возможно, что-то связано с дженериками, но я не пробовал

Надеюсь, это может помочь или дает идеи: -)

Ответ 10

Вместо этого используйте абстрактный класс. Итак, у вас будет что-то вроде:

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