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

Posted

技术标签:

【中文标题】如何在 C# 中编写实现给定接口的通用容器类?【英文标题】:How can I write a generic container class that implements a given interface in C#? 【发布时间】:2010-10-25 06:30:34 【问题描述】:

上下文:.NET 3.5,VS2008。我不确定这个问题的标题,所以也可以对标题发表评论:-)

场景如下:我有几个类,比如 Foo 和 Bar,它们都实现了以下接口:

public interface IStartable

    void Start();
    void Stop();

现在我想要一个容器类,它在其构造函数中获取一个 IEnumerable 作为参数。反过来,这个类也应该实现 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 的 DynamicProxy 或 RunSharp)来为我进行布线。

有什么想法吗?

【问题讨论】:

所以你想要一个 StartableGroup 类?它有什么问题? 请问:为什么?这需要解决什么问题? (这可能会影响答案...)。 @Noldorin,@Marc Gravell,在原始问题中添加了动机。 重新评论您对参数的评论 - 它很容易完成,但我可能不得不将“foreach”展开到 IL 中。这是一个反射器的工作。如果你需要,我可能会填空,但不是现在(忙了一个小时左右)。让我知道你是否重视这个。 已更新以实现 args;请注意,它还没有尝试/最终处置 - 稍后会添加。 【参考方案1】:

这不是很漂亮,但它似乎工作:

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
        
    

【讨论】:

我认为你有一个小错误(不会编译):而不是:Cache.Type = type.CreateType();应该是:Type = type.CreateType(); 我尝试了建议的代码,但您的答案似乎不包括带参数的方法(请参阅约束“接口只有 void 方法,带或不带参数”)。目前,当接口包含具有单个参数的方法时会出现异常。 @Ron - re "Type =" - 它们是相同的;我只是想避免 System.Type 的歧义 这个解决方案非常接近我正在寻找的。所以到目前为止的努力+1。如果包含带参数的方法,我将非常乐意接受这个作为答案。再次感谢!【参考方案2】:

它不像基于反射的解决方案那样干净的界面,但一个非常简单和灵活的解决方案是创建一个 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&lt;T&gt; 或其他一些集合类,并使用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

【讨论】:

我认为这不能回答问题。 @DonkeyMaster - 不,它没有回答确切的问题,但我认为如果我正确理解了这个问题,它是一个可能的选择。我的帖子提供了一个手动编写的解决方案,Marc Gravell 的优秀示例提供了一个(运行时)代码生成解决方案。我不知道没有任何一种方法可以做到这一点:原始发布者要求“无需手动编写代码且无需代码生成”的解决方案。 确实,正如@DonkeyMaster 所指出的,这并不能回答问题。它使代码更清晰,也许更优雅,但问题仍然存在:如何在运行时创建这样的代码,而不必在设计时编写(或生成)?【参考方案4】:

Automapper 是一个很好的解决方案。它依赖于下面的LinFu 来创建一个实现接口的实例,但它负责一些水化,并在一个有点流利的api下混合。 LinFu 作者声称它实际上比 CastleProxy 更轻量级和更快。

【讨论】:

感谢您的提示,有时间我会研究一下。【参考方案5】:

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

这是一个好主意——我不得不多次为 IDisposable 实现它;当我想要处理很多东西时。不过要记住的一件事是如何处理错误。如果它记录并继续启动其他人,等等...你需要一些选项来给课程。

我不熟悉 DynamicProxy 以及如何在这里使用它。

【讨论】:

C# 4.0 暂时不会出现。甚至还没有 CTP!【参考方案6】:

您可以使用“List”类及其方法“ForEach”。

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

【讨论】:

这也是我想到的第一件事——但他要求实现上面的“GroupGenerator”类。【参考方案7】:

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

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

【讨论】:

以上是关于如何在 C# 中编写实现给定接口的通用容器类?的主要内容,如果未能解决你的问题,请参考以下文章

C++ 容器上的通用操作

C#实现乞丐版IOC容器

C# WPF通过反射及Ioc容器综合实例

C# WPF通过反射及Ioc容器加载并显示其它项目界面(精品)

Java 泛型

(76)C#里怎么样选择各种通用类型容器