LINQを使用してリストをサブリストに分割する


377

各分割の区切り文字としてアイテムインデックスを使用して、をのList<SomeObject>いくつかの個別のリストに分割する方法はありますSomeObjectか?

例を挙げましょう。

私は持っています List<SomeObject>私が必要List<List<SomeObject>>またはList<SomeObject>[]これらの得られたリストのそれぞれは、元のリスト(順次)の3つの項目の基を含有するように、。

例えば。:

  • 元のリスト: [a, g, e, w, p, s, q, f, x, y, i, m, c]

  • 結果のリスト: [a, g, e], [w, p, s], [q, f, x], [y, i, m], [c]

また、結果のリストのサイズをこの関数のパラメーターにする必要があります。

回答:


378

次のコードを試してください。

public static IList<IList<T>> Split<T>(IList<T> source)
{
    return  source
        .Select((x, i) => new { Index = i, Value = x })
        .GroupBy(x => x.Index / 3)
        .Select(x => x.Select(v => v.Value).ToList())
        .ToList();
}

アイデアは、最初に要素をインデックスでグループ化することです。3で割ると、3つのグループにグループ化されるという効果があります。次に、各グループをリストに、IEnumerableof Listをsのa Listに変換します。List


21
GroupByは暗黙的なソートを行います。パフォーマンスが低下する可能性があります。必要なのは、ある種のSelectManyの逆です。
yfeldblum 2009年

5
@ Justice、GroupByはハッシュによって実装される場合があります。GroupByの実装が「パフォーマンスを低下させる可能性がある」とどのようにしてわかりますか?
エイミーB

5
GroupByは、すべての要素が列挙されるまで何も返しません。それが遅い理由です。OPが必要とするリストは連続しているため[a,g,e]、元のリストをさらに列挙する前に、より良い方法で最初のサブリストを生成できます。
大佐パニック

9
無限のIEnumerableの極端な例を見てみましょう。GroupBy(x=>f(x)).First()グループを生成することはありません。OPはリストについて尋ねましたが、IEnumerableで作業するように記述し、1回の反復のみを行うと、パフォーマンスの利点が得られます。
大佐パニック

8
ただし、@ Nick Orderは保持されません。それはまだ知っておくべきことですが、(0,3,6,9、...)、(1,4,7,10、...)、(2,5,8)にグループ化します。 、11、...)。順序が重要でない場合は問題ありませんが、この場合は重要であるように聞こえます。
Reafexus 2013

325

この質問は少し古いですが、私はこれを書いたばかりで、他の提案されたソリューションよりも少しエレガントだと思います:

/// <summary>
/// Break a list of items into chunks of a specific size
/// </summary>
public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> source, int chunksize)
{
    while (source.Any())
    {
        yield return source.Take(chunksize);
        source = source.Skip(chunksize);
    }
}

14
このソリューションが大好きです。私は無限ループを防ぐために、この健全性チェックを追加することをお勧めしたい: if (chunksize <= 0) throw new ArgumentException("Chunk size must be greater than zero.", "chunksize");
mroach

10
私はこれが好きですが、それほど効率的ではありません
サムサフラン

51
私はこれが好きですが、時間効率はO(n²)です。リストを繰り返し処理して、O(n)時間を取得できます。
hIpPy 2012

8
@hIpPy、それはn ^ 2ですか?私には直線的に見える
Vivek Maharajh 2013年

13
@vivekmaharajh sourceIEnumerable毎回ラップされて置き換えられます。したがって、要素のsourceSkip
取得

99

一般的に、CaseyBによって提案されたアプローチは適切に機能します。実際、あなたがそれを渡した場合List<T>、失敗するのは難しいため、おそらく次のように変更します。

public static IEnumerable<IEnumerable<T>> ChunkTrivialBetter<T>(this IEnumerable<T> source, int chunksize)
{
   var pos = 0; 
   while (source.Skip(pos).Any())
   {
      yield return source.Skip(pos).Take(chunksize);
      pos += chunksize;
   }
}

大規模なコールチェーンを回避できます。それにもかかわらず、このアプローチには一般的な欠陥があります。チャンクごとに2つの列挙を具体化し、問題を強調表示して実行してみます。

foreach (var item in Enumerable.Range(1, int.MaxValue).Chunk(8).Skip(100000).First())
{
   Console.WriteLine(item);
}
// wait forever 

これを克服するために、列挙を1回だけ歩くため、上記のテストを飛行色でパスするCameronのアプローチを試すことができます。

問題は、別の欠陥があり、各チャンクのすべてのアイテムが具体化することです。そのアプローチの問題は、メモリが不足することです。

それを説明するために実行してみてください:

foreach (var item in Enumerable.Range(1, int.MaxValue)
               .Select(x => x + new string('x', 100000))
               .Clump(10000).Skip(100).First())
{
   Console.Write('.');
}
// OutOfMemoryException

最後に、どのような実装でも、チャンクの順不同の反復を処理できる必要があります。次に例を示します。

Enumerable.Range(1,3).Chunk(2).Reverse().ToArray()
// should return [3],[1,2]

この回答の私の最初の改訂版のような多くの非常に最適なソリューションはそこで失敗しました。casperOneの最適化された回答でも同じ問題が見られます。

これらすべての問題に対処するには、以下を使用できます。

namespace ChunkedEnumerator
{
    public static class Extensions 
    {
        class ChunkedEnumerable<T> : IEnumerable<T>
        {
            class ChildEnumerator : IEnumerator<T>
            {
                ChunkedEnumerable<T> parent;
                int position;
                bool done = false;
                T current;


                public ChildEnumerator(ChunkedEnumerable<T> parent)
                {
                    this.parent = parent;
                    position = -1;
                    parent.wrapper.AddRef();
                }

                public T Current
                {
                    get
                    {
                        if (position == -1 || done)
                        {
                            throw new InvalidOperationException();
                        }
                        return current;

                    }
                }

                public void Dispose()
                {
                    if (!done)
                    {
                        done = true;
                        parent.wrapper.RemoveRef();
                    }
                }

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

                public bool MoveNext()
                {
                    position++;

                    if (position + 1 > parent.chunkSize)
                    {
                        done = true;
                    }

                    if (!done)
                    {
                        done = !parent.wrapper.Get(position + parent.start, out current);
                    }

                    return !done;

                }

                public void Reset()
                {
                    // per http://msdn.microsoft.com/en-us/library/system.collections.ienumerator.reset.aspx
                    throw new NotSupportedException();
                }
            }

            EnumeratorWrapper<T> wrapper;
            int chunkSize;
            int start;

            public ChunkedEnumerable(EnumeratorWrapper<T> wrapper, int chunkSize, int start)
            {
                this.wrapper = wrapper;
                this.chunkSize = chunkSize;
                this.start = start;
            }

            public IEnumerator<T> GetEnumerator()
            {
                return new ChildEnumerator(this);
            }

            System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
            {
                return GetEnumerator();
            }

        }

        class EnumeratorWrapper<T>
        {
            public EnumeratorWrapper (IEnumerable<T> source)
            {
                SourceEumerable = source;
            }
            IEnumerable<T> SourceEumerable {get; set;}

            Enumeration currentEnumeration;

            class Enumeration
            {
                public IEnumerator<T> Source { get; set; }
                public int Position { get; set; }
                public bool AtEnd { get; set; }
            }

            public bool Get(int pos, out T item) 
            {

                if (currentEnumeration != null && currentEnumeration.Position > pos)
                {
                    currentEnumeration.Source.Dispose();
                    currentEnumeration = null;
                }

                if (currentEnumeration == null)
                {
                    currentEnumeration = new Enumeration { Position = -1, Source = SourceEumerable.GetEnumerator(), AtEnd = false };
                }

                item = default(T);
                if (currentEnumeration.AtEnd)
                {
                    return false;
                }

                while(currentEnumeration.Position < pos) 
                {
                    currentEnumeration.AtEnd = !currentEnumeration.Source.MoveNext();
                    currentEnumeration.Position++;

                    if (currentEnumeration.AtEnd) 
                    {
                        return false;
                    }

                }

                item = currentEnumeration.Source.Current;

                return true;
            }

            int refs = 0;

            // needed for dispose semantics 
            public void AddRef()
            {
                refs++;
            }

            public void RemoveRef()
            {
                refs--;
                if (refs == 0 && currentEnumeration != null)
                {
                    var copy = currentEnumeration;
                    currentEnumeration = null;
                    copy.Source.Dispose();
                }
            }
        }

        public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> source, int chunksize)
        {
            if (chunksize < 1) throw new InvalidOperationException();

            var wrapper =  new EnumeratorWrapper<T>(source);

            int currentPos = 0;
            T ignore;
            try
            {
                wrapper.AddRef();
                while (wrapper.Get(currentPos, out ignore))
                {
                    yield return new ChunkedEnumerable<T>(wrapper, chunksize, currentPos);
                    currentPos += chunksize;
                }
            }
            finally
            {
                wrapper.RemoveRef();
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            int i = 10;
            foreach (var group in Enumerable.Range(1, int.MaxValue).Skip(10000000).Chunk(3))
            {
                foreach (var n in group)
                {
                    Console.Write(n);
                    Console.Write(" ");
                }
                Console.WriteLine();
                if (i-- == 0) break;
            }


            var stuffs = Enumerable.Range(1, 10).Chunk(2).ToArray();

            foreach (var idx in new [] {3,2,1})
            {
                Console.Write("idx " + idx + " ");
                foreach (var n in stuffs[idx])
                {
                    Console.Write(n);
                    Console.Write(" ");
                }
                Console.WriteLine();
            }

            /*

10000001 10000002 10000003
10000004 10000005 10000006
10000007 10000008 10000009
10000010 10000011 10000012
10000013 10000014 10000015
10000016 10000017 10000018
10000019 10000020 10000021
10000022 10000023 10000024
10000025 10000026 10000027
10000028 10000029 10000030
10000031 10000032 10000033
idx 3 7 8
idx 2 5 6
idx 1 3 4
             */

            Console.ReadKey();


        }

    }
}

また、チャンクの順不同の反復に対して導入できる最適化のラウンドもありますが、ここでは範囲外です。

どの方法を選ぶべきですか?それは完全にあなたが解決しようとしている問題に依存します。あなたが最初の欠陥に関心がないなら、簡単な答えは信じられないほど魅力的です。

ほとんどのメソッドと同様に、これはあなたがそれはあなたが修正する必要がありますスレッドセーフにしたい場合は、マルチスレッド化のために、スタッフは奇妙得ることができる安全ではありませんEnumeratorWrapper


バグはEnumerable.Range(0、100).Chunk(3).Reverse()。ToArray()が間違っているか、Enumerable.Range(0、100).ToArray()。Chunk(3).Reverse()でしょうか.ToArray()が例外をスローしますか?
Cameron MacFarland

@SamSaffron私は自分の回答を更新し、コードを大幅に簡素化しました。
casperOne

IQueryable <>のチャンクについてはどうですか?私の推測では、最大の操作をプロバイダーに委任したい場合は、テイク/スキップアプローチが最適でしょう
Guillaume86

@ Guillaume86同意します。IListまたはIQueryableがあれば、これをはるかに高速にするあらゆる種類のショートカットを使用できます(Linqは、他のあらゆる種類のメソッドに対して内部的にこれを行います)
Sam Saffron

1
これは、効率を上げるための最善の答えです。各列で追加のプロセスを実行するIEnumerableでSqlBulkCopyを使用すると問題が発生するため、1つのパスのみで効率的に実行する必要があります。これにより、IEnumerableを扱いやすいサイズのチャンクに分割できます。(不思議に思う人のために、私はSqlBulkCopyのストリーミングモードを有効にしましたが、これは壊れているようです)。
Brain2000 2016年

64

あなたは可能性が使用してクエリの数を使用Takeし、Skip、それが元のリストにあまりにも多くの反復を追加し、私は信じています。

むしろ、次のように独自のイテレータを作成する必要があると思います。

public static IEnumerable<IEnumerable<T>> GetEnumerableOfEnumerables<T>(
  IEnumerable<T> enumerable, int groupSize)
{
   // The list to return.
   List<T> list = new List<T>(groupSize);

   // Cycle through all of the items.
   foreach (T item in enumerable)
   {
     // Add the item.
     list.Add(item);

     // If the list has the number of elements, return that.
     if (list.Count == groupSize)
     {
       // Return the list.
       yield return list;

       // Set the list to a new list.
       list = new List<T>(groupSize);
     }
   }

   // Return the remainder if there is any,
   if (list.Count != 0)
   {
     // Return the list.
     yield return list;
   }
}

これを呼び出すと、LINQが有効になり、結果のシーケンスに対して他の操作を実行できます。


サムの答えに照らして、これなしでこれを行う簡単な方法があると感じました:

  • リストを繰り返し処理する(元々は行っていません)
  • チャンクを解放する前にアイテムをグループ化する(アイテムの大きなチャンクの場合、メモリの問題が発生します)
  • サムが投稿したすべてのコード

とはいえ、ここで別のパスを示します。これは、拡張メソッドでコード化してIEnumerable<T>呼び出されChunkます。

public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> source, 
    int chunkSize)
{
    // Validate parameters.
    if (source == null) throw new ArgumentNullException("source");
    if (chunkSize <= 0) throw new ArgumentOutOfRangeException("chunkSize",
        "The chunkSize parameter must be a positive value.");

    // Call the internal implementation.
    return source.ChunkInternal(chunkSize);
}

そこまで驚くことではなく、基本的なエラーチェックだけです。

に移動ChunkInternal

private static IEnumerable<IEnumerable<T>> ChunkInternal<T>(
    this IEnumerable<T> source, int chunkSize)
{
    // Validate parameters.
    Debug.Assert(source != null);
    Debug.Assert(chunkSize > 0);

    // Get the enumerator.  Dispose of when done.
    using (IEnumerator<T> enumerator = source.GetEnumerator())
    do
    {
        // Move to the next element.  If there's nothing left
        // then get out.
        if (!enumerator.MoveNext()) yield break;

        // Return the chunked sequence.
        yield return ChunkSequence(enumerator, chunkSize);
    } while (true);
}

基本的には、 IEnumerator<T>各項目を手動で反復します。現在列挙されているアイテムがあるかどうかを確認します。各チャンクが列挙された後、アイテムが残っていない場合はブレークアウトします。

シーケンスにアイテムがあることを検出すると、内部IEnumerable<T>実装の責任を次のように委任しChunkSequenceます。

private static IEnumerable<T> ChunkSequence<T>(IEnumerator<T> enumerator, 
    int chunkSize)
{
    // Validate parameters.
    Debug.Assert(enumerator != null);
    Debug.Assert(chunkSize > 0);

    // The count.
    int count = 0;

    // There is at least one item.  Yield and then continue.
    do
    {
        // Yield the item.
        yield return enumerator.Current;
    } while (++count < chunkSize && enumerator.MoveNext());
}

以来MoveNext、すでに上で呼ばれていたIEnumerator<T>に渡されChunkSequence、それがで返されるアイテムを生み出しますCurrent以上返すようにしてください決してなっていないし、次にカウントをインクリメントchunkSize項目とすべての反復後シーケンスの次の項目に移動する(ただし、数の場合は短絡します生成されたアイテムがチャンクサイズを超えています)。

アイテムが残っていない場合、InternalChunkメソッドは外側のループで別のパスを作成しますがMoveNext、2回目に呼び出されても、ドキュメントに従って falseを返します。(強調鉱山)のとおり。

MoveNextがコレクションの最後を渡すと、列挙子はコレクションの最後の要素の後に配置され、MoveNextはfalseを返します。列挙子がこの位置にある場合、MoveNextへの後続の呼び出しも、Resetが呼び出されるまでfalseを返します。

この時点で、ループが壊れ、シーケンスのシーケンスが終了します。

これは簡単なテストです:

static void Main()
{
    string s = "agewpsqfxyimc";

    int count = 0;

    // Group by three.
    foreach (IEnumerable<char> g in s.Chunk(3))
    {
        // Print out the group.
        Console.Write("Group: {0} - ", ++count);

        // Print the items.
        foreach (char c in g)
        {
            // Print the item.
            Console.Write(c + ", ");
        }

        // Finish the line.
        Console.WriteLine();
    }
}

出力:

Group: 1 - a, g, e,
Group: 2 - w, p, s,
Group: 3 - q, f, x,
Group: 4 - y, i, m,
Group: 5 - c,

重要なノートでは、これはなりません、あなたが親配列の任意の時点で全体の子配列またはブレークを排出していない場合は動作します。これは重要な注意点ですが、あなたのユースケースは、あなたが消費するということであれば、すべてを、シーケンスのシーケンスの要素を、これはうまくいきます。

さらに、Samが一時点で行ったように、注文をいじると奇妙なことが起こります。


私はこれが最良の解決策だと思います...唯一の問題は、リストに長さがないことです...カウントがあります。しかし、それは簡単に変更できます。リストを構築することなく、オフセット/長さの組み合わせでメインリストへの参照を含むienumerableを返すことで、これをより良くすることができます。したがって、グループサイズが大きい場合、メモリを浪費しません。書いて欲しいならコメントしてね。
アミール

@アミール私はそれが書かれたものを見たいと思っています
サマンドムーア'31年

これは素晴らしくて高速です-Cameronはあなたの後にも非常によく似たものを投稿しましたが、チャンクをバッファリングすることが唯一の注意です、これはチャンクとアイテムサイズが大きい場合にメモリ不足につながる可能性があります。代替案については私の答えを見てください。
サムサフラン

@SamSaffronええ、に多数のアイテムがあるList<T>場合、バッファリングのためにメモリの問題が発生することは明らかです。振り返ってみると、私は答えでそのことに注意すべきでしたが、当時は焦点があまりにも多くの反復にあるように思われました。そうは言っても、あなたの解決策は確かに毛深いです。私はそれをテストしませんでした、しかし今、それはより毛深い解決策があるかどうか疑問に思っています。
casperOne 2012年

@casperOneええ...列挙可能なものを分割する方法を探していたときにこのページをくれました。私の特定のユースケースでは、データベースから返された非常に大きなレコードのリストを分割しています。それが爆破するであろうリスト(実際には、dapperにはこのユースケースのためだけにbuffer:falseオプションがあります)
Sam Saffron

48

わかりました、これが私の見解です:

  • 完全に遅延:無限の列挙型で動作します
  • 中間コピー/バッファリングなし
  • O(n)実行時間
  • 内部シーケンスが部分的にしか消費されない場合にも機能します

public static IEnumerable<IEnumerable<T>> Chunks<T>(this IEnumerable<T> enumerable,
                                                    int chunkSize)
{
    if (chunkSize < 1) throw new ArgumentException("chunkSize must be positive");

    using (var e = enumerable.GetEnumerator())
    while (e.MoveNext())
    {
        var remaining = chunkSize;    // elements remaining in the current chunk
        var innerMoveNext = new Func<bool>(() => --remaining > 0 && e.MoveNext());

        yield return e.GetChunk(innerMoveNext);
        while (innerMoveNext()) {/* discard elements skipped by inner iterator */}
    }
}

private static IEnumerable<T> GetChunk<T>(this IEnumerator<T> e,
                                          Func<bool> innerMoveNext)
{
    do yield return e.Current;
    while (innerMoveNext());
}

使用例

var src = new [] {1, 2, 3, 4, 5, 6}; 

var c3 = src.Chunks(3);      // {{1, 2, 3}, {4, 5, 6}}; 
var c4 = src.Chunks(4);      // {{1, 2, 3, 4}, {5, 6}}; 

var sum   = c3.Select(c => c.Sum());    // {6, 15}
var count = c3.Count();                 // 2
var take2 = c3.Select(c => c.Take(2));  // {{1, 2}, {4, 5}}

解説

このコードは、2つのyieldベースのイテレーターをネストすることで機能します。

外部イテレーターは、内部(チャンク)イテレーターによって効果的に消費された要素の数を追跡する必要があります。これを超える閉鎖することによって行われているremaininginnerMoveNext()。チャンクの未使用の要素は、次のチャンクが外部反復子によって生成される前に破棄されます。これが必要なのは、内部列挙子が(完全に)消費されない場合(たとえば、c3.Count()6を返す場合)に一貫性のない結果が得られるためです。

注: 回答は、@ aolszowkaによって指摘された欠点に対処するために更新されました。


2
非常に素晴らしい。私の「正しい」解決策はそれよりもはるかに複雑でした。これが私にとっての第一の答えです。
CaseyB 2014年

これは、ToArray()が呼び出されたとき(APIの観点から)予期しない動作の影響を受けます。また、スレッドセーフでもありません。
aolszowka 2014年

@aolszowka:詳しく説明してもらえますか?
3dGrabber 2014年

@ 3dGrabber多分それは私があなたのコードをリファクタリングした方法だったでしょう(それはここで過去に少し長すぎて申し訳ありません、基本的に私がsourceEnumeratorに渡した拡張メソッドの代わりに)。私が使用したテストケースは、この効果に対するものでした。int [] arrayToSort = new int [] {9、7、2、6、3、4、8、5、1、10、11、12、13}; var source = Chunkify <int>(arrayToSort、3).ToArray(); 13のチャンク(要素の数)があることを示す結果がSourceになります。これは、列挙子がインクリメントされなかった内部列挙を照会しない限り、私には意味がありました。
aolszowka 2014年

1
@aolszowka:非常に有効なポイント。警告と使用法のセクションを追加しました。コードは、内部の列挙型を反復処理することを前提としています。あなたの解決策であなたは怠惰を失います。私は、カスタムのキャッシングIEnumeratorで両方の世界のベストを得ることが可能であるべきだと思います。解決策を見つけたら、ここに投稿します...
3dGrabber '20

18

完全に怠惰、数えたりコピーしたりしない:

public static class EnumerableExtensions
{

  public static IEnumerable<IEnumerable<T>> Split<T>(this IEnumerable<T> source, int len)
  {
     if (len == 0)
        throw new ArgumentNullException();

     var enumer = source.GetEnumerator();
     while (enumer.MoveNext())
     {
        yield return Take(enumer.Current, enumer, len);
     }
  }

  private static IEnumerable<T> Take<T>(T head, IEnumerator<T> tail, int len)
  {
     while (true)
     {
        yield return head;
        if (--len == 0)
           break;
        if (tail.MoveNext())
           head = tail.Current;
        else
           break;
     }
  }
}

この解決策は非常に洗練されているので、この回答に2回以上賛成できないのは残念です。
マーク

3
これが失敗することはないと思います。しかし、それは確かに奇妙な振る舞いをする可能性があります。100個のアイテムがあり、10個のバッチに分割し、それらのバッチのアイテムを列挙せずにすべてのバッチを列挙した場合、1のバッチは100個になります
CaseyB

1
@CaseyBが述べたように、同じ失敗3dGrabberからこの被るはここで扱わstackoverflow.com/a/20953521/1037948を、しかし、男は速いことです!
drzaus

1
これは美しい解決策です。それが約束することを正確に行います。
Rod Hartzell、2016

これまでで最もエレガントで、ポイントソリューションです。唯一のことは、負の数のチェックを追加し、ArgumentNullExceptionをArgumentExceptionに置き換えることです
Romain Vergnory

13

次の提案が最速だと思います。私はArray.Copyを使用する機能のためにソースEnumerableの遅延を犠牲にし、各サブリストの長さを事前に知っています。

public static IEnumerable<T[]> Chunk<T>(this IEnumerable<T> items, int size)
{
    T[] array = items as T[] ?? items.ToArray();
    for (int i = 0; i < array.Length; i+=size)
    {
        T[] chunk = new T[Math.Min(size, array.Length - i)];
        Array.Copy(array, i, chunk, 0, chunk.Length);
        yield return chunk;
    }
}

最速であるだけでなく、結果の列挙可能な操作(items.Chunk(5).Reverse()。SelectMany(x => x)
too too

9

@JaredParのソリューションを改善して、真の遅延評価を行うことができます。GroupAdjacentBy同じキーを持つ連続した要素のグループを生成するメソッドを使用します。

sequence
.Select((x, i) => new { Value = x, Index = i })
.GroupAdjacentBy(x=>x.Index/3)
.Select(g=>g.Select(x=>x.Value))

グループは1つずつ生成されるため、このソリューションは長いシーケンスまたは無限シーケンスで効率的に機能します。


8

私は数年前にClump拡張メソッドを書きました。うまく機能し、ここで最も速い実装です。:P

/// <summary>
/// Clumps items into same size lots.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="source">The source list of items.</param>
/// <param name="size">The maximum size of the clumps to make.</param>
/// <returns>A list of list of items, where each list of items is no bigger than the size given.</returns>
public static IEnumerable<IEnumerable<T>> Clump<T>(this IEnumerable<T> source, int size)
{
    if (source == null)
        throw new ArgumentNullException("source");
    if (size < 1)
        throw new ArgumentOutOfRangeException("size", "size must be greater than 0");

    return ClumpIterator<T>(source, size);
}

private static IEnumerable<IEnumerable<T>> ClumpIterator<T>(IEnumerable<T> source, int size)
{
    Debug.Assert(source != null, "source is null.");

    T[] items = new T[size];
    int count = 0;
    foreach (var item in source)
    {
        items[count] = item;
        count++;

        if (count == size)
        {
            yield return items;
            items = new T[size];
            count = 0;
        }
    }
    if (count > 0)
    {
        if (count == size)
            yield return items;
        else
        {
            T[] tempItems = new T[count];
            Array.Copy(items, tempItems, count);
            yield return tempItems;
        }
    }
}

それはうまくいくはずですが、チャンクの100%をバッファリングしています。私はそれを避けようとしていました...しかし、それは信じられないほど毛むくじゃらです。
サムサフラン

@SamSaffronうん。特に、あなたがplinqのようなものをミックスに投入した場合、それは私の実装がもともとあったものです。
Cameron MacFarland

私の答えを拡大し、あなたの考えを教えてください
Sam Saffron

@CameronMacFarland-count == sizeの2番目のチェックが必要な理由を説明できますか?ありがとう。
dugas 2013

8

System.InteractiveBuffer()この目的のために提供しています。いくつかの簡単なテストでは、パフォーマンスがサムのソリューションに似ていることが示されています。


1
バッファリングのセマンティクスを知っていますか?例:300kの大きな文字列を吐き出す列挙子があり、それを10,000サイズのチャンクに分割しようとすると、メモリ不足になりますか?
サムサフラン

Buffer()戻るIEnumerable<IList<T>>ので、ええ、あなたはおそらくそこに問題があるでしょう-それはあなたのようにストリーミングされません。
dahlbyk

7

ここに私が数ヶ月前に書いたリスト分割ルーチンがあります:

public static List<List<T>> Chunk<T>(
    List<T> theList,
    int chunkSize
)
{
    List<List<T>> result = theList
        .Select((x, i) => new {
            data = x,
            indexgroup = i / chunkSize
        })
        .GroupBy(x => x.indexgroup, x => x.data)
        .Select(g => new List<T>(g))
        .ToList();

    return result;
}

6

この小さなスニペットがうまく機能していることがわかります。

public static IEnumerable<List<T>> Chunked<T>(this List<T> source, int chunkSize)
{
    var offset = 0;

    while (offset < source.Count)
    {
        yield return source.GetRange(offset, Math.Min(source.Count - offset, chunkSize));
        offset += chunkSize;
    }
}

5

これはどうですか?

var input = new List<string> { "a", "g", "e", "w", "p", "s", "q", "f", "x", "y", "i", "m", "c" };
var k = 3

var res = Enumerable.Range(0, (input.Count - 1) / k + 1)
                    .Select(i => input.GetRange(i * k, Math.Min(k, input.Count - i * k)))
                    .ToList();

私の知る限り、GetRange()は取得したアイテムの数に関して線形です。したがって、これはうまく機能するはずです。


5

これは古い質問ですが、これが私が結んだものです。enumerableを1回だけ列挙しますが、各パーティションのリストを作成します。ToArray()いくつかの実装のように呼び出されたときに、予期しない動作の影響を受けません。

    public static IEnumerable<IEnumerable<T>> Partition<T>(IEnumerable<T> source, int chunkSize)
    {
        if (source == null)
        {
            throw new ArgumentNullException("source");
        }

        if (chunkSize < 1)
        {
            throw new ArgumentException("Invalid chunkSize: " + chunkSize);
        }

        using (IEnumerator<T> sourceEnumerator = source.GetEnumerator())
        {
            IList<T> currentChunk = new List<T>();
            while (sourceEnumerator.MoveNext())
            {
                currentChunk.Add(sourceEnumerator.Current);
                if (currentChunk.Count == chunkSize)
                {
                    yield return currentChunk;
                    currentChunk = new List<T>();
                }
            }

            if (currentChunk.Any())
            {
                yield return currentChunk;
            }
        }
    }

これを拡張メソッドに変換するとよいでしょう:public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> source, int chunkSize)
krizzzn

あなたの答えのために+1。ただし、2つのことをお勧めします。1。whileの代わりにforeachを使用し、blockを使用します。2.リストのコンストラクターでchunkSizeを渡して、リストが予想される最大サイズを認識できるようにします。
ウスマンザファール2014

4

David Bのソリューションが最も効果的であることがわかりました。しかし、それをより一般的なソリューションに適合させました。

list.GroupBy(item => item.SomeProperty) 
   .Select(group => new List<T>(group)) 
   .ToArray();

3
これはいいですが、元の質問者が求めていたものとはかなり異なります。
エイミーB

4

この次のソリューションは、O(n)が思いつく最もコンパクトなものです。

public static IEnumerable<T[]> Chunk<T>(IEnumerable<T> source, int chunksize)
{
    var list = source as IList<T> ?? source.ToList();
    for (int start = 0; start < list.Count; start += chunksize)
    {
        T[] chunk = new T[Math.Min(chunksize, list.Count - start)];
        for (int i = 0; i < chunk.Length; i++)
            chunk[i] = list[start + i];

        yield return chunk;
    }
}

4

古いコードですが、これは私が使用してきたものです:

    public static IEnumerable<List<T>> InSetsOf<T>(this IEnumerable<T> source, int max)
    {
        var toReturn = new List<T>(max);
        foreach (var item in source)
        {
            toReturn.Add(item);
            if (toReturn.Count == max)
            {
                yield return toReturn;
                toReturn = new List<T>(max);
            }
        }
        if (toReturn.Any())
        {
            yield return toReturn;
        }
    }

投稿した後、これは6年前にcasperOneが投稿したコードとまったく同じであることに気付きました。カウント全体を必要としないため、.Count()の代わりに.Any()を使用するように変更しました。 。
ロバートマッキー

3

リストのタイプがsystem.collections.genericの場合は、「CopyTo」メソッドを使用して、配列の要素を他のサブ配列にコピーできます。コピーする開始要素と要素の数を指定します。

また、元のリストの3つのクローンを作成し、各リストの「RemoveRange」を使用して、リストを必要なサイズに縮小することもできます。

または、ヘルパーメソッドを作成して実行します。


2

それは古い解決策ですが、私は別のアプローチをしました。Skip目的のオフセットに移動し、Take目的の数の要素を抽出するために使用します。

public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> source, 
                                                   int chunkSize)
{
    if (chunkSize <= 0)
        throw new ArgumentOutOfRangeException($"{nameof(chunkSize)} should be > 0");

    var nbChunks = (int)Math.Ceiling((double)source.Count()/chunkSize);

    return Enumerable.Range(0, nbChunks)
                     .Select(chunkNb => source.Skip(chunkNb*chunkSize)
                     .Take(chunkSize));
}

1
私が使用したアプローチと非常に似ていますが、ソースをIEnumerableにしないことをお勧めします。たとえば、sourceがLINQクエリの結果である場合、Skip / TakeはクエリのnbChunk列挙をトリガーします。高価になる可能性があります。ソースのタイプとしてIListまたはICollectionを使用することをお勧めします。これで問題は完全に回避されます。
RBデビッドソン

2

パッケージ化/メンテナンスされたソリューションに関心がある人のために、MoreLINQライブラリは、Batch要求された動作に一致する拡張メソッドを提供します。

IEnumerable<char> source = "Example string";
IEnumerable<IEnumerable<char>> chunksOfThreeChars = source.Batch(3);

Batch実装はに似ているキャメロンMacFarlandの答えを返す前に、チャンク/バッチを変換するための過負荷を追加して、そして実行する非常によく。


これは受け入れられる答えになるはずです。車輪を再発明する代わりに、morelinqを使用する必要があります
Otabek Kholikov '16

1

モジュラーパーティショニングの使用:

public IEnumerable<IEnumerable<string>> Split(IEnumerable<string> input, int chunkSize)
{
    var chunks = (int)Math.Ceiling((double)input.Count() / (double)chunkSize);
    return Enumerable.Range(0, chunks).Select(id => input.Where(s => s.GetHashCode() % chunks == id));
}

1

ちょうど私の2セントを入れてください。リストを「バケット化」する(左から右に視覚化する)場合は、次のようにします。

 public static List<List<T>> Buckets<T>(this List<T> source, int numberOfBuckets)
    {
        List<List<T>> result = new List<List<T>>();
        for (int i = 0; i < numberOfBuckets; i++)
        {
            result.Add(new List<T>());
        }

        int count = 0;
        while (count < source.Count())
        {
            var mod = count % numberOfBuckets;
            result[mod].Add(source[count]);
            count++;
        }
        return result;
    }

1

別の方法は、Rx Bufferオペレーターを使用することです

//using System.Linq;
//using System.Reactive.Linq;
//using System.Reactive.Threading.Tasks;

var observableBatches = anAnumerable.ToObservable().Buffer(size);

var batches = aList.ToObservable().Buffer(size).ToList().ToTask().GetAwaiter().GetResult();

私見最もポーパー答え。
Stanislav Berkov

1
public static List<List<T>> GetSplitItemsList<T>(List<T> originalItemsList, short number)
    {
        var listGroup = new List<List<T>>();
        int j = number;
        for (int i = 0; i < originalItemsList.Count; i += number)
        {
            var cList = originalItemsList.Take(j).Skip(i).ToList();
            j += number;
            listGroup.Add(cList);
        }
        return listGroup;
    }

0

私は一次回答をとり、それを分割する場所を決定するためのIOCコンテナーにしました。(本当に3つの項目でのみ分割することを検討している人のために、回答を検索しながらこの投稿を読んでいますか?

このメソッドにより、必要に応じて任意のタイプのアイテムを分割できます。

public static List<List<T>> SplitOn<T>(List<T> main, Func<T, bool> splitOn)
{
    int groupIndex = 0;

    return main.Select( item => new 
                             { 
                               Group = (splitOn.Invoke(item) ? ++groupIndex : groupIndex), 
                               Value = item 
                             })
                .GroupBy( it2 => it2.Group)
                .Select(x => x.Select(v => v.Value).ToList())
                .ToList();
}

OPの場合、コードは

var it = new List<string>()
                       { "a", "g", "e", "w", "p", "s", "q", "f", "x", "y", "i", "m", "c" };

int index = 0; 
var result = SplitOn(it, (itm) => (index++ % 3) == 0 );

0

Sam Saffronのアプローチと同じようにパフォーマンスが優れています。

public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> source, int size)
{
    if (source == null) throw new ArgumentNullException(nameof(source));
    if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size), "Size must be greater than zero.");

    return BatchImpl(source, size).TakeWhile(x => x.Any());
}

static IEnumerable<IEnumerable<T>> BatchImpl<T>(this IEnumerable<T> source, int size)
{
    var values = new List<T>();
    var group = 1;
    var disposed = false;
    var e = source.GetEnumerator();

    try
    {
        while (!disposed)
        {
            yield return GetBatch(e, values, group, size, () => { e.Dispose(); disposed = true; });
            group++;
        }
    }
    finally
    {
        if (!disposed)
            e.Dispose();
    }
}

static IEnumerable<T> GetBatch<T>(IEnumerator<T> e, List<T> values, int group, int size, Action dispose)
{
    var min = (group - 1) * size + 1;
    var max = group * size;
    var hasValue = false;

    while (values.Count < min && e.MoveNext())
    {
        values.Add(e.Current);
    }

    for (var i = min; i <= max; i++)
    {
        if (i <= values.Count)
        {
            hasValue = true;
        }
        else if (hasValue = e.MoveNext())
        {
            values.Add(e.Current);
        }
        else
        {
            dispose();
        }

        if (hasValue)
            yield return values[i - 1];
        else
            yield break;
    }
}

}


0

無限のジェネレーターで動作します:

a.Zip(a.Skip(1), (x, y) => Enumerable.Repeat(x, 1).Concat(Enumerable.Repeat(y, 1)))
 .Zip(a.Skip(2), (xy, z) => xy.Concat(Enumerable.Repeat(z, 1)))
 .Where((x, i) => i % 3 == 0)

デモコード:https : //ideone.com/GKmL7M

using System;
using System.Collections.Generic;
using System.Linq;

public class Test
{
  private static void DoIt(IEnumerable<int> a)
  {
    Console.WriteLine(String.Join(" ", a));

    foreach (var x in a.Zip(a.Skip(1), (x, y) => Enumerable.Repeat(x, 1).Concat(Enumerable.Repeat(y, 1))).Zip(a.Skip(2), (xy, z) => xy.Concat(Enumerable.Repeat(z, 1))).Where((x, i) => i % 3 == 0))
      Console.WriteLine(String.Join(" ", x));

    Console.WriteLine();
  }

  public static void Main()
  {
    DoIt(new int[] {1});
    DoIt(new int[] {1, 2});
    DoIt(new int[] {1, 2, 3});
    DoIt(new int[] {1, 2, 3, 4});
    DoIt(new int[] {1, 2, 3, 4, 5});
    DoIt(new int[] {1, 2, 3, 4, 5, 6});
  }
}
1

1 2

1 2 3
1 2 3

1 2 3 4
1 2 3

1 2 3 4 5
1 2 3

1 2 3 4 5 6
1 2 3
4 5 6

しかし、実際には私はlinqなしで対応するメソッドを書くことを好みます。


0

これをチェック!シーケンスカウンターと日付を含む要素のリストがあります。シーケンスが再開するたびに、新しいリストを作成します。

例 メッセージのリスト。

 List<dynamic> messages = new List<dynamic>
        {
            new { FcntUp = 101, CommTimestamp = "2019-01-01 00:00:01" },
            new { FcntUp = 102, CommTimestamp = "2019-01-01 00:00:02" },
            new { FcntUp = 103, CommTimestamp = "2019-01-01 00:00:03" },

            //restart of sequence
            new { FcntUp = 1, CommTimestamp = "2019-01-01 00:00:04" },
            new { FcntUp = 2, CommTimestamp = "2019-01-01 00:00:05" },
            new { FcntUp = 3, CommTimestamp = "2019-01-01 00:00:06" },

            //restart of sequence
            new { FcntUp = 1, CommTimestamp = "2019-01-01 00:00:07" },
            new { FcntUp = 2, CommTimestamp = "2019-01-01 00:00:08" },
            new { FcntUp = 3, CommTimestamp = "2019-01-01 00:00:09" }
        };

カウンターが再開するときに、リストを個別のリストに分割したいと思います。これがコードです:

var arraylist = new List<List<dynamic>>();

        List<dynamic> messages = new List<dynamic>
        {
            new { FcntUp = 101, CommTimestamp = "2019-01-01 00:00:01" },
            new { FcntUp = 102, CommTimestamp = "2019-01-01 00:00:02" },
            new { FcntUp = 103, CommTimestamp = "2019-01-01 00:00:03" },

            //restart of sequence
            new { FcntUp = 1, CommTimestamp = "2019-01-01 00:00:04" },
            new { FcntUp = 2, CommTimestamp = "2019-01-01 00:00:05" },
            new { FcntUp = 3, CommTimestamp = "2019-01-01 00:00:06" },

            //restart of sequence
            new { FcntUp = 1, CommTimestamp = "2019-01-01 00:00:07" },
            new { FcntUp = 2, CommTimestamp = "2019-01-01 00:00:08" },
            new { FcntUp = 3, CommTimestamp = "2019-01-01 00:00:09" }
        };

        //group by FcntUp and CommTimestamp
        var query = messages.GroupBy(x => new { x.FcntUp, x.CommTimestamp });

        //declare the current item
        dynamic currentItem = null;

        //declare the list of ranges
        List<dynamic> range = null;

        //loop through the sorted list
        foreach (var item in query)
        {
            //check if start of new range
            if (currentItem == null || item.Key.FcntUp < currentItem.Key.FcntUp)
            {
                //create a new list if the FcntUp starts on a new range
                range = new List<dynamic>();

                //add the list to the parent list
                arraylist.Add(range);
            }

            //add the item to the sublist
            range.Add(item);

            //set the current item
            currentItem = item;
        }

-1

私の2セントを挿入するには...

チャンク化するソースにリストタイプを使用することで、別の非常にコンパクトなソリューションを見つけました。

public static IEnumerable<IEnumerable<TSource>> Chunk<TSource>(this IEnumerable<TSource> source, int chunkSize)
{
    // copy the source into a list
    var chunkList = source.ToList();

    // return chunks of 'chunkSize' items
    while (chunkList.Count > chunkSize)
    {
        yield return chunkList.GetRange(0, chunkSize);
        chunkList.RemoveRange(0, chunkSize);
    }

    // return the rest
    yield return chunkList;
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.