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

有没有办法得到下面的函数声明?

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

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

目前我已经接受了:

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

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

无论如何,我希望这样做是因为 Foo在需要接口类型的地方进行反射... ... 我可以将它作为一个普通参数传递进去,并在函数本身中执行必要的检查,但这似乎更加安全(我认为性能更好一些,因为所有检查都是在编译时完成的)。

141330 次浏览

不,实际上,如果你认为 classstruct意味着 classstruct,你就错了。class表示 任何引用类型(例如也包括接口) ,struct表示 任何值类型(例如 structenum)。

您不能在任何已发布的 C # 版本中这样做,也不能在即将发布的 C # 4.0中这样做。这也不是 C # 的限制—— CLR 本身没有“接口”约束。

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

例如,在 WCF 中使用这个(“ where T : class”)将客户机限制为服务契约(接口)。

使用一个抽象类代替:

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

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

typeof(T).IsInterface

你已经安顿下来的是你能做的最好的事情:

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

要进一步了解 Robert 的答案,这还要晚一些,但是您可以使用静态 helper 类对每个类型进行一次运行时检查:

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<Actual>();

毕竟,Actual类满足 IBase约束。

我尝试做一些类似的事情,并使用了一个变通解决方案: 我考虑了结构上的隐式和显式操作符: 其思想是将 Type 包装在一个可以隐式转换为 Type 的结构中。

下面是这样一种结构:

Public struct 接口类型 { 私有类型;

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()

也许还有一些与泛型有关的事情,但我没有尝试

希望这能有所帮助或提供一些想法: -)

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

  • 我让我的接口继承了一个空接口 IInterface
  • 我将通用 T 参数约束为 IInterface

源代码是这样的:

  • 希望作为泛型参数传递的任何接口:

    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
    }
    

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

基本思想是,如果不能执行检查编译时间,则应该在尽可能早的时间点执行,基本上就是应用程序启动的时刻。如果所有检查都正常,应用程序将运行; 如果检查失败,应用程序将立即失败。

行为

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

接下来最好的事情就是程序一旦启动就会崩溃。

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

先决条件

我们需要一个约束机制,因此,如果没有更好的东西... ... 让我们使用一个属性。该属性将出现在一般约束的顶部,以检查它是否与我们的条件相匹配。如果没有,我们就会出现一个丑陋的错误。

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

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

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

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

我们分手吧

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

触发约束需要执行下列操作之一:

  1. 在类型(继承、泛型约束、类成员)中使用类型时的编译时
  2. 编译时,当在方法体中使用类型时
  3. 运行时,当使用反射基于泛型基类构造某些内容时。
  4. 运行时,当使用反射来构造基于 RTTI 的内容时。

在这一点上,我想声明,你应该始终避免做(4)在任何程序 IMO。无论如何,这些检查都不会支持它,因为它实际上意味着解决了停止问题。

案例1: 使用类型

例如:

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

例二:

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

我认为理论上可以使用与 case (2)类似的技巧来检查这一点,但是实现它要困难得多(您需要检查在某个代码路径中是否调用了 MakeGenericType)。我不会在这里详细说明..。

案例4: 反射,运行时 RTTI

例如:

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

恕我直言,这是最糟糕的情况,正如我之前解释的那样,这通常是一个糟糕的主意。不管怎样,没有实际的方法可以用支票来解决这个问题。

测试一下

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

解决方案 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: 如果不想让 struct 作为见证者,可以创建一个接口

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

并使用一个约束:

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

接口的实现:

interface IA :ISInterface<IA>{ }

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