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의 그룹으로 그룹화의 효과와가 IEnumerableListA와 ListList


21
GroupBy는 암시 적 정렬을 수행합니다. 성능이 저하 될 수 있습니다. 우리에게 필요한 것은 SelectMany와는 반대입니다.
yfeldblum

5
@Justice, GroupBy는 해싱으로 구현 될 수 있습니다. GroupBy의 구현이 "성능을 떨어 뜨릴 수 있음"을 어떻게 알 수 있습니까?
Amy B

5
GroupBy는 모든 요소를 ​​열거 할 때까지 아무것도 반환하지 않습니다. 그것이 느린 이유입니다. OP가 원하는 목록은 연속적이므로 더 나은 방법은 [a,g,e]더 많은 원본 목록을 열거하기 전에 첫 번째 하위 목록을 생성 할 수 있습니다.
대령 패닉

9
무한 IEnumerable의 극단적 인 예를 보자. GroupBy(x=>f(x)).First()그룹을 만들지 않습니다. OP는 목록에 대해 질문했지만 IEnumerable과 함께 작동하여 단일 반복 만 작성하면 성능 이점을 얻습니다.
Colonic Panic

8
@Nick Order는 그래도 보존되지 않습니다. 여전히 아는 것이 좋지만 (0,3,6,9, ...), (1,4,7,10, ...), (2,5,8 , 11, ...). 순서가 중요하지 않으면 괜찮지 만이 경우 문제가되는 것처럼 들립니다.
Reafexus

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
나는 이것을 좋아하지만 매우 효율적이지 않다
Sam Saffron

51
나는 이것을 좋아하지만 시간 효율성은 O(n²)입니다. 당신은 목록을 반복하고 O(n)시간을 얻을 수 있습니다.
hiPyy

8
@ hIpPy, 어떻게 n ^ 2입니까? 나에게 선형 보인다
Vivek Maharajh

13
@vivekmaharajh source는 매번 랩핑 되어 교체됩니다 IEnumerable. 에서 요소를 가지고 지금 source의 층을 통과 SkipS
라세 Espeholt

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

대규모 콜 체인을 피할 수 있습니다. 그럼에도 불구하고,이 접근법에는 일반적인 결함이 있습니다. 실행 시도 문제를 강조하기 위해 청크 당 두 개의 열거를 구체화합니다.

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.


버그가 Enumerable.Range (0, 100) .Chunk (3) .Reverse (). ToArray ()가 잘못되었거나 Enumerable.Range (0, 100) .ToArray (). Chunk (3) .Reverse ()입니까? .ToArray ()에서 예외가 발생합니까?
Cameron MacFarland

@ SamSaffron 나는 대답을 업데이트하고 눈에 띄는 유스 케이스라고 생각하는 코드를 엄청나게 단순화했습니다 (주의 사항을 인정하십시오).
casperOne

IQueryable <>을 청크하는 것은 어떻습니까? 제 생각에는 최대 작업을 제공자에게 위임하려는 경우 Take / Skip 접근 방식이 최적 일 것입니다.
Guillaume86

@ Guillaume86 동의합니다. IList 또는 IQueryable을 사용하면 훨씬 더 빠른 모든 종류의 단축키를 사용할 수 있습니다 (Linq는 모든 종류의 다른 방법에 대해 내부적
Sam Saffron

1
이것은 효율성을위한 최고의 해답입니다. 각 열에서 추가 프로세스를 실행하는 IEnumerable과 함께 SqlBulkCopy를 사용하는 데 문제가 있으므로 한 번의 패스로 효율적으로 실행해야합니다. 이를 통해 IEnumerable을 관리 가능한 크기의 청크로 나눌 수 있습니다. 궁금한 점은 SqlBulkCopy의 스트리밍 모드를 사용하도록 설정 한 것 같습니다.
Brain2000

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가 활성화되어 결과 시퀀스에서 다른 작업을 수행 할 수 있습니다.


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("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두 번째로 호출 되면 문서 (강조 광산)에 따라 여전히 false를 반환합니다 .

MoveNext가 컬렉션의 끝을 통과하면 열거자는 컬렉션의 마지막 요소 뒤에 위치하고 MoveNext는 false를 반환합니다. 열거자가이 위치에있을 때 ResetNext가 호출 될 때까지 MoveNext에 대한 후속 호출도 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이 한 시점에서했던 것처럼 주문을 가지고 놀면 이상한 일을 할 것 입니다.


나는 이것이 최선의 해결책이라고 생각합니다 ... 유일한 문제는 목록에 길이가 없다는 것입니다 ... 카운트가 있습니다. 그러나 변경하기 쉽습니다. List를 구성하지 않고 오프셋 / 길이 조합으로 기본 목록에 대한 참조를 포함하는 이너 블러 블을 반환하여이를 개선 할 수 있습니다. 따라서 그룹 크기가 크면 메모리를 낭비하지 않습니다. 내가 쓰길 원한다면 의견을 말하십시오.
Amir

@Amir 나는 그것을 작성하고 싶습니다
samandmoore

이것은 훌륭하고 빠릅니다. Cameron은 당신과 마찬가지로 매우 비슷한 것을 게시했습니다. 단지 덩어리는 버퍼를 버퍼링한다는 것입니다. 덩어리와 항목 크기가 크면 메모리 부족으로 이어질 수 있습니다. 훨씬 더 답답하지만 대안은 내 대답을 참조하십시오.
Sam Saffron

@SamSaffron 그래,에 많은 수의 항목이 List<T>있으면 버퍼링으로 인해 메모리 문제가 발생합니다. 돌이켜 보면 대답에서 주목해야했지만 초점이 너무 많은 반복에 초점을 둔 것으로 보였습니다. 즉, 귀하의 솔루션은 실제로 더 무성합니다. 나는 그것을 테스트하지 않았지만 이제는 덜 털이 많은 해결책이 있는지 궁금합니다.
casperOne

@casperOne 예 ... Google은 열거 형을 분할하는 방법을 검색 할 때이 페이지를 제공했습니다. 특정 사용 사례에 대해 db에서 반환 된 엄청나게 큰 레코드 목록을 분할하고 있습니다. 그것은 날려 버릴 것이다 목록 (사실 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}}

설명

이 코드는 두 개의 yield반복자 를 중첩하여 작동 합니다.

외부 반복자는 내부 (청크) 반복자가 효과적으로 소비 한 요소 수를 추적해야합니다. 로 종료하면 remaining됩니다 innerMoveNext(). 청크의 소비되지 않은 요소는 외부 이터레이터가 다음 청크를 생성하기 전에 삭제됩니다. 그렇지 않으면 내부 열거 가능 항목이 (완전히) 소비되지 않을 때 (예 : c3.Count()6을 반환 할 때) 일관성없는 결과가 나오기 때문에 필요 합니다.

참고 : @aolszowka가 지적한 단점을 해결하기 위해 답변이 업데이트되었습니다.


2
아주 좋아요 내 "올바른"솔루션은 그보다 훨씬 복잡했습니다. 이것이 IMHO의 # 1 답변입니다.
CaseyB

ToArray ()가 호출 될 때 예기치 않은 (API 관점에서) 동작이 발생하며 스레드로부터 안전하지 않습니다.
aolszowka

@ aolszowka : 당신은 정교하게 할 수 있습니까?
3dGrabber

@ 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 개의 청크 (요소 수)가 있음을 나타내는 소스 결과. 이것은 열거자가 증가하지 않은 내부 열거를 쿼리하지 않는 한 의미가 있습니다.
aolszowka

1
@ aolszowka : 매우 유효한 포인트. 경고와 사용법 섹션을 추가했습니다. 이 코드에서는 내부 열거 형에 대해 반복한다고 가정합니다. 솔루션을 사용하면 게으름을 잊어 버릴 수 있습니다. 사용자 지정 캐싱 IEnumerator를 사용하여 두 세계를 모두 활용할 수 있어야한다고 생각합니다. 해결책을 찾으면 여기에 게시 할 것입니다.
3dGrabber

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

이 솔루션은 매우 우아하여이 답변을 두 번 이상지지 할 수 없어 죄송합니다.
Mark

3
나는 이것이 결코 실패하지 않을 것이라고 생각한다. 그러나 분명히 이상한 행동을 할 수 있습니다. 100 개의 품목이 있고 10 개의 배치로 분할하고 해당 배치의 품목을 열거하지 않고 모든 배치를 열거 한 경우 1의 100 배치로 끝납니다.
CaseyB

1
@CaseyB가 언급 한 바와 같이, 같은 실패에서이 겪고있다는 3dGrabber 여기에 해결 stackoverflow.com/a/20953521/1037948 하지만, 남자는 빨리입니다!
drzaus 2016 년

1
이것은 아름다운 해결책입니다. 그것이 약속 한 것을 정확하게 수행합니다.
Rod Hartzell

가장 우아하고 포인트 솔루션까지. 유일한 방법은 당신이 경우 ArgumentException하여 경우 ArgumentNullException을 음수에 대한 검사를 추가하고 교체해야한다
로맹 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).
너무

9

@JaredPar의 솔루션을 개선하여 진정한 지연 평가를 수행 할 수 있습니다. GroupAdjacentBy동일한 키로 연속 요소 그룹을 생성 하는 방법을 사용합니다 .

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

그룹은 하나씩 생성되므로이 솔루션은 길거나 무한한 시퀀스로 효율적으로 작동합니다.


8

몇 년 전에 Clump extension 방법을 작성했습니다. 훌륭하게 작동하며 여기서 가장 빠른 구현입니다. :피

/// <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 %를 버퍼링하고 있습니다.
Sam Saffron

@SamSaffron Yep. 특히 plinq와 같은 것들을 믹스에 던져 넣으면 원래 구현되었습니다.
Cameron MacFarland 7:03의

내 대답을 넓히고 생각을 알려주세요
Sam Saffron

@CameronMacFarland-count == size의 두 번째 검사가 왜 필요한지 설명 할 수 있습니까? 감사.
dugas

8

Buffer()이를 위해 System.Interactive가 제공 됩니다. 일부 빠른 테스트에 따르면 성능은 Sam의 솔루션과 유사합니다.


1
버퍼링 의미를 알고 있습니까? 예 : 300k 큰 문자열을 뱉어 내고 10,000 크기의 덩어리로 나누려고하는 열거자가있는 경우 메모리가 부족합니까?
Sam Saffron

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

이것은 오래된 질문이지만 이것이 내가 끝낸 것입니다. 열거 형을 한 번만 열거하지만 각 파티션에 대한 목록을 만듭니다. 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하십시오. 그러나 나는 두 가지를 권장합니다. 1. while 대신 블록을 사용하십시오. 2. List가 생성자의 최대 크기를 알 수 있도록 List 생성자에서 chunkSize를 전달합니다.
Usman Zafar

4

David B의 솔루션이 가장 효과적이라는 것을 알았습니다. 그러나 우리는 더 일반적인 솔루션에 적용했습니다.

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

3
이것은 좋지만 원래 질문자가 요구 한 것과는 다릅니다.
Amy B

4

이 다음 솔루션은 내가 얻을 수있는 가장 컴팩트 한 것입니다.

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

게시 한 후, 이것은 전체 카운트가 필요하지 않으므로 .Count () 대신 .Any () 대신 .Any ()를 사용하여 6 년 전에 게시 한 casperOne 코드와 거의 동일하다는 것을 깨달았습니다. .
Robert McKee

3

목록이 system.collections.generic 유형 인 경우 사용 가능한 "CopyTo"메소드를 사용하여 배열의 요소를 다른 하위 배열로 복사 할 수 있습니다. 복사 할 시작 요소 및 요소 수를 지정합니다.

원본 목록을 3 개 복제 한 다음 각 목록에서 "RemoveRange"를 사용하여 목록을 원하는 크기로 축소 할 수도 있습니다.

또는 도우미 메소드를 작성하여 수행하십시오.


2

오래된 솔루션이지만 다른 접근 방식이 있습니다. 내가 사용 Skip목적의 오프셋 (offset)와 이동 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 Davidson

2

패키지 / 유지 관리 솔루션에 관심이있는 사용자를 위해 MoreLINQ 라이브러리는 Batch요청한 동작과 일치 하는 확장 방법을 제공합니다 .

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

Batch구현은 유사하다 카메론 MacFarland의 답변을 반환하기 전에 청크 / 배치를 변환하기위한 과부하의 추가와 함께, 그리고 수행 아주 잘.


이것이 정답이어야합니다. 바퀴를 재창조하는 대신 morelinq를 사용해야합니다.
Otabek Kholikov

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

그냥 내 두 센트를 넣어. 목록을 "버킷"(왼쪽에서 오른쪽으로 시각화)하려는 경우 다음을 수행 할 수 있습니다.

 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 버퍼 연산자를 사용하는 것입니다

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

IMHO 대부분의 정답.
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

내 두 센트를 삽입하려면 ...

소스를 청크 할 목록 유형을 사용하여 매우 컴팩트 한 또 다른 솔루션을 찾았습니다.

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.