簡體   English   中英

使用 LINQ 將列表拆分為子列表

[英]Split List into Sublists with LINQ

有什么方法可以將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]

我還需要生成的列表大小作為此函數的參數。

試試下面的代碼。

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組,然后將每組轉換為一個列表,將ListIEnumerable轉換為ListList

我剛剛寫了這個,我認為它比其他提出的解決方案更優雅:

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

一般來說, 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;
   }
}

這將避免大量調用鏈。 盡管如此,這種方法有一個普遍的缺陷。 它為每個塊實現兩個枚舉,以突出問題嘗試運行:

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

為了克服這個問題,我們可以嘗試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

可以使用許多使用TakeSkip的查詢,但我相信這會在原始列表中添加太多迭代。

相反,我認為您應該創建自己的迭代器,如下所示:

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,因此您可以對結果序列執行其他操作。


根據Sam 的回答,我覺得有一種更簡單的方法可以做到這一點:

  • 再次遍歷列表(我最初沒有這樣做)
  • 在釋放塊之前將項目分組實現(對於大塊的項目,會有內存問題)
  • Sam 發布的所有代碼

也就是說,這是另一個過程,我已將其編入到IEnumerable<T>的擴展方法中,稱為Chunk

public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> source, 
    int chunkSize)
{
    // Validate parameters.
    if (source == null) throw new ArgumentNullException(nameof(source));
    if (chunkSize <= 0) throw new ArgumentOutOfRangeException(nameof(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已經在傳遞給ChunkSequenceIEnumerator<T>ChunkSequence ,它產生Current返回的項目,然后增加計數,確保永遠不會返回超過chunkSize項目,並在每次迭代后移動到序列中的下一個項目(但如果產生的項目數超過塊大小,則短路)。

如果沒有剩余的項目,則InternalChunk方法將在外循環中再次傳遞,但是當第二次MoveNext時,它仍然會返回 false, 根據文檔(重點是我的):

如果 MoveNext 到達集合的末尾,則枚舉器將定位在集合中的最后一個元素之后,並且 MoveNext 返回 false。 當枚舉器位於此位置時,對 MoveNext 的后續調用也會返回 false,直到調用 Reset。

此時,循環將中斷,序列序列將終止。

這是一個簡單的測試:

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 曾經做過的那樣

好的,這是我的看法:

  • 完全懶惰:適用於無限可枚舉
  • 沒有中間復制/緩沖
  • 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}}

說明

該代碼通過嵌套兩個基於yield的迭代器來工作。

外部迭代器必須跟蹤內部(塊)迭代器有效消耗了多少元素。 這是通過使用innerMoveNext()關閉remaining來完成的。 在外部迭代器產生下一個塊之前,塊的未消耗元素將被丟棄。 這是必要的,因為否則你會得到不一致的結果,當內部枚舉沒有(完全)消耗時(例如c3.Count()將返回 6)。

注意:已更新答案以解決@aolszowka 指出的缺點。

完全懶惰,不計數或復制:

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

我認為以下建議將是最快的。 我正在犧牲源 Enumerable 的懶惰性,以便能夠使用 Array.Copy 並提前知道我的每個子列表的長度。

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

幾年前我寫了一個 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;
        }
    }
}

System.Interactive為此提供了Buffer() 一些快速測試表明性能類似於 Sam 的解決方案。

我們可以改進@JaredPar 的解決方案來做真正的懶惰評估。 我們使用GroupAdjacentBy方法生成具有相同鍵的連續元素組:

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

由於這些組是一對一產生的,因此該解決方案可以有效地處理長序列或無限序列。

對於對打包/維護解決方案感興趣的任何人, MoreLINQ庫提供了與您請求的行為相匹配的Batch擴展方法:

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

Batch實現類似於Cameron MacFarland 的 answer ,添加了用於在返回之前轉換塊/批處理的重載,並且性能非常好。

我發現這個小片段可以很好地完成這項工作。

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

這是我幾個月前寫的列表拆分例程:

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

這個如何?

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()在所取項目數方面是線性的。 所以這應該表現良好。

這是一個老問題,但這就是我的結論; 它僅枚舉可枚舉對象一次,但確實為每個分區創建列表。 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;
            }
        }
    }

我們發現 David B 的解決方案效果最好。 但我們將其調整為更通用的解決方案:

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

舊代碼,但這是我一直在使用的:

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

以下解決方案是我能想到的最緊湊的解決方案,即 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;
    }
}

這是一個舊的解決方案,但我有不同的方法。 我使用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));
}

如果列表是 system.collections.generic 類型,您可以使用“CopyTo”方法將數組元素復制到其他子數組。 您指定要復制的起始元素和元素數。

您還可以制作 3 個原始列表的克隆,並使用每個列表上的“RemoveRange”將列表縮小到您想要的大小。

或者只是創建一個輔助方法來為你做這件事。

另一種方法是使用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();

使用模塊化分區:

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

只需投入我的兩分錢。 如果您想“存儲”列表(從左到右可視化),您可以執行以下操作:

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

插入我的兩分錢...

通過使用要分塊的源的列表類型,我找到了另一個非常緊湊的解決方案:

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

我采用了主要答案並將其作為 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 );

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

}

可以使用無限生成器:

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 的情況下編寫相應的方法。

看一下這個! 我有一個帶有序列計數器和日期的元素列表。 每次序列重新啟動時,我都想創建一個新列表。

前任。 消息列表。

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

如果源集合實現 IList < T >(通過索引隨機訪問),我們可以使用以下方法。 它僅在元素被真正訪問時獲取元素,因此這對於延遲評估的集合特別有用。 類似於無界 IEnumerable<T>,但適用於 IList<T>。

    public static IEnumerable<IEnumerable<T>> Chunkify<T>(this IList<T> src, int chunkSize)
    {
        if (src == null) throw new ArgumentNullException(nameof(src));
        if (chunkSize < 1) throw new ArgumentOutOfRangeException(nameof(chunkSize), $"must be > 0, got {chunkSize}");

        for(var ci = 0; ci <= src.Count/chunkSize; ci++){
            yield return Window(src, ci*chunkSize, Math.Min((ci+1)*chunkSize, src.Count)-1);
        }
    }

    private static IEnumerable<T> Window<T>(IList<T> src, int startIdx, int endIdx)
    {
        Console.WriteLine($"window {startIdx} - {endIdx}");
        while(startIdx <= endIdx){
            yield return src[startIdx++];
        }
    }

有什么方法可以使用項目索引作為每個拆分的定界符,將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]

我還需要將結果列表的大小作為此函數的參數。

問題是如何“使用 LINQ 將列表拆分為子列表”,但有時您可能希望這些子列表是對原始列表的引用,而不是副本。 這允許您修改子列表中的原始列表。 在這種情況下,這可能對您有用。

public static IEnumerable<Memory<T>> RefChunkBy<T>(this T[] array, int size)
{
    if (size < 1 || array is null)
    {
        throw new ArgumentException("chunkSize must be positive");
    }

    var index = 0;
    var counter = 0;

    for (int i = 0; i < array.Length; i++)
    {
        if (counter == size)
        {
            yield return new Memory<T>(array, index, size);
            index = i;
            counter = 0;
        }
        counter++;

        if (i + 1 == array.Length)
        {
            yield return new Memory<T>(array, index, array.Length - index);
        }
    }
}

用法:

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

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

// as extension method
var c3 = src.RefChunkBy(3);      // {{1, 2, 3}, {4, 5, 6}};
var c4 = src.RefChunkBy(4);      // {{1, 2, 3, 4}, {5, 6}};

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

隨意使此代碼更好。

通過引入 .NET 6.0,System.Linq 命名空間中添加了一個名為 Chunk 的新方法:

Enumerable.Chunk(IEnumerable, Int32) 方法

public static System.Collections.Generic.IEnumerable<TSource[]> Chunk<TSource> (this System.Collections.Generic.IEnumerable<TSource> source, int size);

使用這種新方法,除了最后一個塊之外的每個塊的大小都是size 最后一個塊將包含剩余的元素,並且可能具有較小的大小。

您可能在想,為什么不使用 Skip and Take 呢? 這是真的,我認為這只是更簡潔一點,並使事情更具可讀性。

下面是一個例子:

var list = Enumerable.Range(1, 100);
var chunkSize = 10;
foreach(var chunk in list.Chunk(chunkSize)) //Returns a chunk with the correct size. 
{
    Parallel.ForEach(chunk, (item) =>
    {
        //Do something Parallel here. 
        Console.WriteLine(item);
    });
}

從 .NET 6 開始,您現在可以使用本機Chunk()方法,可用於IEnumerable<T>IQueryable<T>

更多信息(和鏈接)在這里: https : //docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/linq/partitioning-data

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM