2009-05-11 16 views
24

Contexto: .NET 3.5, VS2008. No estoy seguro del título de esta pregunta, así que no dude en comentar sobre el título :-)¿Cómo puedo escribir una clase de contenedor genérica que implemente una interfaz dada en C#?

Aquí está el escenario: Tengo varias clases, digamos Foo and Bar, todas ellas implementan la siguiente interfaz :

public interface IStartable 
{ 
    void Start(); 
    void Stop(); 
} 

Y ahora me gustaría tener una clase de contenedor, que recibe un IEnumerable <IStartable> como un argumento en su constructor. Esta clase, a su vez, también debe implementar la interfaz IStartable:

public class StartableGroup : IStartable // this is the container class 
{ 
    private readonly IEnumerable<IStartable> startables; 

    public StartableGroup(IEnumerable<IStartable> startables) 
    { 
     this.startables = startables; 
    } 

    public void Start() 
    { 
     foreach (var startable in startables) 
     { 
      startable.Start(); 
     } 
    } 

    public void Stop() 
    { 
     foreach (var startable in startables) 
     { 
      startable.Stop(); 
     } 
    } 
} 

Así que mi pregunta es: ¿cómo puedo hacerlo sin tener que escribir manualmente el código, y sin generación de código? En otras palabras, me gustaría tener algo así como el siguiente.

var arr = new IStartable[] { new Foo(), new Bar("wow") }; 
var mygroup = GroupGenerator<IStartable>.Create(arr); 
mygroup.Start(); // --> calls Foo's Start and Bar's Start 

Restricciones:

  • Sin generación de código (es decir, ningún código textual real en tiempo de compilación)
  • La interfaz tiene sólo métodos vacíos, con o sin argumentos

Motivo:

  • Tengo una aplicación bastante grande, con muchos complementos de varias interfaces. escribir manualmente una clase de "contenedor de grupos" para cada interfaz "sobrecarga" del proyecto con las clases
  • manualmente escribir el código es propenso a errores
  • Cualquier adición o actualización de firmas a la interfaz IStartable dará lugar a cambios (manual) en el " grupo de contenedores" clase
  • Aprender

entiendo que tengo que utilizar la reflexión aquí, pero yo prefiero usar un marco sólido (como el castillo de DynamicProxy o RunSharp) para hacer el cableado para mí.

¿Alguna idea?

+0

¿Entonces * no * desea tener una clase StartableGroup? ¿Qué pasa con eso? – Noldorin

+0

¿Puedo preguntar: ¿por qué? ¿Cuál es el problema que esto necesita resolver? (Esto puede afectar la respuesta ...). –

+0

@Noldorin, @Marc Gravell, motivación añadida a la pregunta original. –

Respuesta

26

Esto no es bonito, pero parece que funciona:

public static class GroupGenerator 
{ 
    public static T Create<T>(IEnumerable<T> items) where T : class 
    { 
     return (T)Activator.CreateInstance(Cache<T>.Type, items); 
    } 
    private static class Cache<T> where T : class 
    { 
     internal static readonly Type Type; 
     static Cache() 
     { 
      if (!typeof(T).IsInterface) 
      { 
       throw new InvalidOperationException(typeof(T).Name 
        + " is not an interface"); 
      } 
      AssemblyName an = new AssemblyName("tmp_" + typeof(T).Name); 
      var asm = AppDomain.CurrentDomain.DefineDynamicAssembly(
       an, AssemblyBuilderAccess.RunAndSave); 
      string moduleName = Path.ChangeExtension(an.Name,"dll"); 
      var module = asm.DefineDynamicModule(moduleName, false); 
      string ns = typeof(T).Namespace; 
      if (!string.IsNullOrEmpty(ns)) ns += "."; 
      var type = module.DefineType(ns + "grp_" + typeof(T).Name, 
       TypeAttributes.Class | TypeAttributes.AnsiClass | 
       TypeAttributes.Sealed | TypeAttributes.NotPublic); 
      type.AddInterfaceImplementation(typeof(T)); 

      var fld = type.DefineField("items", typeof(IEnumerable<T>), 
       FieldAttributes.Private); 
      var ctor = type.DefineConstructor(MethodAttributes.Public, 
       CallingConventions.HasThis, new Type[] { fld.FieldType }); 
      var il = ctor.GetILGenerator(); 
      // store the items 
      il.Emit(OpCodes.Ldarg_0); 
      il.Emit(OpCodes.Ldarg_1); 
      il.Emit(OpCodes.Stfld, fld); 
      il.Emit(OpCodes.Ret); 

      foreach (var method in typeof(T).GetMethods()) 
      { 
       var args = method.GetParameters(); 
       var methodImpl = type.DefineMethod(method.Name, 
        MethodAttributes.Private | MethodAttributes.Virtual, 
        method.ReturnType, 
        Array.ConvertAll(args, arg => arg.ParameterType)); 
       type.DefineMethodOverride(methodImpl, method); 
       il = methodImpl.GetILGenerator(); 
       if (method.ReturnType != typeof(void)) 
       { 
        il.Emit(OpCodes.Ldstr, 
         "Methods with return values are not supported"); 
        il.Emit(OpCodes.Newobj, typeof(NotSupportedException) 
         .GetConstructor(new Type[] {typeof(string)})); 
        il.Emit(OpCodes.Throw); 
        continue; 
       } 

       // get the iterator 
       var iter = il.DeclareLocal(typeof(IEnumerator<T>)); 
       il.Emit(OpCodes.Ldarg_0); 
       il.Emit(OpCodes.Ldfld, fld); 
       il.EmitCall(OpCodes.Callvirt, typeof(IEnumerable<T>) 
        .GetMethod("GetEnumerator"), null); 
       il.Emit(OpCodes.Stloc, iter); 
       Label tryFinally = il.BeginExceptionBlock(); 

       // jump to "progress the iterator" 
       Label loop = il.DefineLabel(); 
       il.Emit(OpCodes.Br_S, loop); 

       // process each item (invoke the paired method) 
       Label doItem = il.DefineLabel(); 
       il.MarkLabel(doItem); 
       il.Emit(OpCodes.Ldloc, iter); 
       il.EmitCall(OpCodes.Callvirt, typeof(IEnumerator<T>) 
        .GetProperty("Current").GetGetMethod(), null); 
       for (int i = 0; i < args.Length; i++) 
       { // load the arguments 
        switch (i) 
        { 
         case 0: il.Emit(OpCodes.Ldarg_1); break; 
         case 1: il.Emit(OpCodes.Ldarg_2); break; 
         case 2: il.Emit(OpCodes.Ldarg_3); break; 
         default: 
          il.Emit(i < 255 ? OpCodes.Ldarg_S 
           : OpCodes.Ldarg, i + 1); 
          break; 
        } 
       } 
       il.EmitCall(OpCodes.Callvirt, method, null); 

       // progress the iterator 
       il.MarkLabel(loop); 
       il.Emit(OpCodes.Ldloc, iter); 
       il.EmitCall(OpCodes.Callvirt, typeof(IEnumerator) 
        .GetMethod("MoveNext"), null); 
       il.Emit(OpCodes.Brtrue_S, doItem); 
       il.Emit(OpCodes.Leave_S, tryFinally); 

       // dispose iterator 
       il.BeginFinallyBlock(); 
       Label endFinally = il.DefineLabel(); 
       il.Emit(OpCodes.Ldloc, iter); 
       il.Emit(OpCodes.Brfalse_S, endFinally); 
       il.Emit(OpCodes.Ldloc, iter); 
       il.EmitCall(OpCodes.Callvirt, typeof(IDisposable) 
        .GetMethod("Dispose"), null); 
       il.MarkLabel(endFinally); 
       il.EndExceptionBlock(); 
       il.Emit(OpCodes.Ret); 
      } 
      Cache<T>.Type = type.CreateType(); 
#if DEBUG  // for inspection purposes... 
      asm.Save(moduleName); 
#endif 
     } 
    } 
} 
+0

Me salvó escribir algo :-) –

+0

Creo que hay un pequeño error allí (no compilará): En lugar de: Caché .Type = type.CreateType(); Debe ser: Tipo = tipo.Crear tipo(); –

+0

Intenté el código sugerido, y parece que su respuesta no cubre los métodos con argumentos (vea la restricción "La interfaz tiene solo métodos vacíos, con o sin argumentos"). Actualmente hay una excepción cuando la interfaz incluye un método con un solo argumento. –

2

Usted podría subclase List<T> o alguna otra clase de colección y el uso de la where restricción de tipo genérico para limitar el tipo T ser sólo IStartable clases.

class StartableList<T> : List<T>, IStartable where T : IStartable 
{ 
    public StartableList(IEnumerable<T> arr) 
     : base(arr) 
    { 
    } 

    public void Start() 
    { 
     foreach (IStartable s in this) 
     { 
      s.Start(); 
     } 
    } 

    public void Stop() 
    { 
     foreach (IStartable s in this) 
     { 
      s.Stop(); 
     } 
    } 
} 

También podría declarar la clase así si no desea que sea una clase genérica que requiera un parámetro de tipo.

public class StartableList : List<IStartable>, IStartable 
{ ... } 

Su código de ejemplos de uso sería entonces algo como esto:

var arr = new IStartable[] { new Foo(), new Bar("wow") }; 
var mygroup = new StartableList<IStartable>(arr); 
mygroup.Start(); // --> calls Foo's Start and Bar's Start 
+1

Creo que no responde la pregunta. – DonkeyMaster

+0

@DonkeyMaster - No, no responde la pregunta exacta, pero creo que es una alternativa posible si entiendo la pregunta correctamente. Mi publicación ofrece una solución escrita a mano, la excelente muestra de Marc Gravell ofrece una solución de generación de código (en tiempo de ejecución). No sé si tendré una mano libre para hacerlo: el cartel original pidió una solución "sin escribir manualmente el código y sin generación de código". –

+0

De hecho, como señaló @DonkeyMaster, esto no responde la pregunta. Hace que el código sea más claro y quizás más elegante, pero la pregunta sigue siendo: ¿cómo puedo crear dicho código en tiempo de ejecución, sin tener que escribirlo (o generarlo) en el momento del diseño? –

0

Se podía esperar para C# 4.0 y utilizar el enlace dinámico.

Esta es una gran idea: he tenido que implementar esto para IDisposable en varias ocasiones; cuando quiero que se eliminen muchas cosas.Una cosa a tener en cuenta es cómo se manejarán los errores. Si se registra y sigue iniciando a otros, etc. Necesitará algunas opciones para impartir la clase.

No estoy familiarizado con DynamicProxy y cómo podría usarse aquí.

+0

C# 4.0 no estará aquí por un tiempo. ¡Ni siquiera hay una CTP disponible! – DonkeyMaster

0

Puede utilizar la clase "Lista" y su método "ForEach".

var startables = new List<IStartable>(array_of_startables); 
startables.ForEach(t => t.Start(); } 
+0

Esto es lo primero que se me vino a la mente también, pero está pidiendo una implementación de la clase anterior "GroupGenerator". –

0

Si entiendo correctamente, está solicitando una implementación del "GroupGenerator".

Sin ninguna experiencia real con CastleProxy mi recomendación sería utilizar GetMethods() para obtener los métodos iniciales enumerados en la interfaz y luego crear un nuevo tipo sobre la marcha usando Reflection.Emit con los nuevos métodos que enumeran a través de los objetos y llame a cada método correspondiente. El rendimiento no debería ser tan malo.

4

No es tan limpia una interfaz como la solución basada en la reflexión, sino una solución muy simple y flexible es crear un método ParaTodos como tal :

static void ForAll<T>(this IEnumerable<T> items, Action<T> action) 
{ 
    foreach (T item in items) 
    { 
     action(item); 
    } 
} 

y se puede llamar así:

arr.ForAll(x => x.Start()); 
2

Automapper es una buena solución para esto. Se basa en LinFu debajo para crear una instancia que implementa una interfaz, pero se encarga de parte de la hidratación, y se mezcla bajo una API fluida. El autor LinFu afirma que en realidad es mucho más liviano y más rápido que Castle 's Proxy.

+0

Gracias por el consejo, lo investigaré cuando tenga tiempo. –

Cuestiones relacionadas