从数组中获取通用枚举器

111

在C#中,如何从给定的数组获取通用枚举器?

在下面的代码中,MyArray是一个由MyType对象组成的数组。我想以所示的方式获取MyIEnumerator,但似乎我获取到了一个空的枚举器(尽管我已确认MyArray.Length > 0)。

MyType[] MyArray = ... ;
IEnumerator<MyType> MyIEnumerator = MyArray.GetEnumerator() as IEnumerator<MyType>;

只是出于好奇,你为什么想获取枚举器? - David Klempfner
1
@Backwards_Dave 在我的情况下,在单线程环境中,有一个文件列表,每个文件都必须以异步方式处理一次。我可以使用索引来增加,但可枚举对象更酷:) - hpaknia
8个回答

117

适用于2.0及以上版本:

((IEnumerable<MyType>)myArray).GetEnumerator()

适用于3.5+(类似LINQ,效率略低):

myArray.Cast<MyType>().GetEnumerator()   // returns IEnumerator<MyType>

4
LINQy代码实际上返回了Cast方法结果的枚举器,而不是数组的枚举器... - Guffa
1
Guffa:由于枚举器仅提供只读访问,因此在使用方面并没有太大的区别。 - Mehrdad Afshari
11
正如@Mehrdad所暗示的那样,使用LINQ/Cast具有非常不同的运行时行为,因为数组的每个元素都将通过额外的MoveNextCurrent枚举器往返传递,如果数组很大,这可能会影响性能。无论如何,可以通过首先获取正确的枚举器(即使用我答案中展示的两种方法之一)完全轻松地避免问题。 - Glenn Slayden
8
第一个选项是更好的选择!不要让任何人被“花哨的LINQy”版本所迷惑,因为它完全是多余的。 - henon
@GlennSlayden 不仅对于大型数组而言速度慢,对于任何大小的数组都是如此。因此,如果您在最内层循环中使用 myArray.Cast<MyType>().GetEnumerator(),即使对于微小的数组,它也可能会显著地减慢您的速度。 - Evgeniy Berezovsky
@EugeneBeresovsky 我试图保持礼貌.. :-) 但说真的,也许更重要的问题是底层数组的元素类型是引用(class)还是值类型(struct)。当每个值类型大于十几个字节时,额外的按值洗牌可以迅速降低LINQ管道的性能。 - Glenn Slayden

66

您可以自行决定是否需要调用一个额外的库来处理类型转换是否太丑陋:

int[] arr;
IEnumerator<int> Get1()
{
    return ((IEnumerable<int>)arr).GetEnumerator();  // <-- 1 non-local call

    // ldarg.0 
    // ldfld int32[] foo::arr
    // castclass System.Collections.Generic.IEnumerable`1<int32>
    // callvirt instance class System.Collections.Generic.IEnumerator`1<!0> System.Collections.Generic.IEnumerable`1<int32>::GetEnumerator()
}

IEnumerator<int> Get2()
{
    return arr.AsEnumerable().GetEnumerator();   // <-- 2 non-local calls

    // ldarg.0 
    // ldfld int32[] foo::arr
    // call class System.Collections.Generic.IEnumerable`1<!!0> System.Linq.Enumerable::AsEnumerable<int32>(class System.Collections.Generic.IEnumerable`1<!!0>)
    // callvirt instance class System.Collections.Generic.IEnumerator`1<!0> System.Collections.Generic.IEnumerable`1<int32>::GetEnumerator()
}

为了完整起见,还应该注意以下内容是不正确的,并且在运行时会崩溃,因为T[]选择了非泛型的IEnumerable接口作为其默认(即非显式)实现的GetEnumerator()

IEnumerator<int> NoGet()                    // error - do not use
{
    return (IEnumerator<int>)arr.GetEnumerator();

    // ldarg.0 
    // ldfld int32[] foo::arr
    // callvirt instance class System.Collections.IEnumerator System.Array::GetEnumerator()
    // castclass System.Collections.Generic.IEnumerator`1<int32>
}

神秘的是,为什么SZGenericArrayEnumerator<T>没有继承自SZArrayEnumerator——一个当前被标记为“sealed”的内部类——因为这将允许(协变)泛型枚举器默认返回?

你在 ((IEnumerable<int>)arr) 中使用了额外的括号,但在 (IEnumerator<int>)arr 中只使用了一组括号,这是有原因的吗? - David Klempfner
1
@Backwards_Dave 是的,这就是区分顶部正确示例和底部错误示例的原因。请检查 C# 一元“转换”运算符的运算符优先级 - Glenn Slayden
@GlennSlayden,使用as关键字的IL代码是否比强制转换更优? - Rabadash8820
“Casting”与as将直接映射到它们自己的特定IL指令——castclassisinst。虽然根据其定义,“as”似乎更具有存在性的“作用”,但这两者之间的实际性能差异在任何情况下都不会是可测量的。 - Glenn Slayden

30

因为我不喜欢强制转换类型,所以进行了一点更新:

your_array.AsEnumerable().GetEnumerator();

AsEnumerable() 也会进行类型转换,所以你仍然需要进行类型转换 :) 或许你可以使用 your_array.OfType<T>().GetEnumerator(); - Mladen B.
3
区别在于它是在编译时进行的隐式转换,而不是在运行时进行的显式转换。因此,如果类型错误,您将会遇到编译时错误而不是运行时错误。 - Hank Schultz
@HankSchultz 如果类型错误,那么your_array.AsEnumerable()在第一次编译时就无法通过,因为AsEnumerable()只能用于实现IEnumerable的类型的实例。 - David Klempfner

9
为了使代码尽可能干净,我喜欢让编译器完成所有工作。没有强制转换(因此它实际上是类型安全的)。不使用第三方库(System.Linq)(没有运行时开销)。
    public static IEnumerable<T> GetEnumerable<T>(this T[] arr)
    {
        return arr;
    }

// 为了使用该代码:

    String[] arr = new String[0];
    arr.GetEnumerable().GetEnumerator()

这利用了一些编译器的魔法来保持一切清洁。
另一个需要注意的点是,我的答案是唯一一个可以进行编译时检查的答案。
对于其他任何解决方案,如果“arr”的类型发生变化,则调用代码将编译通过,但在运行时会失败,导致运行时错误。
我的答案将导致代码无法编译,因此我在编码时出现错误的机会较小,因为它会提示我使用了错误的类型。

1
GlennSlayden和MehrdadAfshari所展示的类型转换也是编译时证明。 - t3chb0t
2
@t3chb0t,不是这样。它们在运行时正常工作因为我们知道Foo[]实现了IEnumerable <Foo>,但如果这个情况有所改变,编译时将无法检测到变化。显式转换永远不能编译时证明是正确的。相反,将数组分配/返回给IEnumerable<Foo>使用的是隐式转换,这是可以编译时证明是正确的。 - Toxantron
@Toxantron 如果你想将某个类型强制转换为IEnumerable<T>,那么该类型必须实现IEnumerable接口,否则无法编译通过。当你说“但如果情况改变了”的时候,能否举个例子说明你的意思? - David Klempfner
1
当然可以编译。这就是 InvalidCastException 的作用。你的编译器可能会警告你,某些转换无法运行。尝试 var foo = (int)new object()。它可以编译成功,但在运行时会崩溃。 - Toxantron

2

YourArray.OfType<StringId>().GetEnumerator();

这段代码可能会表现更好,因为它只需要检查类型,而不是进行强制转换。


在使用 OfType<..type..>() 时,您必须明确指定类型 - 至少在我的 double[][] 情况下。 - M. Mimpen
@M.Mimpen,好眼力。标记语言吞掉了我未转义的 < 和 >。 - Andrew Dennison

1
    MyType[] arr = { new MyType(), new MyType(), new MyType() };

    IEnumerable<MyType> enumerable = arr;

    IEnumerator<MyType> en = enumerable.GetEnumerator();

    foreach (MyType item in enumerable)
    {

    }

请问您能解释一下以上代码会执行什么操作吗? - Phani
1
这应该得到更高的票数!使用编译器的隐式转换是最干净、最简单的解决方案。 - Toxantron

0

自从 @Mehrdad Afshari 的回答后,我为此编写了扩展方法:

public static IEnumerator<T> GetEnumerator<T>(this T[] array) {
    return (IEnumerator<T>)array.GetEnumerator();
}

那么你可以像这样实现IEnumerable:

public class MulticoloredLine : IEnumerable<ColoredLine> {
    private ColoredLine[] coloredLines;    

    #region IEnumerable<ColoredLine>
    public IEnumerator<ColoredLine> GetEnumerator() => 
    coloredLines.GetEnumerator<ColoredLine>();
    IEnumerator IEnumerable.GetEnumerator() => coloredLines.GetEnumerator();
    #endregion
}

-1
当然,你可以实现自己的通用数组枚举器。
using System.Collections;
using System.Collections.Generic;

namespace SomeNamespace
{
    public class ArrayEnumerator<T> : IEnumerator<T>
    {
        public ArrayEnumerator(T[] arr)
        {
            collection = arr;
            length = arr.Length;
        }
        private readonly T[] collection;
        private int index = -1;
        private readonly int length;

        public T Current { get { return collection[index]; } }

        object IEnumerator.Current { get { return Current; } }

        public bool MoveNext() { index++; return index < length; }

        public void Reset() { index = -1; }

        public void Dispose() {/* Nothing to dispose. */}
    }
}

这基本上等同于Glenn Slayden提到的SZGenericArrayEnumerator<T>的.NET实现。当然,只有在值得付出努力的情况下才应该这样做。在大多数情况下,这是不必要的。


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