1. ホーム
  2. c#

[解決済み] LINQを使用してシーケンス内の最後の要素以外を取得する方法は?

2022-04-27 03:01:18

質問

例えば、ある配列があるとします。

IEnumerable<int> sequence = GetSequenceFromExpensiveSource();
// sequence now contains: 0,1,2,3,...,999999,1000000

シーケンスの取得は安くはなく、動的に生成されるので、一度だけ繰り返し行いたい。

0〜999999を取得したい(つまり、最後の要素以外をすべて取得したい)

というようなことができると認識しています。

sequence.Take(sequence.Count() - 1);

が、これでは大きな配列に対して2つの列挙が発生してしまいます。

LINQの構成で、これを可能にするものはありますか?

sequence.TakeAllButTheLastElement();

解決方法は?

しかし、ジェネレータ(yield return)を使えば、自分で簡単にアルゴリズムをコーディングすることができます。

public static IEnumerable<T> TakeAllButLast<T>(this IEnumerable<T> source) {
    var it = source.GetEnumerator();
    bool hasRemainingItems = false;
    bool isFirst = true;
    T item = default(T);

    do {
        hasRemainingItems = it.MoveNext();
        if (hasRemainingItems) {
            if (!isFirst) yield return item;
            item = it.Current;
            isFirst = false;
        }
    } while (hasRemainingItems);
}

static void Main(string[] args) {
    var Seq = Enumerable.Range(1, 10);

    Console.WriteLine(string.Join(", ", Seq.Select(x => x.ToString()).ToArray()));
    Console.WriteLine(string.Join(", ", Seq.TakeAllButLast().Select(x => x.ToString()).ToArray()));
}

あるいは、一般化されたソリューションとして、最後のn個のアイテムを破棄する(コメントで提案されているようなキューを使用する)。

public static IEnumerable<T> SkipLastN<T>(this IEnumerable<T> source, int n) {
    var  it = source.GetEnumerator();
    bool hasRemainingItems = false;
    var  cache = new Queue<T>(n + 1);

    do {
        if (hasRemainingItems = it.MoveNext()) {
            cache.Enqueue(it.Current);
            if (cache.Count > n)
                yield return cache.Dequeue();
        }
    } while (hasRemainingItems);
}

static void Main(string[] args) {
    var Seq = Enumerable.Range(1, 4);

    Console.WriteLine(string.Join(", ", Seq.Select(x => x.ToString()).ToArray()));
    Console.WriteLine(string.Join(", ", Seq.SkipLastN(3).Select(x => x.ToString()).ToArray()));
}