是否有任何方法可以将List<SomeObject>分离为SomeObject的几个单独的列表,使用项目索引作为每个分割的分隔符?

让我举个例子:

我有一个List<SomeObject>,我需要一个List<List<SomeObject>>或List<SomeObject>[],这样每个结果列表将包含一组原始列表的3个项目(依次)。

eg.:

原始列表:[a, g, e, w, p, s, q, f, x, y, i, m, c] 结果列表:[a、g e], [w、p, s], [q, f, x]、[y,我,m], [c]

我还需要结果列表的大小是这个函数的参数。


当前回答

山姆·萨弗伦的做法是如此的做作。

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

}

其他回答

我刚刚写了这个,我认为它比其他提出的解决方案更优雅一点:

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

如果源集合实现了IList < T >(按索引随机访问),我们可以使用下面的方法。它只在真正访问元素时获取元素,因此这对于延迟求值的集合特别有用。类似于unbounded 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++];
        }
    }

您可以使用一些使用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,因此您可以对结果序列执行其他操作。


根据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已经在传递给ChunkSequence的IEnumerator<T>上被调用,它产生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曾经做的那样。

完全懒惰的,没有计数或复制的:

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

看看这个!我有一个序列计数器和日期的元素列表。每次序列重新启动时,我都想创建一个新列表。

例:消息列表。

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