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


164

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

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

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

目前,我已经满足:

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

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

对于它的价值,我想Foo要这样做,因为是在需要接口类型的地方进行反射...我可以将其作为常规参数传入,并在函数本身中进行必要的检查,但这似乎要类型安全得多(而且我假设性能更高一点,因为所有检查都是在编译时完成的)。


4
实际上,您的IBase dea是迄今为止我见过的最好的。不幸的是,您不能将其用于您不拥有的接口。C#要做的就是让所有接口都从IOjbect继承,就像所有类都从Object继承一样。
Rhyous 2015年

1
注意:这恰好是一个很普遍的想法。像这样IBase使用的空接口称为标记接口。它们为“标记”类型启用特殊行为。
pius

Answers:


132

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

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


7
好的答案,但是您知道为什么不存在这种语法吗?似乎这将是一个不错的功能。
斯蒂芬·霍尔特

@StephenHolt:我认为.NET的创建者在决定允许哪些约束时,将精力集中在那些使泛型类和方法以其他方式无法实现的泛型类型上做事情的方式,而不是阻止它们在荒谬的方式。话虽这么说,对的interface约束T应该允许在T与任何其他引用类型之间进行引用比较,因为在任何接口和几乎任何其他引用类型之间都允许进行引用比较,并且即使在这种情况下也可以进行比较不会造成问题。
2014年

1
这种假设约束的另一个有用的应用是为该类型的实例安全地创建代理。对于接口,它保证是安全的,而对于密封类,它将失败,与具有非虚拟方法的类一样。
伊凡·丹尼洛夫

@IvanDanilov:有许多可以想象的约束,如果允许的话,将有效地阻止一些荒谬的结构。我同意对“任何接口类型”进行约束会很好,但是我看不到它会允许没有它而无法完成的任何事情,除非尝试进行时会产生编译时提示否则会在运行时失败的事情。
超级猫

113

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

typeof(T).IsInterface

11
+1是指出这一点的唯一答案。我只是添加了一个答案,提出了一种通过只检查一次而不是每次调用该方法一次来提高性能的方法。
phoog,2012年

9
C#中泛型的整个想法是具有编译时安全性。您建议的内容也可以使用非泛型方法执行Foo(Type type)
JacekGorgoń2014年

我喜欢运行时检查。谢谢。
塔里克欧扎贡棍儿

同样在运行时,您可以if (new T() is IMyInterface) { }用来检查接口是否由T类实现。可能不是最有效的,但它可以工作。
tkerwood '17

26

不,实际上,如果您正在思考classstruct表示classes和structs,则您错了。class任何引用类型(例如包括太接口)和struct任何类型的值(例如structenum)。


1
但是,这不是对类与结构之间的区别的定义:每个类都是引用类型(反之亦然),并且对于构造/值类型也是同上
Matthew Scharley,2009年

马修(Matthew):值类型比C#结构更多。例如,枚举是值类型和匹配where T : struct约束。
Mehrdad Afshari 2009年

值得注意的是,约束中使用的接口类型并不暗含class,但是声明接口类型的存储位置实际上是将存储位置声明为实现该类型的类引用。
2012年

4
为了更加精确,where T : struct对应于NotNullableValueTypeConstraint,因此它必须是以外的其他值类型Nullable<>。(所以Nullable<>是不满足where T : struct约束的结构类型。)
Jeppe Stig Nielsen 2013年

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


一个static构造函数不能public,所以这应该给一个编译时错误。此外,您的static类还包含一个实例方法,这也是一个编译时错误。
2013年

感谢nawfal纠正了@JeppeStigNielsen指出的错误-phoog
2015年

10

一段时间以来,我一直在考虑接近编译时的约束,因此这是启动该概念的绝佳机会。

基本思想是,如果无法执行检查编译时间,则应在最早的时间点(基本上是应用程序启动的那一刻)进行检查。如果所有检查都正确,则应用程序将运行;如果检查失败,则应用程序将立即失败。

行为

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

下一个最好的事情是程序在启动时崩溃。

最后一个选择是,程序将在命中代码后立即崩溃。这是.NET的默认行为。对我来说,这是完全不能接受的。

前提条件

我们需要有一个约束机制,所以对于缺少更好的东西……让我们使用属性。该属性将显示在通用约束之上,以检查其是否符合我们的条件。如果没有,我们将给出一个难看的错误。

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

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

(我将其保留在where T:class这里,因为我总是更喜欢编译时检查而不是运行时检查)

因此,这只剩下1个问题,那就是检查我们使用的所有类型是否都符合约束。它能有多难?

让我们分手吧

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

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

  1. 在类型中使用类型(继承,通用约束,类成员)时的编译时
  2. 在方法主体中使用类型时的编译时
  3. 运行时,当使用反射基于通用基类构造对象时。
  4. 运行时,当使用反射基于RTTI构建对象时。

在这一点上,我想指出,您应该始终避免在任何程序IMO中执行(4)。无论如何,这些检查将不支持它,因为这实际上意味着解决暂停问题。

情况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

例:

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

如果可能,我采用了这样的解决方案。仅当您希望将几个特定的​​接口(例如,您具有源访问权限的那些接口)作为通用参数而不是任何一个通用参数传递时,它才有效。

  • 我让出现问题的接口继承了一个空接口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
    }

这没有太大的作用。您T不受接口的限制,它受任何实现的对象的约束IInterface- 任何类型都可以执行此操作,例如,struct Foo : IInterface因为您IInterface很可能是公共的(否则接受它的所有内容都必须是内部的)。
AnorZaken

如果您控制所有想要接受的类型,则可以使用代码生成来创建所有合适的重载,所有这些重载都将重定向到通用的私有方法。
AnorZaken


2

我试图做类似的事情,并使用了一种变通办法:我考虑了结构上的隐式和显式运算符:想法是将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()

也许也与泛型有关,但是我没有尝试过

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


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>为非接口类型实现,但这很难做到。


-4

请改用抽象类。因此,您将获得以下内容:

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

10
因为C#不支持多重继承,所以不能总是用抽象类替换接口。
2013年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.