Tạo lô trong linq


104

Ai đó có thể đề xuất một cách để tạo các lô có kích thước nhất định trong linq không?

Lý tưởng nhất là tôi muốn có thể thực hiện các hoạt động với số lượng lớn có thể cấu hình được.

Câu trả lời:


116

Bạn không cần phải viết bất kỳ mã nào. Sử dụng phương pháp MoreLINQ Batch, chia chuỗi nguồn thành các nhóm có kích thước (MoreLINQ có sẵn dưới dạng gói NuGet mà bạn có thể cài đặt):

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

Được thực hiện như:

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

3
4 byte cho mỗi mục thực hiện khủng khiếp ? Bạn có một số bài kiểm tra cho thấy ý nghĩa khủng khiếp không? Nếu bạn đang tải hàng triệu mục vào bộ nhớ, thì tôi sẽ không làm điều đó. Sử dụng phân trang server-side
Sergey Berezovskiy

4
Tôi không có ý xúc phạm bạn, nhưng có những giải pháp đơn giản hơn mà không tích lũy chút nào. Hơn nữa điều này sẽ phân bổ không gian ngay cả đối với các yếu tố không tồn tại:Batch(new int[] { 1, 2 }, 1000000)
Nick Whaley

7
@NickWhaley tốt, đồng ý với bạn rằng không gian bổ sung sẽ được phân bổ, nhưng trong thực tế đời sống bạn thường có tình huống đối diện - danh sách 1.000 mặt hàng mà nên đi theo lô 50 :)
Sergey Berezovskiy

1
Có, tình hình thường sẽ theo cách khác, nhưng trong cuộc sống thực, đây có thể là đầu vào của người dùng.
Nick Whaley

8
Đây là một giải pháp hoàn toàn tốt. Trong cuộc sống thực, bạn: xác thực thông tin đầu vào của người dùng, coi các lô là toàn bộ tập hợp các mục (luôn tích lũy các mục) và thường xử lý các lô song song (điều này không được hỗ trợ bởi phương pháp trình lặp và sẽ là một bất ngờ khó chịu trừ khi bạn biết chi tiết thực hiện).
Michael Petito

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

và cách sử dụng sẽ là:

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

ĐẦU RA:

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

Làm việc hoàn hảo đối với tôi
FunMatters

16
Sau khi GroupBybắt đầu liệt kê, nó không phải liệt kê đầy đủ nguồn của nó sao? Điều này làm mất đánh giá lười biếng của nguồn và do đó, trong một số trường hợp, tất cả lợi ích của việc phân phối!
ErikE

1
Wow, cảm ơn, bạn đã cứu tôi khỏi tình trạng mất trí. Hoạt động rất tốt
Riaan de Lange

3
Như @ErikE đề cập, phương pháp này liệt kê đầy đủ nguồn của nó nên mặc dù nó có vẻ tốt đẹp, nó đánh bại mục đích của việc đánh giá lười biếng / pipelining
lasseschou

1
Làm điều này - nó hoàn toàn phù hợp khi bạn cần chia khối thứ hiện có thành nhiều khối thứ nhỏ hơn để xử lý hiệu quả. Giải pháp thay thế là một vòng lặp tìm kiếm thô trong đó bạn chia nhỏ các lô theo cách thủ công và vẫn đi qua toàn bộ nguồn.
StingyJack

31

Nếu bạn bắt đầu với sequenceđịnh nghĩa là một IEnumerable<T>và bạn biết rằng nó có thể được liệt kê nhiều lần một cách an toàn (ví dụ: vì nó là một mảng hoặc một danh sách), bạn chỉ có thể sử dụng mẫu đơn giản này để xử lý các phần tử theo lô:

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

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

2
Nice, cách đơn giản cho trạm trộn w / o nhiều mã hoặc nhu cầu về thư viện bên ngoài
DevHawk

5
@DevHawk: đúng rồi. Tuy nhiên, lưu ý rằng hiệu suất sẽ bị ảnh hưởng theo cấp số nhân trên các bộ sưu tập lớn (r).
RobIII

28

Tất cả những điều trên hoạt động rất tệ với các lô lớn hoặc không gian bộ nhớ thấp. Đã phải viết đường ống dẫn sẽ của riêng tôi (chú ý rằng không có mục tích lũy ở bất kỳ đâu):

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

Chỉnh sửa: Vấn đề đã biết với cách tiếp cận này là mỗi lô phải được liệt kê và liệt kê đầy đủ trước khi chuyển sang lô tiếp theo. Ví dụ, điều này không hoạt động:

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

1
Quy trình @LB được đăng ở trên cũng không thực hiện tích lũy vật phẩm.
neontapir,

2
@neontapir Vẫn vậy. Một máy phân loại tiền xu cung cấp cho bạn niken trước, sau đó phân kỳ, trước tiên PHẢI kiểm tra từng đồng xu trước khi đưa cho bạn một xu để đảm bảo không còn niken nào nữa.
Nick Whaley

2
Ahhh ahha, đã bỏ lỡ ghi chú chỉnh sửa của bạn khi tôi lấy mã này. Phải mất một thời gian để hiểu tại sao việc lặp lại các lô chưa được liệt kê lại thực sự liệt kê toàn bộ tập hợp ban đầu (!!!), cung cấp các lô X, mỗi lô có 1 mục (trong đó X là số lượng các mục sưu tập ban đầu).
eli

2
@NickWhaley nếu tôi thực hiện Count () trên kết quả IEnumerable <IEnumerable <T>> bằng mã của bạn, nó đưa ra câu trả lời sai, nó cho biết tổng số phần tử, khi dự kiến ​​là tổng số lô được tạo. Đây không phải là trường hợp với mã hàng loạt MoreLinq
Mrinal Kamboj

1
@JohnZabroski - Đây là ý chính ngắn gọn: gist.github.com/mmurrell/9225ed7c4d107c2195057f77e07f0f68
Matt Murrell

24

Đây là một triển khai Batch hoàn toàn lười biếng, chi phí thấp, một chức năng mà không thực hiện bất kỳ tích lũy nào. Dựa trên (và khắc phục các sự cố trong) giải pháp của Nick Whaley với sự trợ giúp từ EricRoller.

Sự lặp lại đến trực tiếp từ IEnumerable bên dưới, vì vậy các phần tử phải được liệt kê theo thứ tự nghiêm ngặt và được truy cập không quá một lần. Nếu một số phần tử không được sử dụng trong một vòng lặp bên trong, chúng sẽ bị loại bỏ (và việc cố gắng truy cập lại chúng thông qua một trình vòng lặp đã lưu sẽ bị ném InvalidOperationException: Enumeration already finished.).

Bạn có thể kiểm tra một mẫu hoàn chỉnh tại .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
            }
    }
}

2
Đây là cách thực hiện lười biếng duy nhất ở đây. Phù hợp với triển khai itertools.GroupBy python.
Eric Roller

1
Bạn có thể loại bỏ việc kiểm tra donebằng cách luôn gọi e.Count()sau yield return e. Bạn sẽ cần sắp xếp lại vòng lặp trong BatchInner để không gọi hành vi không xác định source.Currentnếu i >= size. Điều này sẽ loại bỏ sự cần thiết phải phân bổ mới BatchInnercho mỗi lô.
Eric Roller

1
Bạn nói đúng, bạn vẫn cần nắm bắt thông tin về tiến độ của từng đợt. Tôi đã tìm thấy lỗi trong mã của bạn nếu bạn thử lấy mục thứ 2 từ mỗi đợt: lỗi fiddle . Thực hiện cố định mà không có một lớp riêng biệt (sử dụng C # 7) là ở đây: cố định fiddle . Lưu ý rằng tôi hy vọng CLR sẽ vẫn tạo hàm cục bộ một lần cho mỗi vòng lặp để nắm bắt biến ivì vậy điều này không nhất thiết phải hiệu quả hơn việc xác định một lớp riêng biệt, nhưng tôi nghĩ nó gọn gàng hơn một chút.
Eric Roller

1
Tôi đã đánh giá phiên bản này bằng cách sử dụng BenchmarkDotNet so với System.Reactive.Linq.EnumerableEx.Buffer và việc triển khai của bạn nhanh hơn 3-4 lần, không có rủi ro về an toàn. Nội bộ, EnumerableEx.Buffer phân bổ một hàng đợi danh sách <T> github.com/dotnet/reactive/blob/…
John Zabroski

1
Nếu bạn muốn một phiên bản có bộ đệm của điều này, bạn có thể thực hiện: public static IEnumerable <IReadOnlyList <T>> BatchBuffered <T> (IEnumerable <T> source, int size) => Batch (source, size) .Select (chunk = > (IReadOnlyList <T>) chunk.ToList ()); Việc sử dụng IReadOnlyList <T> là để gợi ý cho người dùng rằng đầu ra được lưu vào bộ nhớ đệm. Bạn cũng có thể giữ IEnumerable <IEnumerable <T>> để thay thế.
gfache

11

Tôi tự hỏi tại sao không ai đã từng đăng một giải pháp vòng lặp cho trường học cũ. Đây là một:

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

Sự đơn giản này có thể thực hiện được vì phương pháp Take:

... liệt kê sourcevà tạo ra các phần tử cho đến khi countcác phần tử đã được sinh ra hoặc sourcekhông chứa phần tử nào nữa. Nếu countvượt quá số phần tử trong source, tất cả các phần tử của sourceđược trả về

Tuyên bố từ chối trách nhiệm:

Sử dụng Skip và Take bên trong vòng lặp có nghĩa là enumerable sẽ được liệt kê nhiều lần. Điều này rất nguy hiểm nếu việc liệt kê bị trì hoãn. Nó có thể dẫn đến nhiều lần thực thi một truy vấn cơ sở dữ liệu hoặc một yêu cầu web hoặc một tệp đọc. Ví dụ này rõ ràng cho việc sử dụng Danh sách không bị trì hoãn, vì vậy nó ít có vấn đề hơn. Nó vẫn là một giải pháp chậm vì bỏ qua sẽ liệt kê bộ sưu tập mỗi khi nó được gọi.

Điều này cũng có thể được giải quyết bằng cách sử dụng GetRangephương pháp này, nhưng nó yêu cầu một phép tính bổ sung để trích xuất một lô nghỉ có thể:

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

Đây là cách thứ ba để xử lý điều này, hoạt động với 2 vòng lặp. Điều này đảm bảo rằng bộ sưu tập chỉ được liệt kê 1 lần !:

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

2
Giải pháp rất tốt. Mọi người quên làm thế nào để sử dụng cho vòng lặp
VitalickS

1
Sử dụng SkipTakebên trong vòng lặp có nghĩa là có thể liệt kê nhiều lần. Điều này rất nguy hiểm nếu việc liệt kê bị trì hoãn. Nó có thể dẫn đến nhiều lần thực thi một truy vấn cơ sở dữ liệu hoặc một yêu cầu web hoặc một tệp đọc. Trong ví dụ của bạn, bạn có một Listkhông được hoãn lại, vì vậy nó ít có vấn đề hơn.
Theodor Zoulias

@TheodorZoulias vâng, tôi biết, đây thực sự là lý do tại sao tôi đăng giải pháp thứ hai ngày hôm nay. Tôi đã đăng nhận xét của bạn như một tuyên bố từ chối trách nhiệm, bởi vì bạn đã xây dựng nó khá tốt, tôi sẽ trích dẫn cho bạn chứ?
Mong Zhu

Tôi đã viết một giải pháp thứ ba với 2 vòng lặp để bộ sưu tập chỉ được liệt kê 1 lần. điều bỏ qua.take là một giải pháp rất kém hiệu quả
Mong Zhu

4

Cách tiếp cận tương tự như MoreLINQ, nhưng sử dụng Danh sách thay vì Mảng. Tôi chưa thực hiện đo điểm chuẩn, nhưng khả năng đọc quan trọng hơn đối với một số người:

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

1
Bạn KHÔNG nên sử dụng lại biến lô. Người tiêu dùng của bạn có thể hoàn toàn bị ảnh hưởng bởi điều đó. Ngoài ra, hãy chuyển sizetham số vào của bạn new Listđể tối ưu hóa kích thước của nó.
ErikE

1
Dễ dàng sửa chữa: thay thế batch.Clear();vớibatch = new List<T>();
NetMage

3

Đây là một nỗ lực cải tiến triển khai lười biếng của Nick Whaley ( liên kết ) và infogulch ( liên kết ) Batch. Điều này là nghiêm ngặt. Bạn có thể liệt kê các lô theo đúng thứ tự hoặc bạn có một ngoại lệ.

public static IEnumerable<IEnumerable<TSource>> Batch<TSource>(
    this IEnumerable<TSource> source, int size)
{
    if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size));
    using (var enumerator = source.GetEnumerator())
    {
        int i = 0;
        while (enumerator.MoveNext())
        {
            if (i % size != 0) throw new InvalidOperationException(
                "The enumeration is out of order.");
            i++;
            yield return GetBatch();
        }
        IEnumerable<TSource> GetBatch()
        {
            while (true)
            {
                yield return enumerator.Current;
                if (i % size == 0 || !enumerator.MoveNext()) break;
                i++;
            }
        }
    }
}

Và đây là một Batchtriển khai lười biếng cho các loại nguồn IList<T>. Điều này không áp đặt hạn chế đối với việc liệt kê. Các lô có thể được liệt kê một phần, theo bất kỳ thứ tự nào và nhiều hơn một lần. Mặc dù vậy, hạn chế của việc không sửa đổi bộ sưu tập trong quá trình điều tra. Điều này đạt được bằng cách thực hiện một cuộc gọi giả tới enumerator.MoveNext()trước khi mang lại bất kỳ đoạn hoặc phần tử nào. Nhược điểm là không thể tranh cãi được điều tra viên, vì không biết khi nào kết thúc điều tra.

public static IEnumerable<IEnumerable<TSource>> Batch<TSource>(
    this IList<TSource> source, int size)
{
    if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size));
    var enumerator = source.GetEnumerator();
    for (int i = 0; i < source.Count; i += size)
    {
        enumerator.MoveNext();
        yield return GetChunk(i, Math.Min(i + size, source.Count));
    }
    IEnumerable<TSource> GetChunk(int from, int toExclusive)
    {
        for (int j = from; j < toExclusive; j++)
        {
            enumerator.MoveNext();
            yield return source[j];
        }
    }
}

2

Tôi tham gia chương trình này rất muộn nhưng tôi đã tìm thấy điều gì đó thú vị hơn.

Vì vậy, chúng tôi có thể sử dụng ở đây SkipTakecho hiệu suất tốt hơn.

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

        public static IEnumerable<T> Batch2<T>(this IEnumerable<T> items, int skip, int take)
        {
            return items.Skip(skip).Take(take);
        }

    }

Tiếp theo tôi đã kiểm tra với 100000 bản ghi. Việc lặp lại chỉ mất nhiều thời gian hơn trong trường hợpBatch

Code Of ứng dụng console.

static void Main(string[] args)
{
    List<string> Ids = GetData("First");
    List<string> Ids2 = GetData("tsriF");

    Stopwatch FirstWatch = new Stopwatch();
    FirstWatch.Start();
    foreach (var batch in Ids2.Batch(5000))
    {
        // Console.WriteLine("Batch Ouput:= " + string.Join(",", batch));
    }
    FirstWatch.Stop();
    Console.WriteLine("Done Processing time taken:= "+ FirstWatch.Elapsed.ToString());


    Stopwatch Second = new Stopwatch();

    Second.Start();
    int Length = Ids2.Count;
    int StartIndex = 0;
    int BatchSize = 5000;
    while (Length > 0)
    {
        var SecBatch = Ids2.Batch2(StartIndex, BatchSize);
        // Console.WriteLine("Second Batch Ouput:= " + string.Join(",", SecBatch));
        Length = Length - BatchSize;
        StartIndex += BatchSize;
    }

    Second.Stop();
    Console.WriteLine("Done Processing time taken Second:= " + Second.Elapsed.ToString());
    Console.ReadKey();
}

static List<string> GetData(string name)
{
    List<string> Data = new List<string>();
    for (int i = 0; i < 100000; i++)
    {
        Data.Add(string.Format("{0} {1}", name, i.ToString()));
    }

    return Data;
}

Thời gian thực hiện Là như thế này.

Đầu tiên - 00: 00: 00.0708, 00: 00: 00.0660

Thứ hai (Lấy và Bỏ qua Một) - 00: 00: 00.0008, 00: 00: 00.0008


1
GroupByliệt kê đầy đủ trước khi nó tạo ra một hàng. Đây không phải là một cách tốt để làm theo lô.
ErikE

@ErikE Điều đó phụ thuộc vào những gì bạn đang cố gắng đạt được. Nếu việc chia lô không phải là vấn đề và bạn chỉ cần chia các mục thành các phần nhỏ hơn để xử lý thì có thể chỉ là vấn đề. Tôi đang sử dụng này cho MSCRM nơi có thể có 100 hồ sơ mà không có vấn đề đối với LAMBDA để hàng loạt .. nó tiết kiệm chỉ mất vài giây ..
JensB

1
Chắc chắn, có những trường hợp sử dụng mà việc liệt kê đầy đủ không quan trọng. Nhưng tại sao phải viết một phương thức tiện ích hạng hai khi bạn có thể viết một phương thức tuyệt vời?
ErikE

Thay thế tốt nhưng không giống như đầu tiên trả về một danh sách các danh sách cho phép bạn lặp lại.
Gareth Hopkins

đổi foreach (var batch in Ids2.Batch(5000))thành var gourpBatch = Ids2.Batch(5000)và kiểm tra kết quả hẹn giờ. hoặc thêm vào danh sách var SecBatch = Ids2.Batch2(StartIndex, BatchSize);tôi sẽ quan tâm nếu kết quả của bạn cho thời gian thay đổi.
Seabizkit

2

Vì vậy, với một chiếc mũ chức năng được bật, điều này có vẻ tầm thường .... nhưng trong C #, có một số nhược điểm đáng kể.

bạn có thể xem đây là tài liệu mở rộng của IEnumerable (google nó và bạn có thể kết thúc trong một số tài liệu Haskell, nhưng có thể có một số nội dung F # sử dụng mở rộng, nếu bạn biết F #, hãy liếc nhìn tài liệu Haskell và nó sẽ làm giác quan).

Unfold có liên quan đến gấp ("tổng hợp") ngoại trừ việc lặp lại thông qua IEnumerable đầu vào, nó lặp lại qua các cấu trúc dữ liệu đầu ra (mối quan hệ tương tự giữa IEnumerable và IObservable, trên thực tế, tôi nghĩ IObservable thực hiện "mở" được gọi là tạo. ..)

Dù sao thì trước tiên, bạn cần một phương thức mở, tôi nghĩ điều này hoạt động (tiếc là cuối cùng nó sẽ làm nổ tung ngăn xếp đối với các "danh sách" lớn ... bạn có thể viết điều này một cách an toàn trong F # bằng cách sử dụng sản lượng thay vì concat);

    static IEnumerable<T> Unfold<T, U>(Func<U, IEnumerable<Tuple<U, T>>> f, U seed)
    {
        var maybeNewSeedAndElement = f(seed);

        return maybeNewSeedAndElement.SelectMany(x => new[] { x.Item2 }.Concat(Unfold(f, x.Item1)));
    }

điều này hơi khó hiểu vì C # không triển khai một số thứ mà các ngôn ngữ chức năng coi là đương nhiên ... nhưng về cơ bản nó sẽ lấy một hạt giống và sau đó tạo ra câu trả lời "Có thể" của phần tử tiếp theo trong IEnumerable và hạt giống tiếp theo (Có thể không tồn tại trong C #, vì vậy chúng tôi đã sử dụng IEnumerable để giả mạo nó), và nối phần còn lại của câu trả lời (Tôi không thể đảm bảo cho độ phức tạp "O (n?)" của điều này).

Khi bạn đã làm xong điều đó rồi;

    static IEnumerable<IEnumerable<T>> Batch<T>(IEnumerable<T> xs, int n)
    {
        return Unfold(ys =>
            {
                var head = ys.Take(n);
                var tail = ys.Skip(n);
                return head.Take(1).Select(_ => Tuple.Create(tail, head));
            },
            xs);
    }

tất cả trông khá sạch sẽ ... bạn lấy phần tử "n" làm phần tử "tiếp theo" trong IEnumerable, và "đuôi" là phần còn lại của danh sách chưa được xử lý.

nếu không có gì trong đầu ... bạn đã kết thúc ... bạn trả về "Không có gì" (nhưng được giả mạo là IEnumerable trống>) ... nếu không bạn trả lại phần tử đầu và đuôi để xử lý.

bạn có thể làm điều này bằng cách sử dụng IObservable, có lẽ đã có một phương pháp giống như "Batch" ở đó và bạn có thể sử dụng phương pháp đó.

Nếu nguy cơ tràn ngăn xếp đáng lo ngại (có lẽ nên xảy ra), thì bạn nên triển khai trong F # (và có lẽ đã có một số thư viện F # (FSharpX?) Với điều này).

(Tôi chỉ mới thực hiện một số thử nghiệm thô sơ về điều này, vì vậy có thể có những lỗi kỳ lạ trong đó).


1

Tôi đã viết một triển khai IEnumerable tùy chỉnh hoạt động mà không cần linq và đảm bảo một cách liệt kê duy nhất trên dữ liệu. Nó cũng hoàn thành tất cả những điều này mà không yêu cầu danh sách hoặc mảng sao lưu gây ra vụ nổ bộ nhớ trên các tập dữ liệu lớn.

Dưới đây là một số bài kiểm tra cơ bản:

    [Fact]
    public void ShouldPartition()
    {
        var ints = new List<int> {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
        var data = ints.PartitionByMaxGroupSize(3);
        data.Count().Should().Be(4);

        data.Skip(0).First().Count().Should().Be(3);
        data.Skip(0).First().ToList()[0].Should().Be(0);
        data.Skip(0).First().ToList()[1].Should().Be(1);
        data.Skip(0).First().ToList()[2].Should().Be(2);

        data.Skip(1).First().Count().Should().Be(3);
        data.Skip(1).First().ToList()[0].Should().Be(3);
        data.Skip(1).First().ToList()[1].Should().Be(4);
        data.Skip(1).First().ToList()[2].Should().Be(5);

        data.Skip(2).First().Count().Should().Be(3);
        data.Skip(2).First().ToList()[0].Should().Be(6);
        data.Skip(2).First().ToList()[1].Should().Be(7);
        data.Skip(2).First().ToList()[2].Should().Be(8);

        data.Skip(3).First().Count().Should().Be(1);
        data.Skip(3).First().ToList()[0].Should().Be(9);
    }

Phương pháp mở rộng để phân vùng dữ liệu.

/// <summary>
/// A set of extension methods for <see cref="IEnumerable{T}"/>. 
/// </summary>
public static class EnumerableExtender
{
    /// <summary>
    /// Splits an enumerable into chucks, by a maximum group size.
    /// </summary>
    /// <param name="source">The source to split</param>
    /// <param name="maxSize">The maximum number of items per group.</param>
    /// <typeparam name="T">The type of item to split</typeparam>
    /// <returns>A list of lists of the original items.</returns>
    public static IEnumerable<IEnumerable<T>> PartitionByMaxGroupSize<T>(this IEnumerable<T> source, int maxSize)
    {
        return new SplittingEnumerable<T>(source, maxSize);
    }
}

Đây là lớp triển khai

    using System.Collections;
    using System.Collections.Generic;

    internal class SplittingEnumerable<T> : IEnumerable<IEnumerable<T>>
    {
        private readonly IEnumerable<T> backing;
        private readonly int maxSize;
        private bool hasCurrent;
        private T lastItem;

        public SplittingEnumerable(IEnumerable<T> backing, int maxSize)
        {
            this.backing = backing;
            this.maxSize = maxSize;
        }

        public IEnumerator<IEnumerable<T>> GetEnumerator()
        {
            return new Enumerator(this, this.backing.GetEnumerator());
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return this.GetEnumerator();
        }

        private class Enumerator : IEnumerator<IEnumerable<T>>
        {
            private readonly SplittingEnumerable<T> parent;
            private readonly IEnumerator<T> backingEnumerator;
            private NextEnumerable current;

            public Enumerator(SplittingEnumerable<T> parent, IEnumerator<T> backingEnumerator)
            {
                this.parent = parent;
                this.backingEnumerator = backingEnumerator;
                this.parent.hasCurrent = this.backingEnumerator.MoveNext();
                if (this.parent.hasCurrent)
                {
                    this.parent.lastItem = this.backingEnumerator.Current;
                }
            }

            public bool MoveNext()
            {
                if (this.current == null)
                {
                    this.current = new NextEnumerable(this.parent, this.backingEnumerator);
                    return true;
                }
                else
                {
                    if (!this.current.IsComplete)
                    {
                        using (var enumerator = this.current.GetEnumerator())
                        {
                            while (enumerator.MoveNext())
                            {
                            }
                        }
                    }
                }

                if (!this.parent.hasCurrent)
                {
                    return false;
                }

                this.current = new NextEnumerable(this.parent, this.backingEnumerator);
                return true;
            }

            public void Reset()
            {
                throw new System.NotImplementedException();
            }

            public IEnumerable<T> Current
            {
                get { return this.current; }
            }

            object IEnumerator.Current
            {
                get { return this.Current; }
            }

            public void Dispose()
            {
            }
        }

        private class NextEnumerable : IEnumerable<T>
        {
            private readonly SplittingEnumerable<T> splitter;
            private readonly IEnumerator<T> backingEnumerator;
            private int currentSize;

            public NextEnumerable(SplittingEnumerable<T> splitter, IEnumerator<T> backingEnumerator)
            {
                this.splitter = splitter;
                this.backingEnumerator = backingEnumerator;
            }

            public bool IsComplete { get; private set; }

            public IEnumerator<T> GetEnumerator()
            {
                return new NextEnumerator(this.splitter, this, this.backingEnumerator);
            }

            IEnumerator IEnumerable.GetEnumerator()
            {
                return this.GetEnumerator();
            }

            private class NextEnumerator : IEnumerator<T>
            {
                private readonly SplittingEnumerable<T> splitter;
                private readonly NextEnumerable parent;
                private readonly IEnumerator<T> enumerator;
                private T currentItem;

                public NextEnumerator(SplittingEnumerable<T> splitter, NextEnumerable parent, IEnumerator<T> enumerator)
                {
                    this.splitter = splitter;
                    this.parent = parent;
                    this.enumerator = enumerator;
                }

                public bool MoveNext()
                {
                    this.parent.currentSize += 1;
                    this.currentItem = this.splitter.lastItem;
                    var hasCcurent = this.splitter.hasCurrent;

                    this.parent.IsComplete = this.parent.currentSize > this.splitter.maxSize;

                    if (this.parent.IsComplete)
                    {
                        return false;
                    }

                    if (hasCcurent)
                    {
                        var result = this.enumerator.MoveNext();

                        this.splitter.lastItem = this.enumerator.Current;
                        this.splitter.hasCurrent = result;
                    }

                    return hasCcurent;
                }

                public void Reset()
                {
                    throw new System.NotImplementedException();
                }

                public T Current
                {
                    get { return this.currentItem; }
                }

                object IEnumerator.Current
                {
                    get { return this.Current; }
                }

                public void Dispose()
                {
                }
            }
        }
    }

1

Tôi biết mọi người đã sử dụng các hệ thống phức tạp để thực hiện công việc này, và tôi thực sự không hiểu tại sao. Lấy và bỏ qua sẽ cho phép tất cả các thao tác đó bằng cách sử dụng lựa chọn chung với Func<TSource,Int32,TResult>chức năng biến đổi. Giống:

public IEnumerable<IEnumerable<T>> Buffer<T>(IEnumerable<T> source, int size)=>
    source.Select((item, index) => source.Skip(size * index).Take(size)).TakeWhile(bucket => bucket.Any());

2
Điều này có thể rất kém hiệu quả, bởi vì giá trị đã cho sourcesẽ được lặp lại rất thường xuyên.
Kevin Meier

1
Điều này không chỉ không hiệu quả mà còn có thể tạo ra kết quả không chính xác. Không có gì đảm bảo rằng một kiểu liệt kê sẽ mang lại các phần tử giống nhau khi được liệt kê hai lần. Hãy đếm được đây là một ví dụ: Enumerable.Range(0, 1).SelectMany(_ => Enumerable.Range(0, new Random().Next())).
Theodor Zoulias

1

Chỉ cần thực hiện một dòng khác. Nó hoạt động ngay cả với một danh sách trống, trong trường hợp này bạn nhận được một bộ sưu tập lô kích thước bằng không.

var aList = Enumerable.Range(1, 100).ToList(); //a given list
var size = 9; //the wanted batch size
//number of batches are: (aList.Count() + size - 1) / size;

var batches = Enumerable.Range(0, (aList.Count() + size - 1) / size).Select(i => aList.GetRange( i * size, Math.Min(size, aList.Count() - i * size)));

Assert.True(batches.Count() == 12);
Assert.AreEqual(batches.ToList().ElementAt(0), new List<int>() { 1, 2, 3, 4, 5, 6, 7, 8, 9 });
Assert.AreEqual(batches.ToList().ElementAt(1), new List<int>() { 10, 11, 12, 13, 14, 15, 16, 17, 18 });
Assert.AreEqual(batches.ToList().ElementAt(11), new List<int>() { 100 });

1

Một cách khác là sử dụng toán tử 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();

Bạn không bao giờ phải sử dụng GetAwaiter().GetResult(). Đây là mùi mã cho mã đồng bộ bắt buộc phải gọi mã không đồng bộ.
gfache

-2
    static IEnumerable<IEnumerable<T>> TakeBatch<T>(IEnumerable<T> ts,int batchSize)
    {
        return from @group in ts.Select((x, i) => new { x, i }).ToLookup(xi => xi.i / batchSize)
               select @group.Select(xi => xi.x);
    }

Thêm một số mô tả / văn bản trong câu trả lời của bạn. Chỉ đặt mã có thể có nghĩa là ít hơn hầu hết thời gian.
Ariful Haque
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.