2010-02-09 32 views
14

Tôi có một số dữ liệu có nhiều thuộc tính khác nhau và tôi muốn nhóm dữ liệu đó theo thứ bậc. Ví dụ:Làm cách nào để phân loại dữ liệu theo cấp bậc bằng LINQ?

public class Data 
{ 
    public string A { get; set; } 
    public string B { get; set; } 
    public string C { get; set; } 
} 

tôi muốn này nhóm như:

A1 
- B1 
    - C1 
    - C2 
    - C3 
    - ... 
- B2 
    - ... 
A2 
- B1 
    - ... 
... 

Hiện nay, tôi đã có thể vào nhóm này sử dụng LINQ như vậy mà nhóm đầu chia dữ liệu bằng A, sau đó mỗi nhóm chia bởi B, sau đó mỗi nhóm B chứa các phân nhóm bằng C, vv LINQ trông như thế này (giả sử một chuỗi IEnumerable<Data> gọi data):

var hierarchicalGrouping = 
      from x in data 
      group x by x.A 
       into byA 
       let subgroupB = from x in byA 
           group x by x.B 
            into byB 
            let subgroupC = from x in byB 
                group x by x.C 
            select new 
            { 
             B = byB.Key, 
             SubgroupC = subgroupC 
            } 
       select new 
       { 
        A = byA.Key, 
        SubgroupB = subgroupB 
       }; 

Như bạn có thể thấy, điều này hơi lộn xộn khi phân nhóm nhiều hơn. Có cách nào tốt hơn để thực hiện loại nhóm này không? Nó có vẻ như có nên và tôi chỉ không nhìn thấy nó.

Cập nhật
Cho đến nay, tôi đã phát hiện ra rằng hiện nhóm thứ bậc này bằng cách sử dụng LINQ API thông thạo chứ không phải là ngôn ngữ truy vấn cho là cải thiện khả năng đọc, nhưng nó không cảm thấy rất khô.

Có hai cách tôi đã thực hiện việc này: một cách sử dụng GroupBy với bộ chọn kết quả, phương thức kia sử dụng GroupBy theo sau là cuộc gọi Select. Cả hai có thể được định dạng để có thể đọc được nhiều hơn so với sử dụng ngôn ngữ truy vấn nhưng vẫn không mở rộng tốt.

var withResultSelector = 
    data.GroupBy(a => a.A, (aKey, aData) => 
     new 
     { 
      A = aKey, 
      SubgroupB = aData.GroupBy(b => b.B, (bKey, bData) => 
       new 
       { 
        B = bKey, 
        SubgroupC = bData.GroupBy(c => c.C, (cKey, cData) => 
        new 
        { 
         C = cKey, 
         SubgroupD = cData.GroupBy(d => d.D) 
        }) 
       }) 
     }); 

var withSelectCall = 
    data.GroupBy(a => a.A) 
     .Select(aG => 
     new 
     { 
      A = aG.Key, 
      SubgroupB = aG 
       .GroupBy(b => b.B) 
       .Select(bG => 
      new 
      { 
       B = bG.Key, 
       SubgroupC = bG 
        .GroupBy(c => c.C) 
        .Select(cG => 
       new 
       { 
        C = cG.Key, 
        SubgroupD = cG.GroupBy(d => d.D) 
       }) 
      }) 
     }); 

Những gì tôi muốn ...
tôi có thể mường tượng ra một vài cách mà điều này có thể được thể hiện (giả sử ngôn ngữ và khuôn khổ hỗ trợ nó). Đầu tiên là tiện ích mở rộng GroupBy có một loạt các cặp chức năng để lựa chọn khóa và chọn kết quả, Func<TElement, TKey>Func<TElement, TResult>. Mỗi cặp mô tả nhóm phụ tiếp theo. Tùy chọn này rơi xuống vì mỗi cặp có khả năng sẽ yêu cầu TKeyTResult khác với những người khác, điều đó có nghĩa là GroupBy sẽ cần các tham số hữu hạn và khai báo phức tạp.

Tùy chọn thứ hai sẽ là phương pháp mở rộng SubGroupBy có thể được ghép để tạo ra các nhóm con. SubGroupBy sẽ giống như GroupBy nhưng kết quả sẽ là nhóm trước đó được phân đoạn tiếp theo. Ví dụ:

var groupings = data 
    .GroupBy(x=>x.A) 
    .SubGroupBy(y=>y.B) 
    .SubGroupBy(z=>z.C) 

// This version has a custom result type that would be the grouping data. 
// The element data at each stage would be the custom data at this point 
// as the original data would be lost when projected to the results type. 
var groupingsWithCustomResultType = data 
    .GroupBy(a=>a.A, x=>new { ... }) 
    .SubGroupBy(b=>b.B, y=>new { ... }) 
    .SubGroupBy(c=>c.C, c=>new { ... }) 

Khó khăn với điều này là làm thế nào để thực hiện các phương pháp hiệu quả nhất với sự hiểu biết hiện tại của tôi, mỗi cấp sẽ tái tạo các đối tượng mới để mở rộng các đối tượng trước đó. Lần lặp đầu tiên sẽ tạo các nhóm A, sau đó sẽ tạo các đối tượng có khóa A và các nhóm B, thứ ba sẽ làm lại tất cả và thêm các nhóm C. Điều này có vẻ không hiệu quả (mặc dù tôi nghi ngờ các tùy chọn hiện tại của tôi thực sự làm điều này anyway). Sẽ rất tuyệt nếu các cuộc gọi được truyền xung quanh một mô tả meta về những gì được yêu cầu và các trường hợp chỉ được tạo ra trên lần cuối cùng, nhưng điều đó cũng khó nghe.Lưu ý rằng anh ta tương tự như những gì có thể được thực hiện với GroupBy nhưng không có các cuộc gọi phương thức lồng nhau.

Hy vọng tất cả điều đó có ý nghĩa. Tôi hy vọng tôi đang đuổi theo cầu vồng ở đây, nhưng có lẽ không.

Update - một lựa chọn
Một khả năng khác mà tôi nghĩ là hơn tao nhã hơn đề xuất trước đây của tôi phụ thuộc vào từng nhóm cha mẹ không chỉ là một chìa khóa và một chuỗi các mục con (như trong ví dụ), giống như IGrouping cung cấp hiện nay. Điều đó có nghĩa là một tùy chọn để xây dựng nhóm này sẽ là một loạt các bộ chọn khóa và một bộ chọn kết quả.

Nếu các khóa được giới hạn ở một loại thiết lập, không phải là không hợp lý, thì điều này có thể được tạo thành một chuỗi các bộ chọn khóa và bộ chọn kết quả hoặc bộ chọn kết quả và params của bộ chọn khóa. Tất nhiên, nếu các khóa phải có các loại khác nhau và các cấp độ khác nhau, điều này sẽ trở nên khó khăn một lần nữa ngoại trừ độ sâu hữu hạn của phân cấp do cách tham số generics hoạt động.

Dưới đây là một số ví dụ minh họa về những gì tôi có nghĩa là:

Ví dụ:

public static /*<grouping type>*/ SubgroupBy(
    IEnumerable<Func<TElement, TKey>> keySelectors, 
    this IEnumerable<TElement> sequence, 
    Func<TElement, TResult> resultSelector) 
{ 
    ... 
} 

var hierarchy = data.SubgroupBy(
        new [] { 
         x => x.A, 
         y => y.B, 
         z => z.C }, 
        a => new { /*custom projection here for leaf items*/ }) 

Hoặc:

public static /*<grouping type>*/ SubgroupBy(
    this IEnumerable<TElement> sequence, 
    Func<TElement, TResult> resultSelector, 
    params Func<TElement, TKey>[] keySelectors) 
{ 
    ... 
} 

var hierarchy = data.SubgroupBy(
        a => new { /*custom projection here for leaf items*/ }, 
        x => x.A, 
        y => y.B, 
        z => z.C) 

này không giải quyết được sự thiếu hiệu quả thực hiện, nhưng nó phải giải quyết phức tạp làm tổ. Tuy nhiên, kiểu trả về của nhóm này là gì? Tôi có cần giao diện của riêng mình không hoặc tôi có thể sử dụng IGrouping bằng cách nào đó. Tôi cần bao nhiêu để xác định hoặc biến chiều sâu của hệ thống phân cấp vẫn làm điều này không thể?

Tôi đoán là điều này phải giống như kiểu trả về từ mọi cuộc gọi IGrouping nhưng hệ thống loại suy ra loại đó nếu nó không liên quan đến bất kỳ tham số nào được truyền?

Vấn đề này kéo dài sự hiểu biết của tôi, điều này thật tuyệt vời, nhưng não tôi lại đau.

+0

@Jeff: Bạn có thể đăng loại mã bạn muốn * muốn viết (có lẽ gọi một số loại trợ giúp) và sau đó chúng ta có thể xem chúng ta có thể làm gì? Tôi nghi ngờ đó là một trong những thứ sẽ đòi hỏi sự quá tải khác nhau cho mọi cấp độ phân cấp (ví dụ: một cho 2 cấp độ, một cho 3 vv) nhưng nó vẫn có thể hữu ích. –

+0

@jon skeet: chắc chắn rồi. Tôi sẽ sớm cung cấp bản cập nhật. Tôi cảm thấy có một giải pháp thanh lịch hơn nhưng tôi không thể nhìn thấy nó. Tôi đã thực hiện một nỗ lực để xác định cuộc gọi của tôi ngày hôm qua nhưng nó rơi hôi của quy tắc generics như mỗi sử dụng của Func yêu cầu loại chung khác nhau. –

+0

@Jon Skeet: Đúng vậy, tôi đã cung cấp một số chi tiết về các tùy chọn tôi đã xem xét (ngoài các hạn chế về ngôn ngữ hoặc khuôn khổ) và suy nghĩ chung của tôi. –

Trả lời

8

Here is a description cách bạn có thể triển khai cơ chế nhóm phân cấp.

Từ mô tả này:

Result lớp: Phương pháp

public class GroupResult 
{ 
    public object Key { get; set; } 
    public int Count { get; set; } 
    public IEnumerable Items { get; set; } 
    public IEnumerable<GroupResult> SubGroups { get; set; } 
    public override string ToString() 
    { return string.Format("{0} ({1})", Key, Count); } 
} 

Extension:

public static class MyEnumerableExtensions 
{ 
    public static IEnumerable<GroupResult> GroupByMany<TElement>(
     this IEnumerable<TElement> elements, 
     params Func<TElement, object>[] groupSelectors) 
    { 
     if (groupSelectors.Length > 0) 
     { 
      var selector = groupSelectors.First(); 

      //reduce the list recursively until zero 
      var nextSelectors = groupSelectors.Skip(1).ToArray(); 
      return 
       elements.GroupBy(selector).Select(
        g => new GroupResult 
        { 
         Key = g.Key, 
         Count = g.Count(), 
         Items = g, 
         SubGroups = g.GroupByMany(nextSelectors) 
        }); 
     } 
     else 
      return null; 
    } 
} 

Cách sử dụng:

var result = customers.GroupByMany(c => c.Country, c => c.City); 

Edit:

Dưới đây là một phiên bản cải tiến và gõ đúng của mã này.

public class GroupResult<TItem> 
{ 
    public object Key { get; set; } 
    public int Count { get; set; } 
    public IEnumerable<TItem> Items { get; set; } 
    public IEnumerable<GroupResult<TItem>> SubGroups { get; set; } 
    public override string ToString() 
    { return string.Format("{0} ({1})", Key, Count); } 
} 

public static class MyEnumerableExtensions 
{ 
    public static IEnumerable<GroupResult<TElement>> GroupByMany<TElement>(
     this IEnumerable<TElement> elements, 
     params Func<TElement, object>[] groupSelectors) 
    { 
     if (groupSelectors.Length > 0) 
     { 
      var selector = groupSelectors.First(); 

      //reduce the list recursively until zero 
      var nextSelectors = groupSelectors.Skip(1).ToArray(); 
      return 
       elements.GroupBy(selector).Select(
        g => new GroupResult<TElement> { 
         Key = g.Key, 
         Count = g.Count(), 
         Items = g, 
         SubGroups = g.GroupByMany(nextSelectors) 
        }); 
     } else { 
      return null; 
     } 
    } 
} 
+0

Điều này không xây dựng cho tôi trên 'Items = g' ... và 'IEnumerable Items' nếu tôi đặt 'IEnumerable Items' –

+0

@Prisoner ZERO: Các mục là của TElement chứ không phải của GroupResult. Tôi đã thêm phiên bản được nhập đúng vào bài đăng. – AxelEckenberger

+1

Phiên bản cải tiến không biên dịch. (GroupResult yêu cầu 1 loại đối số.) –

4

Bạn cần có hàm đệ quy. Hàm đệ quy gọi chính nó cho mỗi nút trong cây.

Để thực hiện việc này trong LINQ, bạn có thể use a Y-combinator.

+0

Làm thế nào để làm việc khi tài sản tôi nhóm theo các thay đổi ở mỗi cấp? –

+0

Nó không. Bạn nên thiết lập liên kết tự tham chiếu bằng cách thêm ParentID vào mỗi nút (để bạn luôn tham khảo ParentID ở mỗi cấp), trừ khi tất nhiên số lượng cấp độ cây (chiều sâu lồng nhau) bị giới hạn bởi thiết kế của ứng dụng. –

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