[c#] linq에서 배치 만들기

누군가 linq에서 특정 크기의 배치를 만드는 방법을 제안 할 수 있습니까?

이상적으로는 구성 가능한 양의 청크에서 작업을 수행 할 수 있기를 원합니다.



답변

코드를 작성할 필요가 없습니다. 소스 시퀀스를 크기가 지정된 버킷으로 일괄 처리하는 MoreLINQ Batch 메서드를 사용 합니다 (MoreLINQ는 설치할 수있는 NuGet 패키지로 제공됨).

int size = 10;
var batches = sequence.Batch(size);

다음과 같이 구현됩니다.

public static IEnumerable<IEnumerable<TSource>> Batch<TSource>(
                  this IEnumerable<TSource> source, int size)
{
    TSource[] bucket = null;
    var count = 0;

    foreach (var item in source)
    {
        if (bucket == null)
            bucket = new TSource[size];

        bucket[count++] = item;
        if (count != size)
            continue;

        yield return bucket;

        bucket = null;
        count = 0;
    }

    if (bucket != null && count > 0)
        yield return bucket.Take(count).ToArray();
}


답변

public static class MyExtensions
{
    public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> items,
                                                       int maxItems)
    {
        return items.Select((item, inx) => new { item, inx })
                    .GroupBy(x => x.inx / maxItems)
                    .Select(g => g.Select(x => x.item));
    }
}

그리고 사용법은 다음과 같습니다.

List<int> list = new List<int>() { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

foreach(var batch in list.Batch(3))
{
    Console.WriteLine(String.Join(",",batch));
}

산출:

0,1,2
3,4,5
6,7,8
9


답변

sequencedefined로 시작하고 IEnumerable<T>여러 번 안전하게 열거 할 수 있다는 것을 알고 있다면 (예 : 배열 또는 목록이기 때문에) 다음과 같은 간단한 패턴을 사용하여 요소를 일괄 처리 할 수 ​​있습니다.

while (sequence.Any())
{
    var batch = sequence.Take(10);
    sequence = sequence.Skip(10);

    // do whatever you need to do with each batch here
}


답변

위의 모든 것은 대량 배치 또는 낮은 메모리 공간에서 끔찍한 성능을 발휘합니다. 파이프 라인이 될 내 자신을 작성해야했습니다 (어디에나 항목이 누적되지 않음).

public static class BatchLinq {
    public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> source, int size) {
        if (size <= 0)
            throw new ArgumentOutOfRangeException("size", "Must be greater than zero.");

        using (IEnumerator<T> enumerator = source.GetEnumerator())
            while (enumerator.MoveNext())
                yield return TakeIEnumerator(enumerator, size);
    }

    private static IEnumerable<T> TakeIEnumerator<T>(IEnumerator<T> source, int size) {
        int i = 0;
        do
            yield return source.Current;
        while (++i < size && source.MoveNext());
    }
}

편집 : 이 접근 방식의 알려진 문제는 다음 배치로 이동하기 전에 각 배치를 열거하고 완전히 열거해야한다는 것입니다. 예를 들어 이것은 작동하지 않습니다.

//Select first item of every 100 items
Batch(list, 100).Select(b => b.First())


답변

이것은 누적을 수행하지 않는 Batch의 완전히 게으르고 오버 헤드가 적은 단일 함수 구현입니다. Nick Whaley의 솔루션을 기반으로 (및 문제 수정)EricRoller의 도움을 받아 을 .

반복은 기본 IEnumerable에서 직접 이루어 지므로 요소는 엄격한 순서로 열거되어야하며 한 번만 액세스해야합니다. 내부 루프에서 일부 요소가 사용되지 않으면 폐기됩니다 (저장된 반복기를 통해 다시 액세스하려고하면InvalidOperationException: Enumeration already finished. ).

.NET Fiddle 에서 전체 샘플을 테스트 할 수 있습니다 .

public static class BatchLinq
{
    public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> source, int size)
    {
        if (size <= 0)
            throw new ArgumentOutOfRangeException("size", "Must be greater than zero.");
        using (var enumerator = source.GetEnumerator())
            while (enumerator.MoveNext())
            {
                int i = 0;
                // Batch is a local function closing over `i` and `enumerator` that
                // executes the inner batch enumeration
                IEnumerable<T> Batch()
                {
                    do yield return enumerator.Current;
                    while (++i < size && enumerator.MoveNext());
                }

                yield return Batch();
                while (++i < size && enumerator.MoveNext()); // discard skipped items
            }
    }
}


답변

왜 아무도 구식 for-loop 솔루션을 게시하지 않았는지 궁금합니다. 다음은 하나입니다.

List<int> source = Enumerable.Range(1,23).ToList();
int batchsize = 10;
for (int i = 0; i < source.Count; i+= batchsize)
{
    var batch = source.Skip(i).Take(batchsize);
}

이 단순함은 Take 메서드가 가능하기 때문에 가능합니다.

… 요소가 생성되거나 더 이상 요소를 포함하지 않을 source때까지 count요소를 열거 하고 생성 source합니다. count요소 수를 초과하는 경우source 의 모든 요소 source반환을

부인 성명:

루프 내에서 Skip 및 Take를 사용하면 열거 가능 항목이 여러 번 열거됩니다. 열거 형이 지연되면 위험합니다. 데이터베이스 쿼리, 웹 요청 또는 파일 읽기가 여러 번 실행될 수 있습니다. 이 예제는 지연되지 않은 List 사용을위한 것이므로 문제가 적습니다. skip은 호출 될 때마다 컬렉션을 열거하므로 여전히 느린 솔루션입니다.

GetRange방법을 사용하여 해결할 수도 있지만 가능한 나머지 배치를 추출하려면 추가 계산이 필요합니다.

for (int i = 0; i < source.Count; i += batchsize)
{
    int remaining = source.Count - i;
    var batch = remaining > batchsize  ? source.GetRange(i, batchsize) : source.GetRange(i, remaining);
}

이를 처리하는 세 번째 방법은 2 개의 루프로 작동합니다. 이렇게하면 컬렉션이 한 번만 열거됩니다! :

int batchsize = 10;
List<int> batch = new List<int>(batchsize);

for (int i = 0; i < source.Count; i += batchsize)
{
    // calculated the remaining items to avoid an OutOfRangeException
    batchsize = source.Count - i > batchsize ? batchsize : source.Count - i;
    for (int j = i; j < i + batchsize; j++)
    {
        batch.Add(source[j]);
    }
    batch.Clear();
}


답변

MoreLINQ와 동일한 접근 방식이지만 Array 대신 List를 사용합니다. 벤치마킹을하지 않았지만 가독성이 어떤 사람들에게는 더 중요합니다.

    public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> source, int size)
    {
        List<T> batch = new List<T>();

        foreach (var item in source)
        {
            batch.Add(item);

            if (batch.Count >= size)
            {
                yield return batch;
                batch.Clear();
            }
        }

        if (batch.Count > 0)
        {
            yield return batch;
        }
    }