26 Stimmen

Wie kann ich eine generische Containerklasse schreiben, die eine bestimmte Schnittstelle in C# implementiert?

Kontext: .NET 3.5, VS2008. Ich bin mir nicht sicher über den Titel dieser Frage, so fühlen sich frei, über den Titel zu kommentieren, auch :-)

Hier ist das Szenario: Ich habe mehrere Klassen, sagen wir Foo und Bar, alle von ihnen implementieren die folgende Schnittstelle:

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

Und nun möchte ich eine Containerklasse haben, die in ihrem Konstruktor ein IEnumerable<IStartable> als Argument bekommt. Diese Klasse wiederum soll auch die Schnittstelle IStartable implementieren:

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

Meine Frage ist also: Wie kann ich es tun, ohne den Code manuell zu schreiben und ohne Codegenerierung? Mit anderen Worten, ich hätte gerne so etwas wie das Folgende.

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

Sachzwänge:

  • Keine Codegenerierung (d. h. kein echter Textcode zur Kompilierzeit)
  • Die Schnittstelle hat nur ungültige Methoden, mit oder ohne Argumente

Motivation:

  • Ich habe eine ziemlich große Anwendung mit einer Vielzahl von Plugins mit verschiedenen Schnittstellen. Das manuelle Schreiben einer "Gruppencontainer"-Klasse für jede Schnittstelle "überlastet" das Projekt mit Klassen
  • Manuelles Schreiben des Codes ist fehleranfällig
  • Jede Ergänzung oder Signaturaktualisierung der IStartable-Schnittstelle führt zu (manuellen) Änderungen in der Klasse "Gruppencontainer".
  • Lernen

Ich verstehe, dass ich hier Reflexion verwenden muss, aber ich würde lieber ein robustes Framework verwenden (wie Castle's DynamicProxy o RunSharp ), um die Verkabelung für mich zu erledigen.

Haben Sie eine Idee?

28voto

Marc Gravell Punkte 970173

Das ist nicht schön, aber es scheint zu funktionieren:

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 Punkte 13528

Es ist nicht so eine saubere Schnittstelle wie die reflexionsbasierte Lösung, aber eine sehr einfache und flexible Lösung ist es, eine ForAll-Methode wie diese zu erstellen:

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

Und kann so genannt werden:

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

3voto

Brian Ensink Punkte 10994

Sie könnten die Unterklasse List<T> oder eine andere Sammelklasse und verwenden Sie die where generische Typbeschränkung zur Begrenzung der T Typ nur zu sein IStartable Klassen.

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

Sie könnten die Klasse auch so deklarieren, wenn Sie nicht wollen, dass sie eine generische Klasse ist, die einen Typparameter benötigt.

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

Ihr Beispielcode würde dann etwa so aussehen:

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 Punkte 17969

Automapper ist eine gute Lösung für dieses Problem. Sie stützt sich auf LinFu um eine Instanz zu erstellen, die eine Schnittstelle implementiert, aber es kümmert sich um einige der Hydration und Mixins unter einer etwas fließenden api. Die LinFu Der Autor behauptet, es sei tatsächlich viel leichter und schneller als Castle 's Proxy .

0voto

TheSoftwareJedi Punkte 33452

Sie könnten auf C# 4.0 warten und dynamische Bindungen verwenden.

Das ist eine großartige Idee - ich musste dies für IDisposable bei mehreren Gelegenheiten implementieren, wenn ich viele Dinge entsorgen wollte. Eine Sache, die man im Auge behalten sollte, ist, wie Fehler behandelt werden sollen. Soll es protokollieren und andere starten, etc... Sie müssten der Klasse einige Optionen geben.

Ich bin nicht mit DynamicProxy vertraut und weiß nicht, wie es hier eingesetzt werden könnte.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X