【问题标题】:Cast from IEnumerable to IEnumerable<object>从 IEnumerable 转换为 IEnumerable<object>
【发布时间】:2016-07-19 07:21:58
【问题描述】:

我更喜欢使用IEnumerable&lt;object&gt;,因为LINQ扩展方法是在上面定义的,而不是IEnumerable,所以我可以使用,例如range.Skip(2)。但是,我也更喜欢使用IEnumerable,因为无论T 是引用类型还是值类型,T[] 都可以隐式转换为IEnumerable。对于后一种情况,不涉及拳击,这很好。结果,我可以做到IEnumerable range = new[] { 1, 2, 3 }。似乎不可能将两全其美的东西结合起来。无论如何,我选择在IEnumerable 安顿下来,并在需要应用 LINQ 方法时进行某种类型的转换。

this SO 线程,我知道range.Cast&lt;object&gt;() 能够胜任这项工作。但它会产生性能开销,我认为这是不必要的。我尝试执行像(IEnumerable&lt;object&gt;)range 这样的直接编译时转换。根据我的测试,它适用于引用元素类型,但不适用于值类型。有什么想法吗?

仅供参考,问题源于this 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);
    }
}

【问题讨论】:

  • 你的意思是IEnumerable&lt;object&gt;,还是IEnumerable&lt;T&gt;?如果是前者,你为什么用&lt;object&gt;而不是&lt;T&gt;
  • @Maarten 是的,我的意思是使用IEnumerable&lt;object&gt;,因为我需要处理一般情况,并支持不同的元素类型。
  • 你为什么要和IEnumerable一起工作呢?为什么不改为IEnumerable&lt;int&gt; range = new[] { 1, 2, 3 };? - 顺便提一句。 IEnumerable 确实框,对吗?如果您在(非通用)IEnumerable 上使用foreach,您将获得object。另一方面,一个 generic 可枚举 (IEnumerable&lt;T&gt;) 没有 not 框。
  • @Corak 正如对 Maarten 评论的回复所说,我的意思是使用IEnumerable&lt;object&gt;,因为我需要处理一般情况,并支持不同的元素类型。 关于IEnumerable,我认为拳击在每次枚举时都会发生,但不是在分配时发生。查看更新问题中的测试代码。
  • 仍然不确定我是否理解。你说你想处理“一般情况”。那么“一般情况”将是IEnumerable&lt;T&gt;。因此,如果您现在有一个类似doStuff(IEnumerable items){/* ... */} 的方法,您可以将其更改为doStuff&lt;T&gt;(IEnumerable&lt;T&gt; items){/* ... */},然后您就有一个 实现,您可以在其中处理int[]List&lt;string&gt;IEnumerable&lt;YourClass&gt;,但在一个类型安全(和非装箱)方式。

标签: c# arrays linq ienumerable language-lawyer


【解决方案1】:

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

正确。这是因为IEnumerable&lt;out T&gt; 是协变的,而co-variance/contra-variance is not supported for value types

我知道 range.Cast() 能够完成这项工作。但它 会产生性能开销,我认为这是不必要的。

IMO 如果您想要一组具有给定值类型的对象集合,则性能成本(由装箱带来)是不可避免的。使用非泛型IEnumerable 不会避免装箱,因为IEnumerable.GetEnumerator 提供了IEnumerator,其.Current 属性返回object。我宁愿总是使用IEnumerable&lt;T&gt; 而不是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&lt;object&gt; 之外什么都不做。

    你说:

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

    你是如何测试的?

    【讨论】:

    • 请注意,通过IEnumerable&lt;out T&gt; 的协方差,这意味着如果您有一个IEnumerable&lt;X&gt;,其中X 是一个引用类型,那么.Cast&lt;object&gt;() 将让同一个集合实例通过un -改变。 IE。实际上,它将是对基础集合对象的简单引用转换。对于值类型X.Cast&lt;object&gt;() 的使用必然会贯穿所有项目并将它们装箱(懒惰,就像.Select(x =&gt; (object)x))。
    【解决方案3】:

    尽管我真的不喜欢这种方法,但我知道可以提供一个类似于 LINQ-to-Objects 的工具集,它可以直接在 IEnumerable 接口上调用,而无需强制转换为 IEnumerable&lt;object&gt;(不好:可能的拳击!)并且没有投射到IEnumerable&lt;TFoo&gt;(更糟糕的是:我们需要知道并编写TFoo!)。

    但是,它是:

    • 运行时不是免费的:它可能很重,我没有运行性能测试
    • 对开发人员来说不是免费的:您实际上需要为 IEnumerable 编写所有类似 LINQ 的扩展方法(或找到一个库)
    • 不简单:您需要仔细检查传入的类型,并且需要小心许多可能的选项
    • 不是预言机:给定一个实现 IEnumerable 但未实现 IEnumerable&lt;T&gt; 的集合,它只能抛出错误或静默地将其强制转换为 IEnumerable&lt;object&gt;
    • 并不总是有效:给定一个同时实现IEnumerable&lt;int&gt;IEnumerable&lt;string&gt; 的集合,它根本不知道该做什么;即使放弃并投给IEnumerable&lt;object&gt; 听起来也不对

    以下是 .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;
        }
    }
    

    是的,这很愚蠢。我将它们链接了四次 (2outsidex2inside) 只是为了表明类型信息在后续调用中不会丢失。关键是要表明“入口点”采用非泛型IEnumerable,并且&lt;T&gt; 可以在任何可能的地方解析。您可以轻松地修改代码,使其成为普通的 LINQ-to-Objects .Count() 方法。同样,也可以编写所有其他操作。

    此示例使用dynamic 让平台为 IEnumerable 解析最窄的 T,如果可能的话(我们需要首先确保这一点)。如果没有dynamic(即.Net2.0),我们需要通过反射调用dosomething_X,或者将它实现为dosomething_refs&lt;T&gt;():where T:class+dosomething_vals&lt;T&gt;():where T:struct 并做一些魔术来正确调用它而无需实际转换(可能是反射,再次)。

    尽管如此,您似乎可以让类似 linq 的东西“直接”处理隐藏在非泛型 IEnumerable 后面的东西。这一切都归功于隐藏在 IEnumerable 后面的对象仍然拥有自己的完整类型信息(是的,COM 或 Remoting 的假设可能会失败)。但是.. 我认为选择IEnumerable&lt;T&gt; 是一个更好的选择。让我们将普通的旧 IEnumerable 留给确实没有其他选择的特殊情况。

    ..oh.. 我实际上并没有调查上面的代码是否正确、快速、安全、资源节约、惰性评估等。

    【讨论】:

    • 哇~我肯定很惊讶。 (@_@)
    【解决方案4】:

    IEnumerable&lt;T&gt; 是一个通用接口。只要您只处理编译时已知的泛型和类型,使用IEnumerable&lt;object&gt; 就没有意义 - 使用IEnumerable&lt;int&gt;IEnumerable&lt;T&gt;,完全取决于您是否正在编写泛型方法,或者一个已经知道正确类型的地方。不要试图找到适合所有这些的 IEnumerable - 首先使用正确的 - 这是不可能的,这是非常罕见的,而且大多数情况下,这只是糟糕的对象设计的结果。

    IEnumerable&lt;int&gt; 不能转换为 IEnumerable&lt;object&gt; 的原因可能有点令人惊讶,但实际上非常简单——值类型不是多态的,因此它们不支持协变。不要误会 - IEnumerable&lt;string&gt; 没有实现 IEnumerable&lt;object&gt; - 你可以转换 IEnumerable&lt;string&gt;IEnumerable&lt;object&gt; 的唯一原因是 IEnumerable&lt;T&gt; 是 co -变体。

    这只是一个“令人惊讶但显而易见”的有趣案例。令人惊讶的是,int 源自 object,对吧?然而,很明显,因为int 并不是真正 派生自object,即使它可以通过称为装箱的过程转换object ,它创建了一个“真正的对象派生的 int”。

    【讨论】:

    • 我必须使用IEnumerable&lt;object&gt;,因为我需要支持不同的元素类型(即运行时多态)。
    • @Lingxi 如果是这种情况,请使用IEnumerable&lt;object&gt;作为最后的手段,并通过.Cast&lt;object&gt;获得它。对于引用类型,它将执行简单的强制转换(因为它们是协变的),对于值类型,它将执行与 IEnumerable 相同的操作。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-06-30
    • 2012-03-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多