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

Mat*_*ley 155 c# generics interface constraints

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

public bool Foo<T>() where T : interface;
Run Code Online (Sandbox Code Playgroud)

即.其中T是接口类型(类似于where T : class,和struct).

目前我已经满足于:

public bool Foo<T>() where T : IBase;
Run Code Online (Sandbox Code Playgroud)

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

对于它的价值,我想要这个,因为Foo它正在做需要接口类型的反射......我可以将它作为普通参数传递并在函数本身中进行必要的检查,但这看起来更加类型安全(而且我假设性能更高一些,因为所有检查都是在编译时完成的).

Mar*_*ell 126

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

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

  • 很好的答案,但你有任何想法*为什么*这个语法不存在?看起来它将是一个很好的功能. (7认同)
  • @StephenHolt:我认为 .NET 的创建者在决定允许哪些约束时,专注于让泛型类和方法可以使用泛型类型做他们原本不能做的事情,而不是阻止它们被用于荒谬的方式。话虽如此,“T”上的“接口”约束应该允许在“T”和任何其他引用类型之间进行引用比较,因为任何接口和几乎任何其他引用类型之间都允许进行引用比较,并且即使在这种情况下也允许进行比较没有问题。 (2认同)

Rob*_*ert 108

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

typeof(T).IsInterface
Run Code Online (Sandbox Code Playgroud)

  • +1是唯一能够指出这一点的答案.我只是通过检查每种类型而不是每次调用方法时添加了一种改进性能的方法. (10认同)
  • C#中泛型的整体思想是具有编译时的安全性.您所建议的也可以使用非泛型方法``Foo(Type type)``来执行. (9认同)

Meh*_*ari 26

不,实际上,如果你在思考class并且struct意味着classes和structs,那你就错了.class任何引用类型(例如包括太接口)和struct任何类型的值(例如struct,enum).

  • 更准确地说,`其中T:struct`对应于[`NotNullableValueTypeConstraint`](http://msdn.microsoft.com/en-us/library/System.Reflection.GenericParameterAttributes.aspx),所以这意味着它必须是一个值类型_other_而不是`Nullable <>`.(所以`Nullable <>`是一个不满足`where T:struct`约束的结构类型.) (3认同)

pho*_*oog 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() { /*...*/ }
}
Run Code Online (Sandbox Code Playgroud)

我还注意到,你的"应该工作"的解决方案实际上并没有起作用.考虑:

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; } }
Run Code Online (Sandbox Code Playgroud)

现在没有什么可以阻止你这样叫Foo:

Foo<Actual>();
Run Code Online (Sandbox Code Playgroud)

Actual毕竟,这个类满足了IBase约束条件.


atl*_*ste 10

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

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

行为

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

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

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

Prerequirements

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

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

public class Clas<[IsInterface] T> where T : class
Run Code Online (Sandbox Code Playgroud)

(我一直在where T:class这里,因为我总是喜欢编译时检查到运行时检查)

所以,这只留给我们一个问题,即检查我们使用的所有类型是否与约束匹配.它能有多难?

让我们分手吧

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

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

  1. 编译时,在类型中使用类型(继承,泛型约束,类成员)
  2. 编译时,在方法体中使用类型时
  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> { ... } 
Run Code Online (Sandbox Code Playgroud)

Example 2:

public class TestClass 
{ 
    SomeClass<IMyInterface> myMember; // or a property, method, etc.
} 
Run Code Online (Sandbox Code Playgroud)

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>();
}
Run Code Online (Sandbox Code Playgroud)

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))
Run Code Online (Sandbox Code Playgroud)

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]");
Run Code Online (Sandbox Code Playgroud)

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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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();
    }
}
Run Code Online (Sandbox Code Playgroud)


Pav*_*aev 8

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


Ray*_*Ray 6

如果可能的话,我选择了这样的解决方案.它只适用于您希望将几个特定接口(例如,您具有源访问权限的接口)作为通用参数传递,而不是任何接口.

  • 我让我的界面产生了问题,继承了一个空接口IInterface.
  • 我将通用T参数约束为 IInterface

在源代码中,它看起来像这样: