linq 从只能迭代一次的IEnumerable获取头和尾

s1ag04yj  于 2022-12-06  发布在  其他
关注(0)|答案(5)|浏览(692)

我有一个元素序列。这个序列只能迭代一次,并且可以是“无限的”。
什么是最好的方法得到这样一个序列的头部和尾部?

更新:如果我在最初的问题中包括一些澄清,那就更好了:)

  • 头是序列的第一个元素,尾是“其余的”。这意味着尾也是“无限的”。
  • 当我说“无限”时,我的意思是“非常大”,“我不想一次把它全部存储在内存中”。它也可能是真正的无限,比如传感器数据(但我的情况不是这样)。
  • 当我说它只能迭代一次时,我的意思是生成序列是资源密集型的,所以我不想再做一次。它也可能是易失性数据,就像传感器数据一样,在下一次读取时不会是相同的(但在我的情况下不是这样)。
ttcibm8c

ttcibm8c1#

IEnumerable<T>分解成head & tail并不适合递归处理(不像函数列表),因为当你递归地使用 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<T>,它是由其余元素生成的(通过迭代我们已经创建的枚举器的其余部分)。

4c8rllxm

4c8rllxm2#

Obviously, each call to HeadAndTail should enumerate the sequence again (unless there is some sort of caching used). For example, consider the following:

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.

For the same reason, HeadAndTail could not be implemented as separate Head and Tail methods (unless you want even the first call to Tail to enumerate the sequence again even if it was already enumerated by a call to Head).
Additionally, HeadAndTail should not return an instance of IEnumerable (as it could be enumerated multiple times).
This leaves us with the only option: HeadAndTail should return IEnumerator, and, to make things more obvious, it should accept IEnumerator as well (we're just moving an invocation of GetEnumerator from inside the HeadAndTail to the outside, to emphasize it is of one-time use only).
Now that we have worked out the requirements, the implementation is pretty straightforward:

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);
    }
}

And now it can be used like this:

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

Or in recursive functions like this:

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)))))
gkn4icbw

gkn4icbw3#

虽然这里的其他方法建议使用yield return作为tail可枚举对象,但这样的方法增加了不必要的嵌套开销。更好的方法是将Enumerator<T>转换回可以与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<T>和非泛型IEnumerable使用单独的WrappedEnumerator结构,则可以让它们分别实现IEnumerable<T>IEnumerable;但是,它们不会真正遵守IEnumerable<T>协定,该协定规定应该可以多次调用GetEnumerator(),每次调用都返回一个独立的枚举数。
另一个重要的警告是,如果在IEnumerator<T>上使用AsForEach,则结果WrappedEnumerator应该被枚举 * 恰好 * 一次。如果它从未被枚举,则底层IEnumerator<T>将永远不会调用它的Dispose方法。
将上面提供的方法应用于当前的问题,可以很容易地在IEnumerable<T>上调用GetEnumerator(),读出前几项,然后使用AsForEach()转换余数,以便它可以用于ForEach循环(或者,如上所述,将其转换为IEnumerable<T>的实现)。调用GetEnumerator()创建了对Dispose的义务,得到了IEnumerator<T>,并且如果没有任何东西在尾部调用GetEnumerator(),则执行头/尾分离的类将没有办法做到这一点。

093gszye

093gszye4#

这可能不是最好的方法,但如果使用.ToList()方法,则可以获得[0][Count-1]位置的元素,如果Count〉0。
但是您应该指定“* 只能迭代一次 *”是什么意思

wbgh16ku

wbgh16ku5#

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

相关问题