【问题标题】:Getting head and tail from IEnumerable that can only be iterated once从只能迭代一次的 IEnumerable 获取头部和尾部
【发布时间】:2011-03-09 12:22:27
【问题描述】:

我有一系列元素。该序列只能迭代一次,并且可以是“无限的”。

获得这样一个序列的头部和尾部的最佳方法是什么?

【问题讨论】:

  • 无限序列的尾部是什么?
  • 如何定义无限序列的“尾部”?序列是否在某个时间点开始重复?
  • 你能说明为什么你只能迭代一次吗?
  • 尾部是一个无限序列,带有原始序列的其余部分。我不明白为什么这不可能?我只能迭代一次,因为生成序列的计算资源繁重。
  • 当然可以得到无限序列的尾巴! [1,2,3,4,...] 的尾部是 [2,3,4,...]。

标签: c# linq ienumerable


【解决方案1】:

IEnumerable<T> 分解为头和尾对于递归处理并不是特别好(与函数列表不同),因为当您递归地使用tail 操作时,您将创建许多间接。但是,你可以这样写:

我忽略了参数检查和异常处理之类的东西,但它显示了这个想法......

Tuple<T, IEnumerable<T>> HeadAndTail<T>(IEnumerable<T> source) {
  // Get first element of the 'source' (assuming it is there)
  var en = source.GetEnumerator();
  en.MoveNext();
  // Return first element and Enumerable that iterates over the rest
  return Tuple.Create(en.Current, EnumerateTail(en));
}

// Turn remaining (unconsumed) elements of enumerator into enumerable
IEnumerable<T> EnumerateTail<T>(IEnumerator en) {
  while(en.MoveNext()) yield return en.Current; 
}

HeadAndTail 方法获取第一个元素并将其作为元组的第一个元素返回。元组的第二个元素是IEnumerable&lt;T&gt;,它是从其余元素生成的(通过迭代我们已经创建的枚举器的其余部分)。

【讨论】:

  • EnumerateTail中的IEnumerator参数,不应该是IEnumerator吗?
  • 我会将元组设为Tuple&lt;T, IEnumerator&lt;T&gt;&gt; - 否则它看起来就像你可以迭代尾部多次,但你真的不能。
  • @Jon Skeet:我对这个建议有点困惑。这不会导致使用它的语法尴尬,例如不能直接在尾部使用 Linq?
  • @asgerhallas:是的,它会......但你已经处于尴尬的境地 - 我认为假装尾巴是“正常”序列,您可以重复阅读。
  • @asgerhallas:是的,对于第 100,000 个项目,您将要求第 99,999 个迭代器移动到下一个项目,这将要求第 99,998 个迭代器等。讨厌的堆栈:)
【解决方案2】:

显然,每次调用 HeadAndTail 都应该再次枚举序列(除非使用了某种缓存)。例如,考虑以下情况:

var a = HeadAndTail(sequence);
Console.WriteLine(HeadAndTail(a.Tail).Tail);
//Element #2; enumerator is at least at #2 now.

var b = HeadAndTail(sequence);
Console.WriteLine(b.Tail);
//Element #1; there is no way to get #1 unless we enumerate the sequence again.

出于同样的原因,HeadAndTail 不能实现为单独的 HeadTail 方法(除非您甚至希望第一次调用 Tail 再次枚举序列,即使它已经通过调用 Head 枚举)。

此外,HeadAndTail 不应返回 IEnumerable 的实例(因为它可能被枚举多次)。

这给我们留下了唯一的选择:HeadAndTail 应该返回 IEnumerator,并且,为了让事情更明显,它应该接受 IEnumerator 作为好吧(我们只是将 GetEnumerator 的调用从 HeadAndTail 内部移到外部,以强调它只能一次性使用。

现在我们已经确定了需求,实现非常简单:

class HeadAndTail<T> {
    public readonly T Head;
    public readonly IEnumerator<T> Tail;

    public HeadAndTail(T head, IEnumerator<T> tail) {
        Head = head;
        Tail = tail;
    }
}

static class IEnumeratorExtensions {
    public static HeadAndTail<T> HeadAndTail<T>(this IEnumerator<T> enumerator) {
        if (!enumerator.MoveNext()) return null;
        return new HeadAndTail<T>(enumerator.Current, enumerator);
    }
}

现在可以这样使用了:

Console.WriteLine(sequence.GetEnumerator().HeadAndTail().Tail.HeadAndTail().Head);
//Element #2

或者在这样的递归函数中:

TResult FoldR<TSource, TResult>(
    IEnumerator<TSource> sequence,
    TResult seed,
    Func<TSource, TResult, TResult> f
) {
    var headAndTail = sequence.HeadAndTail();
    if (headAndTail == null) return seed;
    return f(headAndTail.Head, FoldR(headAndTail.Tail, seed, f));
}

int Sum(IEnumerator<int> sequence) {
    return FoldR(sequence, 0, (x, y) => x+y);
}

var array = Enumerable.Range(1, 5);
Console.WriteLine(Sum(array.GetEnumerator())); //1+(2+(3+(4+(5+0)))))

【讨论】:

  • 由于语法错误,您的代码无法编译。应该得到//Element #2 的部分也失败了。
【解决方案3】:

虽然这里的其他方法建议将yield return 用于tail 枚举,但这种方法会增加不必要的嵌套开销。更好的方法是将Enumerator&lt;T&gt; 转换回可以与foreach 一起使用的东西:

public struct WrappedEnumerator<T>
{
    T myEnumerator;
    public T GetEnumerator() { return myEnumerator; }
    public WrappedEnumerator(T theEnumerator) { myEnumerator = theEnumerator; }
}
public static class AsForEachHelper
{
    static public WrappedEnumerator<IEnumerator<T>> AsForEach<T>(this IEnumerator<T> theEnumerator) {return new WrappedEnumerator<IEnumerator<T>>(theEnumerator);}

    static public WrappedEnumerator<System.Collections.IEnumerator> AsForEach(this System.Collections.IEnumerator theEnumerator) 
        { return new WrappedEnumerator<System.Collections.IEnumerator>(theEnumerator); }
}

如果对通用IEnumerable&lt;T&gt; 和非通用IEnumerable 使用单独的WrappedEnumerator 结构,可以让它们分别实现IEnumerable&lt;T&gt;IEnumerable;不过,他们不会真正遵守IEnumerable&lt;T&gt; 合同,该合同规定应该可以多次调用GetEnumerator(),每次调用都返回一个独立的枚举器。

另一个重要的警告是,如果在IEnumerator&lt;T&gt; 上使用AsForEach,则生成的WrappedEnumerator 应该准确枚举一次。如果从不枚举,则底层的IEnumerator&lt;T&gt; 将永远不会调用其Dispose 方法。

将上面提供的方法应用到手头的问题上,很容易在IEnumerable&lt;T&gt; 上调用GetEnumerator(),读出前几项,然后使用AsForEach() 转换余数以便它可以与ForEach 循环一起使用(或者,如上所述,将其转换为IEnumerable&lt;T&gt; 的实现)。然而,重要的是要注意,调用 GetEnumerator() 会产生对 Dispose 产生的 IEnumerator&lt;T&gt; 的义务,并且执行头/尾拆分的类将无法做到这一点,如果没有人调用 GetEnumerator()尾巴。

【讨论】:

    【解决方案4】:

    可能不是最好的方法,但如果您使用 .ToList() 方法,那么如果 Count > 0,您可以获取位置 [0][Count-1] 的元素。

    但是你应该指定“只能迭代一次”是什么意思

    【讨论】:

    • 谢谢,但是无限序列上的 ToList() 将花费太长时间 :)
    【解决方案5】:

    .First().Last() 到底有什么问题?虽然是的,但我必须同意那些问“无限列表的尾部是什么意思”的人的观点……这个概念没有意义,IMO。

    【讨论】:

    • 序列只能迭代一次,所以如果你调用了First,你将无法调用Last
    • 是的。那是愚蠢的——回想起来确实很有意义。
    猜你喜欢
    • 1970-01-01
    • 2021-01-27
    • 1970-01-01
    • 1970-01-01
    • 2017-07-05
    • 1970-01-01
    • 1970-01-01
    • 2017-08-05
    • 1970-01-01
    相关资源
    最近更新 更多