从IEnumerable转换为IEnumerable<object>

14
我更喜欢使用 IEnumerable<object>,因为LINQ扩展方法是在其上定义的,而不是在 IEnumerable 上定义的,这样我就可以使用例如 range.Skip(2)。然而,我也喜欢使用 IEnumerable,因为无论 T 是引用类型还是值类型,T[] 都可以隐式转换为 IEnumerable。对于后一种情况,不涉及装箱,这很好。因此,我可以执行 IEnumerable range = new[] { 1, 2, 3 }。似乎不可能将两者的优点结合起来。无论如何,我选择了使用 IEnumerable,并在需要应用 LINQ 方法时进行某种类型转换。

这个 SO 线程中,我知道 range.Cast<object>() 能够完成任务。但我认为这会产生不必要的性能开销。我尝试执行直接的编译时转换,例如 (IEnumerable<object>)range。根据我的测试,它适用于引用元素类型,但不适用于值类型。有什么想法吗?

供您参考,该问题源自GitHub问题。我使用的测试代码如下:

static void Main(string[] args)
{
    // IEnumerable range = new[] { 1, 2, 3 }; // won't work
    IEnumerable range = new[] { "a", "b", "c" };
    var range2 = (IEnumerable<object>)range;
    foreach (var item in range2)
    {
        Console.WriteLine(item);
    }
}

4
为什么一开始要使用 IEnumerable?为什么不直接用 IEnumerable<int> range = new[] {1, 2, 3}; 呢?顺便问一下,非泛型的 IEnumerable 会进行装箱吗?如果你在一个非泛型的 IEnumerable 上使用 foreach,你得到的是一个 object。然而,另一方面,泛型 的可枚举类型 (IEnumerable<T>) 不会进行装箱。 - Corak
@quetzalcoatl 已更新。谢谢。 - Lingxi
@KirillBestemyanov 同意。我计划在 IEnumerable 上定居,而不是 IEnumerable<object>。现在,我只是等待团队对这个提议的回应 :) - Lingxi
1
我还不确定我理解了。你说你想处理“一般情况”。那么,“一般情况”应该是 IEnumerable<T>。所以,如果你现在有一个像doStuff(IEnumerable items){/* ... */} 的方法,你可以将其更改为doStuff<T>(IEnumerable<T> items){/* ... */},然后你就有了一个实现,可以用类型安全(和非装箱)的方式处理int[]List<string>IEnumerable<YourClass> - Corak
"但是我认为这会产生不必要的性能开销"。您是否测量过性能开销,以了解其相关性? - Enigmativity
显示剩余9条评论
4个回答

11
根据我的测试,它适用于引用类型但不适用于值类型。
正确。这是因为IEnumerable<out T>是协变的,而协变/逆变不支持值类型
我了解到range.Cast()能够完成工作。但是在我看来,这会带来不必要的性能开销。
在我看来,如果你想要一个包含值类型集合中对象的集合,则性能成本(由装箱引起)是不可避免的。使用非泛型的IEnumerable不能避免装箱,因为IEnumerable.GetEnumerator提供了一个IEnumerator,其Current属性返回一个object。我更喜欢始终使用IEnumerable<T>而不是IEnumerable。所以只需使用.Cast方法并忘记装箱。

2

在反编译该扩展程序后,源代码显示如下:

public static IEnumerable<TResult> Cast<TResult>(this IEnumerable source)
    {
      IEnumerable<TResult> enumerable = source as IEnumerable<TResult>;
      if (enumerable != null)
        return enumerable;
      if (source == null)
        throw Error.ArgumentNull("source");
      return Enumerable.CastIterator<TResult>(source);
    }

    private static IEnumerable<TResult> CastIterator<TResult>(IEnumerable source)
    {
      foreach (TResult result in source)
        yield return result;
    }

这基本上只是首先使用 IEnumerable<object>

你说:

根据我的测试,它适用于引用元素类型,但不适用于值类型。

你是如何测试的?


请注意,由于 IEnumerable<out T> 的协变性,这意味着如果您有一个 X 是引用类型的 IEnumerable<X>,那么 .Cast<object>() 将让完全相同的集合实例通过未更改。也就是说,它实际上是对底层集合对象进行简单的引用转换。对于值类型 X,使用 .Cast<object>() 必须运行所有项并将它们装箱(惰性地,就像 .Select(x => (object)x))。 - Jeppe Stig Nielsen

1
尽管我不太喜欢这种方法,但我知道可以提供一个类似于LINQ-to-Objects的工具集,可以直接在IEnumerable接口上调用,而不需要强制转换为IEnumerable(不好:可能会出现装箱!)并且不需要转换为IEnumerable(更糟糕的是:我们需要知道并编写TFoo!)。然而,它是:
  • 运行时不免费:它可能很重,我没有运行性能测试
  • 开发者不免费:您实际上需要编写所有那些针对IEnumerable的类似LINQ的扩展方法(或找到一个库来执行此操作)
  • 不简单:您需要仔细检查传入类型,并小心处理许多可能的选项
  • 不是神谕:给定一个实现了IEnumerable但未实现的集合,它只能抛出错误或将其静默地转换为IEnumerable<object>
  • 不总是有效:给定一个既实现了IEnumerable<int>又实现了IEnumerable<string>的集合,它根本无法知道该怎么做;即使放弃并将其强制转换为IEnumerable<object>也不正确

以下是.Net4+的示例:

using System;
using System.Linq;
using System.Collections.Generic;

class Program
{
    public static void Main()
    {
        Console.WriteLine("List<int>");
        new List<int> { 1, 2, 3 }
            .DoSomething()
            .DoSomething();

        Console.WriteLine("List<string>");
        new List<string> { "a", "b", "c" }
            .DoSomething()
            .DoSomething();

        Console.WriteLine("int[]");
        new int[] { 1, 2, 3 }
            .DoSomething()
            .DoSomething();

        Console.WriteLine("string[]");
        new string[] { "a", "b", "c" }
            .DoSomething()
            .DoSomething();

        Console.WriteLine("nongeneric collection with ints");
        var stack = new System.Collections.Stack();
        stack.Push(1);
        stack.Push(2);
        stack.Push(3);
        stack
            .DoSomething()
            .DoSomething();

        Console.WriteLine("nongeneric collection with mixed items");
        new System.Collections.ArrayList { 1, "a", null }
            .DoSomething()
            .DoSomething();

        Console.WriteLine("nongeneric collection with .. bits");
        new System.Collections.BitArray(0x6D)
            .DoSomething()
            .DoSomething();
    }
}

public static class MyGenericUtils
{
    public static System.Collections.IEnumerable DoSomething(this System.Collections.IEnumerable items)
    {
        // check the REAL type of incoming collection
        // if it implements IEnumerable<T>, we're lucky!
        // we can unwrap it
        // ...usually. How to unwrap it if it implements it multiple times?!
        var ietype = items.GetType().FindInterfaces((t, args) =>
            t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IEnumerable<>),
            null).SingleOrDefault();

        if (ietype != null)
        {
            return
                doSomething_X(
                    doSomething_X((dynamic)items)
                );
                // .doSomething_X() - and since the compile-time type is 'dynamic' I cannot chain
                // .doSomething_X() - it in normal way (despite the fact it would actually compile well)
               // `dynamic` doesn't resolve extension methods!
               // it would look for doSomething_X inside the returned object
               // ..but that's just INTERNAL implementation. For the user
               // on the outside it's chainable
        }
        else
            // uh-oh. no what? it can be array, it can be a non-generic collection
            // like System.Collections.Hashtable .. but..
            // from the type-definition point of view it means it holds any
            // OBJECTs inside, even mixed types, and it exposes them as IEnumerable
            // which returns them as OBJECTs, so..
            return items.Cast<object>()
                .doSomething_X()
                .doSomething_X();
    }

    private static IEnumerable<T> doSomething_X<T>(this IEnumerable<T> valitems)
    {
        // do-whatever,let's just see it being called
        Console.WriteLine("I got <{1}>: {0}", valitems.Count(), typeof(T));
        return valitems;
    }
}

是的,这很傻。我将它们链接了四次(2个外部x2个内部),只是为了显示类型信息在后续调用中不会丢失。重点是要显示“入口点”采用非通用IEnumerable,并且<T>在可以解析的任何地方都会被解析。您可以轻松地修改代码,使其成为普通的LINQ-to-Objects.Count()方法。同样,也可以编写所有其他操作。

此示例使用dynamic,让平台尽可能解析IEnumerable的最窄T(如果可能的话,我们需要确保)。没有dynamic(即.Net2.0),我们需要通过反射调用dosomething_X,或者实现两次dosomething_refs<T>():where T:class+dosomething_vals<T>():where T:struct,并进行一些魔法以正确调用它而不实际转换(可能再次使用反射)。

然而,似乎可以在非泛型 IEnumerable 后面隐藏的对象上“直接”使用类似 Linq 的东西。这要归功于隐藏在 IEnumerable 后面的对象仍然具有自己的完整类型信息(是的,这个假设可能会在 COM 或 Remoting 中失败)。不过..我认为选择 IEnumerable 是更好的选择。让我们把普通的 IEnumerable 留给真正没有其他选择的特殊情况。
哦,我其实没有调查上面的代码是否正确、快速、安全、资源节约、惰性评估等。

哇~我真是惊讶了。(@_@) - Lingxi

0

IEnumerable<T> 是一个通用接口。只要你只处理泛型和编译时已知的类型,就没有必要使用 IEnumerable<object> - 要么使用 IEnumerable<int>,要么使用 IEnumerable<T>,完全取决于你是编写通用方法还是已知正确类型的方法。不要试图找到适合所有情况的 IEnumerable - 首先使用正确的 IEnumerable 很少不可能,并且大多数情况下,这只是糟糕对象设计的结果。

IEnumerable<int> 无法转换为 IEnumerable<object> 的原因可能有些令人惊讶,但实际上非常简单 - 值类型不是多态的,因此它们不支持协变。不要误解 - IEnumerable<string> 不会 实现 IEnumerable<object> - 你可以将 IEnumerable<string> 强制转换为 IEnumerable<object> 的唯一原因是 IEnumerable<T> 是协变的。

这只是一个有趣的案例,既令人惊讶又显而易见。令人惊讶的是,因为 int 派生自 object,对吧?但是,很明显,int 并没有真正从 object 派生,尽管它可以通过一种称为装箱的过程进行强制转换为 object,从而创建一个“真正的派生自 object 的 int”。


我必须使用 IEnumerable<object>,因为我需要支持不同的元素类型(即运行时多态性)。 - Lingxi
如果是这种情况,请使用IEnumerable<object>作为最后的选择,并通过.Cast<object>获取它。对于引用类型,它将执行简单的转换(因为它们是协变的),对于值类型,它将执行与IEnumerable相同的操作。 - Luaan

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