25 votos

¿Cómo puedo escribir un contenedor genérico de la clase que implementa una interfaz determinada en C#?

Contexto: .NET 3.5, VS2008. No estoy seguro sobre el título de esta pregunta, así que siéntete libre de comentar sobre el título, también :-)

Aquí está el escenario: tengo varias clases, dicen los Foo y Bar, todos ellos implementar la interfaz siguiente:

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

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

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 manualmente a escribir el código, y sin la generación de código? En otras palabras, me gustaría tener algo como la 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:

  • Ninguna de generación de código (es decir, no real textual de código en tiempo de compilación)
  • La interfaz tiene sólo métodos void, con o sin argumentos

Motivación:

  • Tengo una bastante grande, con una gran cantidad de plugins de diferentes interfaces. Manual de escritura de un grupo de "contenedor" de la clase para cada interfaz "sobrecargas" el proyecto con las clases
  • Manualmente escribiendo el código es propensa a errores
  • Las adiciones o actualizaciones de firmas para la IStartable interfaz conducirá a (manual) cambios en el grupo de "contenedor" de la clase
  • El aprendizaje

Yo entiendo que tengo el uso de 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í.

Los pensamientos?

28voto

Marc Gravell Puntos 482669

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

4voto

ICR Puntos 6960

No es tan limpio de una interfaz como la de reflexión, basado en la solución, pero de una forma muy sencilla y flexible solución es crear un método ForAll así:

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

2voto

Brian Ensink Puntos 7579

Usted podría subclase List<T> o alguna otra clase de colección y el uso de la where genérico tipo de restricción para limitar la T tipo de ser sólo IStartable de las 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();
        }
    }
}

Usted también podría declarar la clase como esto si no quieren que sea una clase genérica que requieren un parámetro de tipo.

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

Su ejemplo de uso de código sería 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

2voto

Maslow Puntos 7268

Automapper es una buena solución para esto. Se basa en LinFu por debajo para crear una instancia que implementa una interfaz, sino que se ocupa de algunas de la hidratación, y mixins bajo un poco la fluidez de la api. El LinFu autor afirma que en realidad es mucho más ligero y más rápido que Castle's Proxy.

0voto

TheSoftwareJedi Puntos 15921

Usted puede esperar a C# 4.0 y el uso de enlace dinámico.

Esta es una gran idea - he tenido que hacer esto para IDisposable en varias ocasiones; cuando quiero muchas cosas para ser eliminados. Una cosa a tener en cuenta, sin embargo es la forma en que los errores serán manejados. Debe de registro y mantener a partir de otros, etc... tendría algunas opciones para dar la clase.

Yo no estoy familiarizado con DynamicProxy y cómo podría utilizarse aquí.

Iteramos.com

Iteramos es una comunidad de desarrolladores que busca expandir el conocimiento de la programación mas allá del inglés.
Tenemos una gran cantidad de contenido, y también puedes hacer tus propias preguntas o resolver las de los demás.

Powered by:

X