如何将接口用作C#泛型类型约束?

时间:2009-07-08 07:18:02

标签: c# generics interface constraints

有没有办法获得以下函数声明?

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

即。其中T是接口类型(类似于where T : classstruct)。

目前我已决定:

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

其中IBase被定义为我所有自定义接口继承的空接口...不理想,但它应该工作...为什么不能定义泛型类型必须是接口?

为了它的价值,我希望这是因为Foo正在进行反射,它需要一个接口类型...我可以将它作为普通参数传递并在函数本身中进行必要的检查,但是这个似乎有更多类型安全(我想更高性能,因为所有检查都是在编译时完成的。)

11 个答案:

答案 0 :(得分:118)

您可以做的最接近的(基接口方法除外)是“where T : class”,意思是引用类型。没有语法表示“任何接口”。

例如,在WCF中使用此(“where T : class”)将客户端限制为服务合同(接口)。

答案 1 :(得分:103)

我知道这有点晚了但对于那些感兴趣的人,你可以使用运行时检查。

typeof(T).IsInterface

答案 2 :(得分:25)

不,实际上,如果您认为classstruct意味着class es和struct,那么您错了。 class表示任何引用类型(例如也包括接口),struct表示任何值类型(例如struct,{{1 }})。

答案 3 :(得分:19)

为了跟进Robert的回答,这甚至是后来的,但你可以使用一个静态助手类来仅对每种类型进行一次运行时检查:

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约束。

答案 4 :(得分:9)

一段时间以来,我一直在考虑接近编译时的限制,所以这是发布这个概念的绝佳机会。

基本思想是,如果你不能做一个检查编译时间,你应该尽早完成,这基本上就是应用程序启动的那一刻。如果所有检查都没问题,应用程序将运行;如果检查失败,应用程序将立即失败。

<强>行为

最好的结果是,如果不满足约束,我们的程序就不会编译。不幸的是,在当前的C#实现中,这是不可能的。

接下来最好的事情是该程序在它启动的那一刻就崩溃了。

最后一个选项是程序在代码被命中时会崩溃。这是.NET的默认行为。 对我来说,这是完全不可接受的。

<强> Prerequirements

我们需要一个约束机制,所以对于缺乏更好的东西......让我们使用一个属性。该属性将出现在通用约束之上,以检查它是否与我们的条件匹配。如果它没有,我们会发出一个丑陋的错误。

这使我们能够在代码中执行以下操作:

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

(我已将where T:class保留在此处,因为我总是更喜欢编译时检查到运行时检查)

所以,这只会给我们留下1个问题,即检查我们使用的所有类型是否与约束匹配。这有多难?

让我们分手

泛型类型总是在类(/ struct / interface)或方法上。

触发约束要求您执行以下操作之一:

  1. 编译时,在类型中使用类型(继承,泛型约束,类成员)
  2. 编译时,在方法正文中使用类型时
  3. 运行时,使用反射基于通用基类构造内容时。
  4. 运行时,使用反射构建基于RTTI的东西。
  5. 此时,我想说你应该总是避免在任何IMO程序中做(4)。无论如何,这些检查都不会支持它,因为它实际上意味着解决停止问题。

    案例1:使用类型

    示例:

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

    示例2:

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

    基本上这涉及扫描所有类型,继承,成员,参数等等。如果类型是泛型类型并且有约束,我们检查约束;如果它是一个数组,我们检查元素类型。

    此时我必须补充一点,这将打破这样一个事实,即默认情况下.NET加载了类型&#39; lazy&#39;。通过扫描所有类型,我们强制.NET运行时加载它们。对于大多数程序来说,这不应该是一个问题;仍然,如果你在你的代码中使用静态初始化器,你可能会遇到这种方法的问题......那就是说,我不建议任何人这样做(除了这样的事情:-),所以它不应该&# 39;给你很多问题。

    案例2:在方法中使用类型

    示例:

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

    要检查这一点,我们只有一个选项:反编译该类,检查所有使用的成员标记,如果其中一个是泛型​​类型 - 检查参数。

    案例3:反思,运行时通用构造

    示例:

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

    我认为理论上可以用类似于案例(2)的技巧来检查它,但是它的实现要困难得多(你需要检查在某些代码路径中是否调用了MakeGenericType) 。我不打算在这里详细介绍......

    案例4:反思,运行时RTTI

    示例:

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

    这是最糟糕的情况,正如我之前解释的那样,恕我直言。无论哪种方式,使用支票都没有实际的方法来解决这个问题。

    测试批次

    创建一个测试case(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();
        }
    }
    

答案 5 :(得分:8)

您不能在任何已发布的C#版本中执行此操作,也不能在即将到来的C#4.0中执行此操作。它也不是C#限制 - 在CLR本身中没有“接口”约束。

答案 6 :(得分:6)

如果可能的话,我选择了这样的解决方案。只有当您想要将几个特定接口(例如您有源访问权限的接口)作为通用参数传递时,它才有效。

  • 我让我的界面产生了问题,继承了一个空接口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
    }
    

答案 7 :(得分:2)

您已满足的是您可以做的最好的事情:

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

答案 8 :(得分:1)

我尝试做类似的事情并使用了一种解决方案:我考虑了结构上的隐式和显式运算符:我们的想法是将Type包装在一个可以隐式转换为Type的结构中。

这是一个结构:

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

}

基本用法:

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

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

你必须想象你自己的机制主义,但一个例子可能是在参数而不是类型中使用InterfaceType的方法

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

覆盖它的方法应该返回接口类型:

public virtual IEnumerable<InterfaceType> GetInterfaces()

也许有些东西与仿制药有关,但我没试过

希望这可以帮助或提出想法: - )

答案 9 :(得分:0)

解决方案A: 约束的这种组合应保证TInterface是一个接口:

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

它需要一个结构TStruct作为见证人,以证明TInterface是一个结构。

您可以将单个结构用作所有非泛型类型的见证人

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

解决方案B: 如果您不想让结构成为见证人,则可以创建一个界面

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

并使用约束:

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

接口的实现

interface IA :ISInterface<IA>{ }

这解决了一些问题,但需要相信没有人会为非接口类型实现ISInterface<T>,但这很难做到。

答案 10 :(得分:-4)

改为使用抽象类。所以,你会有类似的东西:

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