restricciones genericas clases c# generics interface constraints

genericas - interface where t class c#



¿Cómo puedo usar la interfaz como una restricción de tipo genérico C#? (10)

Desde hace un tiempo, he estado pensando en restricciones de tiempo de compilación cercana, por lo que esta es una oportunidad perfecta para lanzar el concepto.

La idea básica es que si no puede hacer una comprobación de tiempo de compilación, debe hacerlo en el momento más temprano posible, que es básicamente el momento en que comienza la aplicación. Si todos los controles están correctos, la aplicación se ejecutará; si falla un control, la aplicación fallará instantáneamente.

Comportamiento

El mejor resultado posible es que nuestro programa no compila si las restricciones no se cumplen. Desafortunadamente eso no es posible en la implementación actual de C #.

Lo mejor es que el programa se bloquea en el momento en que se inicia.

La última opción es que el programa se bloqueará en el momento en que se golpee el código. Este es el comportamiento predeterminado de .NET. Para mí, esto es completamente inaceptable.

Pre requisitos

Necesitamos tener un mecanismo de restricción, entonces por la falta de algo mejor ... usemos un atributo. El atributo estará presente además de una restricción genérica para verificar si coincide con nuestras condiciones. Si no es así, damos un feo error.

Esto nos permite hacer cosas como esta en nuestro código:

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

(He guardado aquí la where T:class , porque siempre prefiero los controles en tiempo de compilación a los controles en tiempo de ejecución)

Entonces, eso solo nos deja con un problema, que es verificar si todos los tipos que usamos coinciden con la restricción. ¿Qué tan difícil puede ser?

Vamos a dividirlo

Los tipos genéricos siempre están en una clase (/ struct / interface) o en un método.

Para activar una restricción, debe hacer una de las siguientes cosas:

  1. Tiempo de compilación, cuando se usa un tipo en un tipo (herencia, restricción genérica, miembro de la clase)
  2. Tiempo de compilación, cuando se usa un tipo en un cuerpo de método
  3. Tiempo de ejecución, cuando usa la reflexión para construir algo basado en la clase base genérica.
  4. Tiempo de ejecución, cuando usa la reflexión para construir algo basado en RTTI.

En este punto, me gustaría declarar que siempre debe evitar hacer (4) en cualquier programa IMO. De todos modos, estas comprobaciones no lo respaldarán, ya que efectivamente significaría resolver el problema de detención.

Caso 1: usando un tipo

Ejemplo:

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

Ejemplo 2:

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

Básicamente esto implica escanear todos los tipos, herencia, miembros, parámetros, etc., etc., etc. Si un tipo es un tipo genérico y tiene una restricción, verificamos la restricción; si es una matriz, verificamos el tipo de elemento.

En este punto, debo agregar que esto romperá el hecho de que, por defecto, .NET carga tipos ''flojos''. Al escanear todos los tipos, forzamos el tiempo de ejecución .NET para cargarlos todos. Para la mayoría de los programas, esto no debería ser un problema; aún así, si usa inicializadores estáticos en su código, puede encontrar problemas con este enfoque ... Dicho esto, no aconsejaría a nadie que haga esto de todos modos (excepto por cosas como esta :-), por lo que no debería dar tienes muchos problemas

Caso 2: usar un tipo en un método

Ejemplo:

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

Para comprobar esto, solo tenemos una opción: descompilar la clase, verificar todos los tokens de miembro que se usan y si uno de ellos es del tipo genérico: compruebe los argumentos.

Caso 3: Reflexión, construcción genérica en tiempo de ejecución

Ejemplo:

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

Supongo que teóricamente es posible verificar esto con trucos similares a caso (2), pero su implementación es mucho más difícil (debe verificar si se llama a MakeGenericType en alguna ruta de código). No entraré en detalles aquí ...

Caso 4: Reflexión, tiempo de ejecución RTTI

Ejemplo:

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

Este es el peor de los casos y como he explicado antes, generalmente una mala idea en mi humilde opinión. De cualquier manera, no hay una manera práctica de resolver esto usando cheques.

Probando el lote

Crear un programa que pruebe los casos (1) y (2) dará como resultado algo como esto:

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

Usando el código

Bueno, esa es la parte fácil :-)

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

¿Hay alguna manera de obtener la siguiente declaración de función?

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

es decir. donde T es un tipo de interfaz (similar a where T : class y struct ).

Actualmente me he conformado con:

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

Donde IBase se define como una interfaz vacía que hereda todas mis interfaces personalizadas ... No es ideal, pero debería funcionar ... ¿Por qué no se puede definir que un tipo genérico debe ser una interfaz?

Por lo que vale, quiero esto porque Foo está haciendo una reflexión donde necesita un tipo de interfaz ... Podría pasarlo como un parámetro normal y hacer la comprobación necesaria en la función en sí, pero esto parecía mucho más seguro (y Supongo que un poco más eficiente, ya que todos los controles se realizan en tiempo de compilación).


Lo más cerca que puede hacer (excepto por su enfoque de interfaz base) es " where T : class ", que significa tipo de referencia. No hay sintaxis para significar "cualquier interfaz".

Esto (" where T : class ") se usa, por ejemplo, en WCF para limitar clientes a contratos de servicio (interfaces).


Lo que ha resuelto es lo mejor que puede hacer:

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


No puede hacer esto en ninguna versión lanzada de C #, ni en la próxima C # 4.0. Tampoco es una limitación de C #; no existe una restricción de "interfaz" en el CLR.


No, en realidad, si estás pensando en class y struct significa class y struct , estás equivocado. class significa cualquier tipo de referencia (por ejemplo, también incluye interfaces) y struct significa cualquier tipo de valor (por ejemplo, struct , enum ).


Para realizar un seguimiento de la respuesta de Robert, esto es aún más tarde, pero puede usar una clase auxiliar estática para hacer que el control del tiempo de ejecución solo sea una vez por tipo:

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() { /*...*/ } }

También observo que su solución "debería funcionar" no funciona, de hecho. Considerar:

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

Ahora no hay nada que te impida llamar a Foo de esta manera:

Foo<Actual>();

La clase Actual , después de todo, cumple con la restricción IBase .


Sé que esto es un poco tarde, pero para aquellos que estén interesados, puedes usar un control en tiempo de ejecución.

typeof(T).IsInterface


Si es posible, fui con una solución como esta. Solo funciona si desea que varias interfaces específicas (por ejemplo, aquellas a las que tiene acceso de origen) se pasen como un parámetro genérico, no cualquiera.

  • Dejé que mis interfaces, que entraron en cuestión, heredaran una interfaz vacía en la interfaz.
  • IInterface parámetro genérico T para que fuera de la IInterface

En origen, se ve así:

  • Cualquier interfaz que desee pasar como parámetro genérico:

    public interface IWhatever : IInterface { // IWhatever specific declarations }

  • IInterface:

    public interface IInterface { // Nothing in here, keep moving }

  • La clase en la que desea poner la restricción de tipo:

    public class WorldPeaceGenerator<T> where T : IInterface { // Actual world peace generating code }


Traté de hacer algo similar y usé una solución alternativa: pensé en el operador implícito y explícito en la estructura: la idea es envolver el Tipo en una estructura que se pueda convertir en Tipo implícitamente.

Aquí hay tal estructura:

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

}

uso básico:

// OK InterfaceType type1 = typeof(System.ComponentModel.INotifyPropertyChanged); // Throws an exception InterfaceType type2 = typeof(WeakReference);

Tienes que imaginar tu propio mecanismo alrededor de esto, pero un ejemplo podría ser un método tomado un InterfaceType en parámetro en lugar de un tipo

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

Un método para anular eso debería devolver tipos de interfaz:

public virtual IEnumerable<InterfaceType> GetInterfaces()

También hay cosas que hacer con genéricos, pero no lo intenté

Espero que esto pueda ayudar o da ideas :-)


Usa una clase abstracta en su lugar. Entonces, tendrías algo como:

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