Как я могу написать общий контейнерный класс, реализующий данный интерфейс на C #?

Контекст: .NET 3.5, VS2008. Я не уверен в названии этого вопроса, поэтому не стесняйтесь комментировать и название :-)

Вот сценарий: у меня есть несколько классов, скажем Foo и Bar, все они реализуют следующий интерфейс:

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

А теперь я хотел бы иметь класс контейнера, который получает IEnumerable ‹IStartable› в качестве аргумента в своем конструкторе. Этот класс, в свою очередь, также должен реализовывать интерфейс 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();
        }
    }
}

Итак, мой вопрос: как я могу это сделать без написания кода вручную и без генерации кода? Другими словами, я хотел бы иметь что-то вроде следующего.

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

Ограничения:

  • Без генерации кода (то есть без реального текстового кода во время компиляции)
  • В интерфейсе есть только недействительные методы, с аргументами или без них.

Мотивация:

  • У меня довольно большое приложение, с множеством плагинов с разными интерфейсами. Написание класса «группового контейнера» для каждого интерфейса вручную «перегружает» проект классами.
  • Написание кода вручную чревато ошибками
  • Любые дополнения или обновления сигнатур в интерфейсе IStartable приведут к (ручным) изменениям в классе «группового контейнера».
  • Обучение

Я понимаю, что здесь мне нужно использовать отражение, но я бы предпочел использовать надежную структуру (например, DynamicProxy от Castle или RunSharp), чтобы выполнить подключение за меня.

Есть предположения?


person Ron Klein    schedule 11.05.2009    source источник
comment
Итак, вы не хотите иметь класс StartableGroup? Что с этим не так?   -  person Noldorin    schedule 11.05.2009
comment
Могу я спросить, почему? Какую проблему необходимо решить? (это может повлиять на ответ ...).   -  person Marc Gravell    schedule 11.05.2009
comment
@Noldorin, @Marc Gravell, мотивация добавлена ​​к исходному вопросу.   -  person Ron Klein    schedule 11.05.2009
comment
Повторите свой комментарий к аргументам - это делается достаточно легко, но мне, вероятно, придется развернуть foreach в IL. Это немного работы с отражателем. Я, вероятно, смогу заполнить пробелы, если вам нужно, но не прямо сейчас (занято около часа). Дайте мне знать, цените ли вы это.   -  person Marc Gravell    schedule 11.05.2009
comment
Обновлено для реализации аргументов; обратите внимание, что у него еще нет try / finally для удаления - добавлю позже.   -  person Marc Gravell    schedule 12.05.2009
comment
Добавлен try / finally и немного обработки ошибок (т.е. он знает, что делать, если вы дадите ему что-то, что не является интерфейсом или методами с возвращаемыми значениями).   -  person Marc Gravell    schedule 12.05.2009
comment
то, что вы описываете, является «составным» шаблоном проектирования en.wikipedia.org/wiki/Composite_pattern Этого довольно легко добиться с помощью DynamicProxy и InterfaceProxyWithTargetInterface.   -  person Krzysztof Kozmic    schedule 09.06.2009


Ответы (7)


Это некрасиво, но вроде работает:

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
        }
    }
}
person Marc Gravell    schedule 11.05.2009
comment
Я думаю, что у вас там небольшая ошибка (не компилируется): Вместо: Cache ‹T› .Type = type.CreateType (); Должно быть: Type = type.CreateType (); - person Ron Klein; 11.05.2009
comment
Я попробовал предложенный код, и кажется, что ваш ответ не касается методов с аргументами (см. Ограничение. Интерфейс имеет только недействительные методы с аргументами или без них). В настоящее время существует исключение, когда интерфейс включает метод с одним аргументом. - person Ron Klein; 11.05.2009
comment
@Ron - re Type = - они идентичны; Я просто хотел избежать двусмысленности с System.Type - person Marc Gravell; 11.05.2009
comment
Это решение очень близко к тому, что я ищу. Итак, +1 за все усилия. Я буду более чем счастлив принять это как ответ, если будут рассмотрены методы с аргументами. Еще раз спасибо! - person Ron Klein; 12.05.2009

Это не такой чистый интерфейс, как решение на основе отражения, но очень простое и гибкое решение - создать такой метод ForAll:

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

И может называться так:

arr.ForAll(x => x.Start());
person ICR    schedule 11.05.2009

Вы можете создать подкласс List<T> или какой-либо другой класс коллекции и использовать ограничение общего типа where, чтобы ограничить тип T только IStartable классами.

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

Вы также можете объявить такой класс, если не хотите, чтобы он был универсальным классом, требующим параметра типа.

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

Тогда ваш пример кода использования будет выглядеть примерно так:

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
person Brian Ensink    schedule 11.05.2009
comment
Думаю, это не ответ на вопрос. - person DonkeyMaster; 11.05.2009
comment
@DonkeyMaster - Нет, он не отвечает на точный вопрос, но я думаю, что это возможная альтернатива, если я правильно понял вопрос. Мой пост предлагает написанное вручную решение, отличный образец Марка Гравелла предлагает решение для генерации кода (во время выполнения). Я не знаю, как обойтись без них: исходный плакат просил найти решение без ручного написания кода и без генерации кода. - person Brian Ensink; 11.05.2009
comment
Действительно, как заметил @DonkeyMaster, это не отвечает на вопрос. Это делает код более ясным и, возможно, более элегантным, но остается вопрос: как я могу создать такой код во время выполнения, без необходимости его писать (или генерировать) во время разработки? - person Ron Klein; 12.05.2009

Automapper - хорошее решение этой проблемы. Он полагается на LinFu внизу, чтобы создать экземпляр, реализующий интерфейс, но он заботится о некоторой гидратации и смешивается под несколько плавным api. Автор LinFu утверждает, что на самом деле он намного легче и быстрее, чем Proxy Castle.

person Maslow    schedule 19.11.2009
comment
Спасибо за подсказку, я разберусь, когда у меня будет время. - person Ron Klein; 20.11.2009

Вы можете дождаться C # 4.0 и использовать динамическую привязку.

Это отличная идея - мне несколько раз приходилось реализовывать это для IDisposable; когда я хочу избавиться от многих вещей. Однако следует помнить о том, как будут обрабатываться ошибки. Если он войдет в систему и продолжит запускать другие, и т. Д. Вам понадобятся некоторые параметры, чтобы дать классу.

Я не знаком с DynamicProxy и тем, как его можно здесь использовать.

person TheSoftwareJedi    schedule 11.05.2009
comment
C # 4.0 скоро не будет. Еще нет даже ОСАГО! - person DonkeyMaster; 11.05.2009

Вы можете использовать класс «Список» и их метод «ForEach».

var startables = new List<IStartable>( array_of_startables );
startables.ForEach( t => t.Start(); }
person TcKs    schedule 11.05.2009
comment
Это первое, что пришло мне в голову, но он просит реализовать вышеупомянутый класс GroupGenerator. - person Robert Venables; 11.05.2009

Если я правильно понимаю, вы запрашиваете реализацию «GroupGenerator».

Без какого-либо реального опыта работы с CastleProxy я бы порекомендовал использовать GetMethods () для получения начальных методов, перечисленных в интерфейсе, а затем создать новый тип на лету с помощью Reflection.Emit с новыми методами, которые перечисляют объекты и вызывают каждый соответствующий метод. Производительность не должна быть такой уж плохой.

person Robert Venables    schedule 11.05.2009