How can I write a generic container class that imp

2019-01-10 19:45发布

问题:

Context: .NET 3.5, VS2008. I'm not sure about the title of this question, so feel free to comment about the title, too :-)

Here's the scenario: I have several classes, say Foo and Bar, all of them implement the following interface:

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

And now I'd like to have a container class, which gets an IEnumerable<IStartable> as an argument in its constructor. This class, in turn, should also implement the IStartable interface:

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

So my question is: how can I do it without manually writing the code, and without code generation? In other words, I'd like to have somethig like the following.

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

Constraints:

  • No code generation (that is, no real textual code at compile time)
  • The interface has only void methods, with or without arguments

Motivation:

  • I have a pretty large application, with a lot of plugins of various interfaces. Manually writing a "group container" class for each interface "overloads" the project with classes
  • Manually writing the code is error prone
  • Any additions or signature updates to the IStartable interface will lead to (manual) changes in the "group container" class
  • Learning

I understand that I have to use reflection here, but I'd rather use a robust framework (like Castle's DynamicProxy or RunSharp) to do the wiring for me.

Any thoughts?

回答1:

This isn't pretty, but it seems to work:

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


回答2:

It's not as clean an interface as the reflection based solution, but a very simple and flexible solution is to create a ForAll method like so:

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

And can be called like so:

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


回答3:

You could subclass List<T> or some other collection class and use the where generic type constraint to limit the T type to be only IStartable classes.

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

You could also declare the class like this if you didn't want it to be a generic class requiring a type parameter.

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

Your sample usage code would then look something like this:

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


回答4:

Automapper is a good solution to this. It relies on LinFu underneath to create an instance that implements an interface, but it takes care of some of the hydration, and mixins under a somewhat fluent api. The LinFu author claims it is actually much more lightweight and faster than Castle's Proxy.



回答5:

You could wait for C# 4.0 and use dynamic binding.

This is a great idea - I've had to implement this for IDisposable on several occasions; when I want many things to be disposed. One thing to keep in mind though is how errors will be handled. Should it log and keep starting others, etc... You'd need some options to give the class.

I'm not familiar with DynamicProxy and how it could be used here.



回答6:

You can use the "List" class and their method "ForEach".

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


回答7:

If I understand correctly, you are asking for an implementation of the "GroupGenerator".

Without any real experience with CastleProxy my recommendation would be to use GetMethods() to get the initial methods listed in the interface and then create a new type on the fly using Reflection.Emit with the new methods that enumerate through the objects and call each corresponding method. The performance shouldn't be too bad.