How can I use interface as a C# generic type constraint?

前端 未结 11 2164
不思量自难忘°
不思量自难忘° 2020-11-27 12:27

Is there a way to get the following function declaration?

public bool Foo() where T : interface;

ie. where T is an interface type

相关标签:
11条回答
  • 2020-11-27 12:54

    For some time now I've been thinking about near-compile-time constraints, so this is a perfect opportunity to launch the concept.

    The basic idea is that if you cannot do a check compile time, you should do it at the earliest possible point in time, which is basically the moment the application starts. If all checks are okay, the application will run; if a check fails, the application will fail instantly.

    Behavior

    The best possible outcome is that our program doesn't compile if the constraints are not met. Unfortunately that's not possible in the current C# implementation.

    Next best thing is that the program crashes the moment it's started.

    The last option is that the program will crash the moment the code is hit. This is the default behavior of .NET. For me, this is completely unacceptable.

    Prerequirements

    We need to have a constraint mechanism, so for the lack of anything better... let's use an attribute. The attribute will be present on top of a generic constraint to check if it matches our conditions. If it doesn't, we give an ugly error.

    This enables us to do things like this in our code:

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

    (I've kept the where T:class here, because I always prefer compile-time checks to run-time checks)

    So, that only leaves us with 1 problem, which is checking if all the types that we use match the constraint. How hard can it be?

    Let's break it up

    Generic types are always either on a class (/struct/interface) or on a method.

    Triggering a constraint requires you to do one of the following things:

    1. Compile-time, when using a type in a type (inheritance, generic constraint, class member)
    2. Compile-time, when using a type in a method body
    3. Run-time, when using reflection to construct something based on the generic base class.
    4. Run-time, when using reflection to construct something based on RTTI.

    At this point, I would like to state that you should always avoid doing (4) in any program IMO. Regardless, these checks won't support it, since it would effectively mean solving the halting problem.

    Case 1: using a type

    Example:

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

    Example 2:

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

    Basically this involves scanning all types, inheritance, members, parameters, etc, etc, etc. If a type is a generic type and has a constraint, we check the constraint; if it's an array, we check the element type.

    At this point I must add that this will break the fact that by default .NET loads types 'lazy'. By scanning all the types, we force the .NET runtime to load them all. For most programs this shouldn't be a problem; still, if you use static initializers in your code, you might encounter problems with this approach... That said, I wouldn't advice anyone to do this anyways (except for things like this :-), so it shouldn't give you a lot of problems.

    Case 2: using a type in a method

    Example:

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

    To check this we have only 1 option: decompile the class, check all member tokens that are used and if one of them is the generic type - check the arguments.

    Case 3: Reflection, runtime generic construction

    Example:

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

    I suppose it's theoretically possible to check this with similar tricks as case (2), but the implementation of it is much harder (you need to check if MakeGenericType is called in some code path). I won't go into details here...

    Case 4: Reflection, runtime RTTI

    Example:

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

    This is the worst case scenario and as I explained before generally a bad idea IMHO. Either way, there's no practical way to figure this out using checks.

    Testing the lot

    Creating a program that tests case (1) and (2) will result in something like this:

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

    Using the code

    Well, that's the easy part :-)

    // 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();
        }
    }
    
    0 讨论(0)
  • 2020-11-27 12:55

    To follow up on Robert's answer, this is even later, but you can use a static helper class to make the runtime check once only per 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() { /*...*/ }
    }
    

    I also note that your "should work" solution does not, in fact, work. Consider:

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

    Now there's nothing stopping you from calling Foo thus:

    Foo<Actual>();
    

    The Actual class, after all, satisfies the IBase constraint.

    0 讨论(0)
  • 2020-11-27 12:56

    Use an abstract class instead. So, you would have something like:

    public bool Foo<T>() where T : CBase;
    
    0 讨论(0)
  • 2020-11-27 12:57

    If possible, I went with a solution like this. It only works if you want several specific interfaces (e.g. those you have source access to) to be passed as a generic parameter, not any.

    • I let my interfaces, which came into question, inherit an empty interface IInterface.
    • I constrained the generic T parameter to be of IInterface

    In source, it looks like this:

    • Any interface you want to be passed as the generic parameter:

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

      public interface IInterface
      {
          // Nothing in here, keep moving
      }
      
    • The class on which you want to put the type constraint:

      public class WorldPeaceGenerator<T> where T : IInterface
      {
          // Actual world peace generating code
      }
      
    0 讨论(0)
  • 2020-11-27 12:57

    Solution A: This combination of constraints should guarantee that TInterface is an interface:

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

    It requires a single struct TStruct as a Witness to proof that TInterface is a struct.

    You can use single struct as a witness for all your non-generic types:

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

    Solution B: If you don't want to make structs as witnesses you can create an interface

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

    and use a constraint:

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

    Implementation for interfaces:

    interface IA :ISInterface<IA>{ }
    

    This solves some of the problems, but requires trust that noone implements ISInterface<T> for non-interface types, but that is pretty hard to do accidentally.

    0 讨论(0)
提交回复
热议问题