2010-09-22 35 views
35

Làm thế nào người ta sẽ lấy một danh sách (sử dụng LINQ) và phá vỡ nó thành một danh sách liệt kê phân vùng danh sách ban đầu trên tất cả các entry 8?LINQ Danh sách phân vùng vào danh sách của 8 thành viên

Tôi tưởng tượng một cái gì đó như thế này sẽ liên quan đến Bỏ qua và/hoặc Take, nhưng tôi vẫn còn khá mới với LINQ.

Chỉnh sửa: Sử dụng C#/.Net 3.5

Chỉnh sửa2: Câu hỏi này được viết khác với câu hỏi "trùng lặp" khác. Mặc dù những vấn đề tương tự, câu trả lời trong câu hỏi này là vượt trội: Cả "chấp nhận" Câu trả lời là rất rắn (với tuyên bố yield) cũng như đề nghị của Jon Skeet sử dụng MoreLinq (mà không được khuyến khích trong câu hỏi "khác". Đôi khi các bản sao là tốt ở chỗ chúng buộc phải kiểm tra lại vấn đề.

+0

Bạn đang sử dụng VB hoặc C#? Sự hiện diện của vòng lặp tạo nên sự khác biệt lớn. –

+1

Đây không phải là bản sao. Câu hỏi khác muốn chia danh sách thành các danh sách con của mọi phần tử n-th, do đó danh sách có các phần tử 0, 8, 16, 24, v.v ... và danh sách có các phần tử 1, 9, 17, 25, v.v ... với các phần tử 2, 10, 18, v.v. Người dùng này muốn đột nhập vào danh sách có 0..7 và danh sách có 8..15 và danh sách có 16..24, tương tự như phân trang –

Trả lời

46

Sử dụng các phương pháp khuyến nông sau để phá vỡ các đầu vào thành các tập con

public static class IEnumerableExtensions 
{ 
    public static IEnumerable<List<T>> InSetsOf<T>(this IEnumerable<T> source, int max) 
    { 
     List<T> 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

Tôi sẽ thử điều này ngay bây giờ vì điều này có vẻ khá thông minh ... Ý nghĩ về "lợi nhuận trở lại" xuất hiện trong đầu tôi trong khi nghiền ngẫm điều này, nhưng tôi không thể nhìn thấy một cách rõ ràng để làm điều đó ... Tôi ' sẽ cho bạn biết cách này hoạt động cho tôi. – Pretzel

+0

Thật tuyệt vời! Đó là thực sự frickin 'mát mẻ. Tôi sẽ đi với cái này! Cảm ơn đã giúp đỡ! :-) – Pretzel

36

Chúng tôi chỉ có như vậy một phương pháp trong MoreLINQ như Batch phương pháp:

// As IEnumerable<IEnumerable<T>> 
var items = list.Batch(8); 

hoặc

// As IEnumerable<List<T>> 
var items = list.Batch(8, seq => seq.ToList()); 
+1

Tuyệt vời, điều này được triển khai rất độc đáo, bao gồm một resultSelector (quan trọng để thao tác/sắp xếp danh sách bên trong). –

+0

Phew. Tôi nghĩ có lẽ tôi đã hơi điên khùng khi không thể hình dung ra điều này. Tốt để thấy rằng có một số điều "thiếu" từ LINQ-to-Objects thông thường. :) – Pretzel

+0

@Pretzel: Nó không phải là điều này là không thể sử dụng đồng bằng cũ LINQ ... nó chỉ là nó không phải là terribly hiệu quả hoặc dễ hiểu. Xem câu trả lời của tôi cho một ví dụ "đồng bằng LINQ". – LBushkin

15

Bạn nên sử dụng thư viện lik e MoreLinq, nhưng nếu bạn thực sự phải làm điều này bằng "LINQ đồng bằng", bạn có thể sử dụng GroupBy:

var sequence = new[] {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16}; 

var result = sequence.Select((x, i) => new {Group = i/8, Value = x}) 
        .GroupBy(item => item.Group, g => g.Value) 
        .Select(g => g.Where(x => true)); 

// result is: { {1,2,3,4,5,6,7,8}, {9,10,11,12,13,14,15,16} } 

Về cơ bản, chúng tôi sử dụng các phiên bản của Select() cung cấp một chỉ số cho giá trị được tiêu thụ, chúng tôi chia chỉ số của 8 để xác định nhóm nào mỗi giá trị thuộc về. Sau đó, chúng tôi nhóm chuỗi theo khóa nhóm này. Cuối cùng Select chỉ làm giảm IGrouping<> xuống thành IEnumerable<IEnumerable<T>> (và không thực sự cần thiết vì IGroupingIEnumerable).

Thật dễ dàng, đủ để biến điều này thành phương pháp có thể tái sử dụng bằng cách tính số hằng số 8 trong ví dụ và thay thế bằng thông số được chỉ định. Nó không nhất thiết phải là giải pháp thanh lịch nhất, và nó không còn là một giải pháp lười biếng, streaming ... nhưng nó hoạt động.

Bạn cũng có thể viết phương pháp mở rộng của riêng mình bằng cách sử dụng các khối lặp (yield return) có thể mang lại hiệu suất tốt hơn và sử dụng ít bộ nhớ hơn GroupBy. Đây là phương thức Batch() của MoreLinq làm IIRC.

+0

Cảm ơn bạn đã nhập. Vâng, nó không có vẻ hiệu quả và như bạn có thể tưởng tượng, tôi đã đấu tranh để hiểu làm thế nào tôi có thể làm điều đó với LINQ thường xuyên. (Tôi đang nhìn vào câu trả lời của bạn ngay bây giờ và tôi thực sự không hiểu nó rất tốt.) Tôi sẽ phải fiddle với nó sau này. (Cảm ơn một lần nữa!) – Pretzel

+2

Cách tiếp cận bằng cách sử dụng 'GroupBy()' bị hỏng nếu chuỗi bạn đang lập kế hoạch theo đợt sẽ rất lớn (hoặc vô hạn). Theo như cách nó hoạt động - nó tạo ra một đối tượng ẩn danh liên kết từng mục với chỉ mục của nó, và sau đó nhóm nó thành một chuỗi các chuỗi dựa trên sự chia rẽ bằng '8' (hoặc bất kỳ hằng số khác không). – LBushkin

0

Đưa sẽ không thể rất hiệu quả, bởi vì nó không loại bỏ các mục thực hiện.

tại sao không sử dụng một vòng lặp đơn giản:

public IEnumerable<IList<T>> Partition<T>(this/* <-- see extension methods*/ IEnumerable<T> src,int num) 
{ 
    IEnumerator<T> enu=src.getEnumerator(); 
    while(true) 
    { 
     List<T> result=new List<T>(num); 
     for(int i=0;i<num;i++) 
     { 
      if(!enu.MoveNext()) 
      { 
       if(i>0)yield return result; 
       yield break; 
      } 
      result.Add(enu.Current); 
     } 
     yield return result; 
    } 
} 
1

Nó không phải ở tất cả những gì các nhà thiết kế LINQ ban đầu có trong tâm trí, nhưng hãy kiểm tra lạm dụng này groupby:

public static IEnumerable<IEnumerable<T>> BatchBy<T>(this IEnumerable<T> items, int batchSize) 
{ 
    var count = 0; 
    return items.GroupBy(x => (count++/batchSize)).ToList(); 
} 

[TestMethod] 
public void BatchBy_breaks_a_list_into_chunks() 
{ 
    var values = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; 
    var batches = values.BatchBy(3); 
    batches.Count().ShouldEqual(4); 
    batches.First().Count().ShouldEqual(3); 
    batches.Last().Count().ShouldEqual(1); 
} 

Tôi nghĩ rằng nó thắng giải "golf" cho câu hỏi này. Các ToList là rất quan trọng vì bạn muốn chắc chắn rằng các nhóm đã thực sự được thực hiện trước khi bạn cố gắng làm bất cứ điều gì với đầu ra.Nếu bạn loại bỏ các ToList, bạn sẽ nhận được một số tác dụng phụ lạ.

+0

Để ghi lại, phiên bản dựa trên "lợi nhuận" của Handcraftsman hoạt động tốt hơn nhiều, nhưng tôi vẫn thích khía cạnh "Này, bạn không phải làm điều đó" của mã này. – Mel

+0

-1 Câu trả lời này là sai. Nếu bạn thay thế phân chia bằng toán tử modulo, bạn nhận được batchSize số phân vùng phù hợp hơn cho luồng này http://stackoverflow.com/questions/438188/split-a-collection-into-n-parts-with-linq – nawfal

+0

Tôi không đồng ý . Nếu bạn sử dụng modulo, thì bạn đang gán các mục cho các nhóm bằng phần còn lại. Bạn sẽ nhận được mục 0 trong nhóm 0, mục 1 trong nhóm 1, v.v. Điều này sẽ hoàn toàn tranh giành thứ tự của họ. Sử dụng phân chia số nguyên, như tôi đã làm ở đây, có nghĩa là các mục 0-2 đi vào nhóm 0, 3-5 đi trong nhóm 1, vv Tôi tin rằng đây là những gì đã được dự định. Nếu bạn đồng ý, vui lòng xóa -1 khỏi câu trả lời của tôi. – Mel

0
from b in Enumerable.Range(0,8) select items.Where((x,i) => (i % 8) == b); 
+0

+1 Nhưng lưu ý nhóm này mỗi mục 8, tức là bạn nhận được {0,8,16}, {1,9,17}, ... –

+0

Điều này là nhiều hơn về tách hơn phân vùng - phù hợp hơn [ở đây] (http: //stackoverflow.com/questions/438188/split-a-collection-into-n-parts-with-linq) – nawfal

+0

Hơn nữa, điều này có thể cung cấp cho IEnumerables bên trong trống nếu số 8 lớn hơn số lượng mục trong 'mục' có thể hoặc có thể không được mong muốn. Tôi kết hợp câu trả lời của bạn trong các chủ đề khác anyway .. – nawfal

0

Giải pháp đơn giản nhất được đưa ra bởi Mel:

public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> items, 
                 int partitionSize) 
{ 
    int i = 0; 
    return items.GroupBy(x => i++/partitionSize).ToArray(); 
} 

ngắn gọn nhưng chậm hơn. Phương thức trên chia tách một IEnumerable thành các khối kích thước cố định mong muốn với tổng số khối không quan trọng. Để chia một IEnumerable vào số N của khối có kích thước tương đương hoặc gần với kích thước bằng nhau, bạn có thể làm:

public static IEnumerable<IEnumerable<T>> Split<T>(this IEnumerable<T> items, 
                int numOfParts) 
{ 
    int i = 0; 
    return items.GroupBy(x => i++ % numOfParts); 
} 

Đẩy nhanh tiến độ điều, một cách tiếp cận đơn giản sẽ làm gì:

public static IEnumerable<IEnumerable<T>> Partition<T>(this IEnumerable<T> items, 
                 int partitionSize) 
{ 
    if (partitionSize <= 0) 
     throw new ArgumentOutOfRangeException("partitionSize"); 

    int innerListCounter = 0; 
    int numberOfPackets = 0; 
    foreach (var item in items) 
    { 
     innerListCounter++; 
     if (innerListCounter == partitionSize) 
     { 
      yield return items.Skip(numberOfPackets * partitionSize).Take(partitionSize); 
      innerListCounter = 0; 
      numberOfPackets++; 
     } 
    } 

    if (innerListCounter > 0) 
     yield return items.Skip(numberOfPackets * partitionSize); 
} 

này nhanh hơn bất cứ điều gì hiện đang trên hành tinh bây giờ :) Các phương pháp tương đương cho hoạt động Splithere

+0

whew, đó là nhanh hơn nhiều! mặc dù http://stackoverflow.com/a/4835541/1037948 bắt đầu cắt bạn ra sau khi một cặp vợ chồng chạy trong linqpad ...;) – drzaus

+0

@drzaus hãy nhớ rằng câu trả lời là một cấu trúc nguy hiểm với các tác dụng phụ. Kể từ khi bên trong và bên ngoài chạy trên cùng một điều tra viên, bạn nhận được kết quả mà bạn không mong đợi. Nếu bạn liệt kê chỉ trình tự bên ngoài, các chuỗi bên trong không còn hợp lệ nữa; hoặc bạn không thể lặp lại các chuỗi bên trong hai lần. Hoặc đơn giản là thử 'var x = returnResult.ElementAt (n) .ToList();', bạn nhận được các kết quả không mong muốn. – nawfal

+0

Vì vậy, gần đây tôi đã thử chạy một số so sánh perf một lần nữa ([test + results here] (https://gist.github.com/zaus/399ca9081912bd6efd7f#file-f2-results-md)) giữa chỉ cần gọi phương thức này và làm điều gì đó với kết quả, và nó không nhanh như một số gợi ý khác. Tui bỏ lỡ điều gì vậy? – drzaus

Các vấn đề liên quan