如何在C#中编写一个通用容器类来实现给定接口?

26

背景:.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

限制:

  • 不允许在编译时生成代码(也就是说,在编译时没有实际文本代码)
  • 接口只能有void方法,可以带或不带参数

动机:

  • 我有一个相当大的应用程序,有很多各种接口的插件。为每个接口手动编写“组容器”类会用类过多
  • 手动编写代码容易出错
  • 对IStartable接口的任何添加或签名更新都将导致“组容器”类的(手动)更改
  • 学习

我知道我必须在这里使用反射,但我宁愿使用强大的框架(如Castle的DynamicProxyRunSharp)来为我完成连线。

有什么想法吗?


那么你不想要一个StartableGroup类吗?它有什么问题吗? - Noldorin
@Noldorin,@Marc Gravell,原问题中添加了动机。 - Ron Klein
关于您对参数的评论 - 这很容易实现,但我可能需要将“foreach”展开到IL中。这需要一些反射工作。如果您需要,我可以填补空白,但不是此时此刻(忙碌了一个小时左右)。如果您需要,请告诉我。 - Marc Gravell
已更新以实现args;请注意,它尚未添加dispose的try/finally -稍后将添加。 - Marc Gravell
你所描述的是“组合”设计模式。可以通过动态代理和接口代理来实现。参考链接:http://en.wikipedia.org/wiki/Composite_pattern - Krzysztof Kozmic
显示剩余2条评论
7个回答

28

这不太美观,但似乎能够工作:

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

我认为你在那里犯了一个小错误(无法编译):
应该是:
Type = type.CreateType();
而不是:
Cache<T>.Type = type.CreateType();
- Ron Klein
我尝试了建议的代码,似乎你的答案没有涵盖带参数的方法(请参见约束“接口仅具有无论是否带参数的void方法”)。目前,当接口包含一个带单个参数的方法时会出现异常。 - Ron Klein
@Ron - 关于 "Type =" - 它们是相同的;我只是想避免与 System.Type 的歧义。 - Marc Gravell
这个解决方案非常接近我所寻找的。因此,对迄今为止的努力表示+1。如果涵盖了带参数的方法,我将非常乐意将其作为答案接受。再次感谢! - Ron Klein

4

这个解决方案并不像基于反射的方案那样干净,但是一个非常简单而灵活的解决方案是创建一个类似下面的 ForAll 方法:

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

可以这样调用:

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

3
您可以创建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

1
我认为那并没有回答问题。 - DonkeyMaster
@DonkeyMaster - 不,它并没有回答确切的问题,但如果我正确理解了问题,我认为这是一个可能的替代方案。我的帖子提供了一个手动编写的解决方案,Marc Gravell的优秀示例提供了一个(运行时)代码生成解决方案。我不知道有没有一种方法可以在不手动编写代码和不使用代码生成的情况下完成它:原始发布者要求提供这样的解决方案。 - Brian Ensink
正如@DonkeyMaster所指出的那样,这并没有回答问题。它使代码更清晰、更优美,但问题仍然存在:我如何在运行时创建这样的代码,而不必在设计时编写它(或生成它)? - Ron Klein

2
"Automapper是一个很好的解决方案。它依赖于LinFu来创建实现接口的实例,但它会处理一些数据填充,并在较为流畅的API下混合它们。LinFu的作者声称它比Castle的代理更加轻量级和快速。"

谢谢你的建议,我有时间会去研究一下。 - Ron Klein

0
你可以使用 "List" 类及其方法 "ForEach"。
var startables = new List<IStartable>( array_of_startables );
startables.ForEach( t => t.Start(); }

这也是我首先想到的 - 但他要求实现上面的“GroupGenerator”类。 - Robert Venables

0

你可以等待C# 4.0并使用动态绑定。

这是一个很好的想法 - 我曾经在几个场合下实现了IDisposable; 当我想要许多东西被处理时。但需要记住的一件事是如何处理错误。应该记录并继续启动其他操作,等等...您需要为类提供一些选项。

我不熟悉DynamicProxy以及它在这里的用法。


C# 4.0 还需要一段时间才能到来。甚至还没有发布 CTP! - DonkeyMaster

0

如果我理解正确,您正在要求实现“GroupGenerator”。

没有使用CastleProxy的实际经验,我的建议是使用GetMethods()获取接口中列出的初始方法,然后使用Reflection.Emit动态创建一个新类型,其中包含枚举对象并调用每个相应方法的新方法。性能不应该太差。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接