ChatGPT解决这个技术问题 Extra ChatGPT

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

Is there a way to get the following function declaration?

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

ie. where T is an interface type (similar to where T : class, and struct).

Currently I've settled for:

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

Where IBase is defined as an empty interface that is inherited by all my custom interfaces... Not ideal, but it should work... Why can't you define that a generic type must be an interface?

For what it's worth, I want this because Foo is doing reflection where it needs an interface type... I could pass it in as a normal parameter and do the necessary checking in the function itself, but this seemed a lot more typesafe (and I suppose a little more performant, since all the checks are done at compiletime).

Actually, your IBase dea is the best I've seen so far. Unfortunately, you can't use it for interfaces you don't own. All C# would have to do is have all interfaces inherit from IOjbect just like all classes inherit from Object.
Note: This happens to be a rather common idea. Empty interfaces like IBase — used in this way — are called marker interfaces. They enable special behaviours for 'marked' types.
Other note : According to Interface design guidelines using marker interfaces should be avoided

M
Marc Gravell

The closest you can do (except for your base-interface approach) is "where T : class", meaning reference-type. There is no syntax to mean "any interface".

This ("where T : class") is used, for example, in WCF to limit clients to service contracts (interfaces).


nice answer, but do you have any idea why this syntax doesn't exist? Seems like it would be a nice-to-have feature.
@StephenHolt: I think think the creators of .NET, in deciding what constraints to allow, were focused on ones that would let generic classes and methods do things with generic types that they otherwise could not, rather than on preventing them from being used in nonsensical ways. That having been said, an interface constraint on T should allow reference comparisons between T and any other reference type, since reference comparisons are allowed between any interface and almost any other reference type, and allowing comparisons even that case would pose no problem.
@supercat another useful application of such hypothetical constraint would be to safely create a proxy for instances of the type. For interface it is guaranteed to be safe, while for sealed classes it would fail, same as for classes with non-virtual methods.
@IvanDanilov: There are a number of conceivable constraints which, if allowed, would usefully block some nonsensical constructs. I agree a constraint for "any interface type" would be nice, but I don't see that it would allow for anything that couldn't be done without it, save for the generation of compile-time squawks when attempts are made to do things that could otherwise fail at runtime.
R
Robert

I know this is a bit late but for those that are interested you can use a runtime check.

typeof(T).IsInterface

+1 for being the only answer to point this out. I just added an answer with an approach to improve the performance by checking each type only once rather than every time the method is called.
The whole idea of generics in C# is to have compile-time safety. What you're suggesting can just as well be performed with a non-generic method Foo(Type type).
I like the runtime check. Thanks.
Also at run time you can use if (new T() is IMyInterface) { } to check if an interface is implemented by the T class. Might not be the most efficient, but it works.
m
mmx

No, actually, if you are thinking class and struct mean classes and structs, you're wrong. class means any reference type (e.g. includes interfaces too) and struct means any value type (e.g. struct, enum).


Isn't that the definition of the difference between a class and a struct though: that every class is a reference type (and vice versa) and ditto for stuct/value types
Matthew: There are more to value types than C# structs. Enums, for instance are value types and match where T : struct constraint.
It's worth noting that an interface types used in constraints do not imply class, but declaring a storage location of an interface type really declares the storage location to be a class reference which implement that type.
To be even more precise, where T : struct corresponds to NotNullableValueTypeConstraint, so it means it must be a value type other than Nullable<>. (So Nullable<> is a struct type which doesn't satisfy the where T : struct constraint.)
p
phoog

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.


A static constructor can't be public, so this should give a compile-time error. Also your static class contains an instance method, that's a compile-time error as well.
Belated thanks to nawfal for correcting the errors noted by @JeppeStigNielsen
a
atlaste

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:

Compile-time, when using a type in a type (inheritance, generic constraint, class member) Compile-time, when using a type in a method body Run-time, when using reflection to construct something based on the generic base class. 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();
    }
}

Instead of static on-app-start, you can even move it to before the app starts in the unit tests, which strays just a little further away from runtime.
P
Pavel Minaev

You cannot do this in any released version of C#, nor in the upcoming C# 4.0. It's not a C# limitation, either - there's no "interface" constraint in the CLR itself.


R
Ray

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 where T : IInterface { // Actual world peace generating code }


This doesn't accomplish much. Your T is not constrained to interfaces, it's constrained to anything that implements IInterface - which any type can do if it wants to, e.g. struct Foo : IInterface since your IInterface is most likely public (otherwise everything that accepts it would have to be internal).
If you control all types you want to accept anyway, then you can use code-generation to create all suitable overloads, all of which just redirects to a generic private method.
K
KevinDeus

What you have settled for is the best you can do:

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

C
Charles HETIER

I tried to do something similar and used a workaround solution: I thought about implicit and explicit operator on structure: The idea is to wrap the Type in a structure that can be converted into Type implicitly.

Here is such a structure:

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

}

basic usage:

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

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

You have to imagine your own mecanism around this, but an example could be a method taken a InterfaceType in parameter instead of a type

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

A method to override that should returns interface types:

public virtual IEnumerable<InterfaceType> GetInterfaces()

There are maybe things to do with generics also, but I didn't tried

Hope this can help or gives ideas :-)


t
thatguy

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.


j
j0k

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

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

You can't always replace an interface with an abstract class since C# doesn't support multiple inheritance.