Split List into Sublists with LINQ

前端 未结 30 2387
灰色年华
灰色年华 2020-11-21 06:26

Is there any way I can separate a List into several separate lists of SomeObject, using the item index as the delimiter of each s

相关标签:
30条回答
  • 2020-11-21 06:49

    There is no way to combine all desirable features like full lazyness, no copying, full generality and safety in one solution. The most fundamental reason is that it cannot be guaranteed that the input is not mutated before the chunks are accessed. Assume that we have a function of the following signature:

    public static IEnumerable<IEnumerable<T>> Chunk<T>(this IEnumerable<T> source, int chunkSize)
    {
        // Some implementation
    }
    

    Then the following way to use it is problematic:

    var myList = new List<int>()
    {
        1,2,3,4
    };
    var myChunks = myList.Chunk(2);
    myList.RemoveAt(0);
    var firstChunk = myChunks.First();    
    Console.WriteLine("First chunk:" + String.Join(',', firstChunk));
    myList.RemoveAt(0);
    var secondChunk = myChunks.Skip(1).First();
    Console.WriteLine("Second chunk:" + String.Join(',', secondChunk));
    // What outputs do we see for first and second chunk? Probably not what you would expect...
    

    Depending on the specific implementation the code will either fail with a runtime error or produce unintuitive results.

    So, at least one of the properties need to be weakened. If you want a bullet-proof lazy solution you need to restrict your input type to an immutable type and even then it is not straightforward to cover up all use cases. If you have control of the usage, you could, however, still opt for the most general solution, as long as you make sure that it is used in a way that works. Otherwise, you might drop the lazyness and accept some amount of copying.

    In the end, it all depends on your use case and requirements which solution is the best choice for you.

    0 讨论(0)
  • 2020-11-21 06:50

    What about this one?

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

    As far as I know, GetRange() is linear in terms of number of items taken. So this should perform well.

    0 讨论(0)
  • 2020-11-21 06:51

    We can improve @JaredPar's solution to do true lazy evaluation. We use a GroupAdjacentBy method that yields groups of consecutive elements with the same key:

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

    Because the groups are yielded one-by-one, this solution works efficiently with long or infinite sequences.

    0 讨论(0)
  • 2020-11-21 06:53

    completely lazy, no counting or copying:

    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;
         }
      }
    }
    
    0 讨论(0)
  • 2020-11-21 06:54

    Old code, but this is what I've been using:

        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;
            }
        }
    
    0 讨论(0)
  • 2020-11-21 06:55

    I took the primary answer and made it to be an IOC container to determine where to split. (For who is really looking to only split on 3 items, in reading this post while searching for an answer?)

    This method allows one to split on any type of item as needed.

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

    So for the OP the code would be

    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 讨论(0)
提交回复
热议问题