【问题标题】:Transferring collection items for collection with two IEnumerable<T> implementations with reflection使用两个带有反射的 IEnumerable<T> 实现来传输集合项以进行集合
【发布时间】:2015-08-12 19:15:27
【问题描述】:

我有这个帮助方法,旨在将集合项从一个集合对象实例传输到另一个集合对象实例。它有效,但我最近遇到了一个问题,即特定集合在不同点实现IEnumerable&lt;T&gt;;。一级为IEnumerable&lt;KeyValuePair&lt;TKey, TValue&gt;&gt;,另一级为IEnumerable&lt;TValue&gt;。在我下面的代码中,secondaryCollection 的声明导致它使用 IEnumerable&lt;TValue&gt; 实例类型,而 collectionType 声明发现它是基本的 ICollection&lt;KeyValuePair&lt;TKey, TValue&gt;&gt; 类型,因此我可以调用 Add()Remove()。尽管Add()Remove() 方法调用失败,但这种类型不匹配。我想如果我能弄清楚如何将secondaryCollection 声明为IEnumerable&lt;object&gt; 类型,其中“对象”的类型为KeyValuePair&lt;TKey, TValue&gt; 而不仅仅是TValue 类型,那么这应该可以在没有类型不匹配异常的情况下工作(它实际上是一个Add()Remove() 方法的参数异常)。问题是这一切都是在反射中完成的,并且类型是未知的。我该怎么做?

这是当前的方法代码:

public void MergeCollection(FieldInfo primaryMember, object primaryObject, FieldInfo secondaryMember, object secondaryObject)
    {
        if (primaryMember == null)
            throw new ArgumentNullException("primaryMember");

        if (primaryObject == null)
            throw new ArgumentNullException("primaryObject");

        if (secondaryMember == null)
            throw new ArgumentNullException("secondaryMember");

        if (secondaryObject == null)
            throw new ArgumentNullException("secondaryObject");

        //Get the collection type and validate
        Type genericType = typeof(ICollection<>);

        Type collectionType = primaryMember.FieldType.GetBaseTypes().FirstOrDefault(t => t.IsGenericType && t.GetGenericArguments().Length == 1 && t == genericType.MakeGenericType(t.GetGenericArguments()));

        if (!collectionType.IsAssignableFrom(secondaryMember.FieldType))
            throw new InvalidOperationException("Primary and secondary collection types do not match.");

        Type collectionParamType = collectionType.GetGenericArguments()[0];


        //Get the collection invocable methods
        MethodInfo add = collectionType.GetMethod("Add", new Type[] { collectionParamType });
        MethodInfo remove = collectionType.GetMethod("Remove", new Type[] { collectionParamType });

        //Declare the collections
        object primaryCollectionObject = primaryMember.GetValue(primaryObject);
        object secondaryCollectionObject = secondaryMember.GetValue(secondaryObject);

        Type genericEnumerableType = typeof(IEnumerable<>);
        Type enumerableType = primaryMember.FieldType.GetBaseTypes().FirstOrDefault(t => t.IsGenericType && t.GetGenericArguments().Length == 1 && t == genericEnumerableType.MakeGenericType(t.GetGenericArguments()));

        IEnumerable<object> secondaryCollection = ((IEnumerable)secondaryCollectionObject).Cast<object>();

        //Transfer the items
        int noItems = secondaryCollection.Count();
        // int noItems = (int)count.GetValue(secondaryCollectionObject);
        for (int i = 0; i < noItems; i++)
        {
            try
            {
                add.Invoke(primaryCollectionObject, new object[] { secondaryCollection.ElementAt(0) });
                remove.Invoke(secondaryCollectionObject, new object[] { secondaryCollection.ElementAt(0) });
            }
            catch (ArgumentException ex)
            {
                //The argument exception can be captured here
            }
        }
    }

编辑:

也许只是为了澄清我需要帮助的内容...我有一个自定义集合,该集合用于由使用反射的方法评估的类中。此集合两次实现 IEnumerable...IEnumerable&lt;TValue&gt;IEnumerable&lt;KeyValuePair&lt;TKey, TValue&gt;&gt;。而不是这个...

IEnumerable<object> secondaryCollection = ((IEnumerable)secondaryCollectionObject).Cast<object>();

由于Cast&lt;T&gt;() 操作,最终使用IEnumerable&lt;TValue&gt;,我需要secondaryCollection 使用IEnumerable&lt;KeyValuePair&lt;TKey, TValue&gt;&gt; 的东西。并且它无法知道该集合最初使用了两种实现。从这一行开始:

Type collectionType = primaryMember.FieldType.GetBaseTypes().FirstOrDefault(t => t.IsGenericType && t.GetGenericArguments().Length == 1 && t == genericType.MakeGenericType(t.GetGenericArguments()));

确实识别了正确的类型,我原本以为可以使用,但我不确定如何使用。

【问题讨论】:

  • 你为什么不直接做public void MoveItems&lt;T&gt;(ICollection&lt;T&gt; source, ICollection&lt;T&gt; target) { /* code to remove all items from source and add to target without reflection */ }
  • 我不能,因为这只是一个流程的一小部分,旨在将较大的对象(消息)拆分为多个较小的对象,跨流程边界发送。这整个过程是通过反射处理的,因此它不必知道细节(或者即使 Message 对象有一个集合,更不用说它的集合类型了)。这使我可以将不同类型的 Message 对象用于不同的目的。
  • 集合可能实现了多个 IEnumerable,但只有一个 ICollection

标签: c# .net reflection


【解决方案1】:

刚刚看到 Zotta 在我准备答案时提出了类似的想法。无论如何,这里是我的。最简单的方法是在“纯”C# 泛型方法中实现主要实现,然后从反射中调用它。现在的问题是如何通过反射调用泛型方法。有几个关于此的 SO 项目,到目前为止,最简单的方法是使用动态功能。这是一个示例解决方案,包括动态或纯反射方法(跳过验证)和一个简单的测试:

static class Helper
{
    public static void Merge<T>(ICollection<T> source, ICollection<T> target)
    {
        foreach (var item in source) target.Add(item);
        source.Clear();
    }

    #region Using dynamic

    public static void MergeCollection(FieldInfo sourceMember, object sourceObject, FieldInfo targetMember, object targetObject)
    {
        var sourceCollection = sourceMember.GetValue(sourceObject);
        var targetCollection = targetMember.GetValue(targetObject);
        Merge((dynamic)sourceCollection, (dynamic)targetCollection);
    }

    #endregion

    #region Using reflection only

    public static void MergeCollection2(FieldInfo sourceMember, object sourceObject, FieldInfo targetMember, object targetObject)
    {
        var collectionType = targetMember.FieldType.GetInterfaces().Single(
            t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(ICollection<>)
        );
        var itemType = collectionType.GetGenericArguments()[0];
        var mergeMethod = MergeMethodInfo.MakeGenericMethod(itemType);
        var sourceCollection = sourceMember.GetValue(sourceObject);
        var targetCollection = targetMember.GetValue(targetObject);
        mergeMethod.Invoke(null, new[] { sourceCollection, targetCollection });
    }

    private static readonly MethodInfo MergeMethodInfo = GetGenericMethodDefinition(
        (ICollection<object> source, ICollection<object> target) => Merge(source, target)
    );

    private static MethodInfo GetGenericMethodDefinition<T1, T2>(Expression<Action<T1, T2>> e)
    {
        return ((MethodCallExpression)e.Body).Method.GetGenericMethodDefinition();
    }

    #endregion

    #region Test

    class MyCollection1<TKey, TValue> : Dictionary<TKey, TValue>, IEnumerable<TValue>
    {
        IEnumerator<TValue> IEnumerable<TValue>.GetEnumerator() { return Values.GetEnumerator(); }
    }

    class MyCollection2<TKey, TValue> : List<KeyValuePair<TKey, TValue>>, IEnumerable<TValue>
    {
        IEnumerator<TValue> IEnumerable<TValue>.GetEnumerator()
        {
            IEnumerable<KeyValuePair<TKey, TValue>> e = this;
            return e.Select(item => item.Value).GetEnumerator();
        }
    }

    class MyClass1
    {
        public MyCollection1<int, string> Items1 = new MyCollection1<int, string>();
    }

    class MyClass2
    {
        public MyCollection2<int, string> Items2 = new MyCollection2<int, string>();
    }

    private static FieldInfo GetField<T, V>(Expression<Func<T, V>> e)
    {
        return (FieldInfo)((MemberExpression)e.Body).Member;
    }

    public static void Test()
    {
        var source = new MyClass1();
        for (int i = 0; i < 10; i++) source.Items1.Add(i + 1, new string((char)('A' + i), 1));
        var target = new MyClass2();
        var sourceField = GetField((MyClass1 c) => c.Items1);
        var targetField = GetField((MyClass2 c) => c.Items2);
        // Merge source into target using dynamic approach
        MergeCollection(sourceField, source, targetField, target);
        // Merge target back into source using reflection approach
        MergeCollection2(targetField, target, sourceField, source);
    }

    #endregion
}

【讨论】:

  • 好的,所以我已经尝试了这些方法,并在调用 Merge() 时以动态方法结束了 RuntimeBinderException。我不确定为什么。如果我将两个参数都设为动态而不是 ICollection 并忽略泛型类型,则它可以工作。考虑到这一点,以下是 10,000 个项目的性能差异:我的原始代码:63 毫秒,动态方法:35 毫秒,您的反射版本:15 毫秒。
  • 奖励积分的最佳方法是什么?您帮助我改进了流程,但 Deepak Bhatia 直接回答了这个问题。
  • 有趣的是,您真的找到了另一种方法来完成同样的事情——动态实现。但正如您所注意到的,它的性能与纯反射相似。我的目标是拥有尽可能多的编译代码,并且只是以某种方式调用它。由于对通用方法的动态 binding 看起来并不总是有效,现在我认为“仅反射”方法更可取。关于你的问题,这取决于你。我有兴趣解决我认为具有挑战性的问题。我是一个“表现”的人,总是寻找最好的解决方案。很高兴这对您有所帮助。干杯。
【解决方案2】:

正如您所说的,问题出在 Cast 方法上。因为您没有在 Cast 方法中传递正确的类型,所以您得到了错误的 IEnumrable。解决方案是不要使用“object”类型调用 Cast 方法,而是使用正确的类型。由于您在编译时没有正确的类型信息,您可能需要使用反射调用 Cast,如下所示:

var castMethod = typeof(Enumerable).GetMethod("Cast", BindingFlags.Static | BindingFlags.Public);
var castGenericMethod = castMethod.MakeGenericMethod(new Type[] {  collectionParamType});
secondaryCollection = castGenericMethod.Invoke(null, new object[] {secondaryCollectionObject})

注意:我刚刚在我的 iPad 中输入了上述代码,因此可能存在一些语法问题。

【讨论】:

  • 抱歉回复晚了。我们在全公司范围内部署了应用程序更新,我的日子过得很疯狂。我试过这个,它确实将集合作为 KeyValuePair 类型。我什至可以将 secondaryCollection 转换为 IEnumerable,但正如您在我的示例中看到的那样,有一个 add.Invoke() 和 remove.Invoke() 旨在将 secondCollection 中的第一项添加到主项,然后删除该第一项。 ElementAt() 可用于 IEnumerable,但不可用于 IEnumerable。我无法转换为 IEnumerable 或者我回到了原来的位置。我该如何解决这个问题?
  • 您不需要强制转换为 IEnumerable,而是使用反射来获取对 ElementAt 方法的引用,就像我们对 Cast 方法所做的那样。
  • 查看如何使用反射调用 ElementAt:stackoverflow.com/questions/1948506/…
  • 谢谢Deepak,你直接回答了我的问题。如果您在下面看到我对 Ivan 的回复,那么奖励积分的最佳方式是什么?
  • 我不认为你可以在我们之间分配积分。这纯粹是您决定授予谁,但我很高兴我的解决方案有所帮助。请参阅如何获得积分:stackoverflow.com/help/bounty
【解决方案3】:

这可能有效(只需将集合作为源和目标传递):

public static void MoveItems(dynamic source, dynamic target) {
    MoveItemsImpl(source, target);
}

public static void MoveItemsImpl<T>(ICollection<T> source, dynamic target) {
    foreach(T item in source)
        target.Add(item);
    source.Clear();//optional
}

有点hacky,但值得一试:)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-11-12
    • 1970-01-01
    • 1970-01-01
    • 2012-12-18
    • 1970-01-01
    • 2011-11-25
    • 2011-02-22
    相关资源
    最近更新 更多