2010-01-13 41 views
267

Tôi có phương thức IEnumerable<T> mà tôi đang sử dụng để tìm các điều khiển trong trang WebForms.IEnumerable and Recursion sử dụng lợi tức lợi nhuận

Phương pháp này đệ quy và tôi đang gặp một số sự cố khi trả về loại tôi muốn khi yield return trả về giá trị của cuộc gọi đệ quy.

Mã của tôi trông như sau:

public static IEnumerable<Control> 
           GetDeepControlsByType<T>(this Control control) 
    { 
     foreach(Control c in control.Controls) 
     { 
      if (c is T) 
      { 
       yield return c; 
      } 

      if(c.Controls.Count > 0) 
      { 
       yield return c.GetDeepControlsByType<T>(); 
      } 
     } 
    } 

này hiện ném một "Không thể chuyển đổi loại khái niệm" lỗi. Tuy nhiên, nếu phương thức này trả về kiểu IEnumerable<Object>, mã được tạo nhưng loại sai được trả về trong đầu ra.

Có cách nào để sử dụng yield return trong khi cũng sử dụng đệ quy không?

+1

http://stackoverflow.com/questions/1815497/enumerating-collections-that-are-not-inherently-ienumerable/ 1815600 # 1815600: liên kết tới câu trả lời "mrydengrens" trên chuỗi "Liệt kê các Bộ sưu tập vốn không phải là IEnumerable?" Mã mẫu của ông dựa trên một bài viết trên blog của Eric Lippert cho bạn thấy cách sử dụng các ngăn xếp trong liệt kê đệ quy với LINQ, do đó tránh việc sử dụng bộ nhớ đắt tiền bởi các trình vòng lặp. imho rất hữu ích! – BillW

+0

BTW. 'if (c.Controls.Count> 0)' -> 'if (c.Controls.Any())', đặc biệt nếu bạn cũng đang sinh lợi :) – mayu

+0

Tôi không nghĩ rằng trường hợp này có lợi từ việc sinh lãi. Để hoàn thành, tôi đã cung cấp một triển khai mà không có 'yield'. Xin vui lòng xem dưới đây :) Và đó là một lớp lót quá :) – mayu

Trả lời

443

Bên trong phương thức trả về IEnumerable<T>, yield return phải trả lại T, không phải là IEnumerable<T>.

Thay

yield return c.GetDeepControlsByType<T>(); 

với:

foreach (var x in c.GetDeepControlsByType<T>()) 
{ 
    yield return x; 
} 
89

Bạn cần phải mang mỗi mục mang lại bởi cuộc gọi đệ quy:

public static IEnumerable<Control> GetDeepControlsByType<T>(this Control control) 
{ 
    foreach(Control c in control.Controls) 
    { 
     if (c is T) 
     { 
      yield return c; 
     } 

     if(c.Controls.Count > 0) 
     { 
      foreach (Control control in c.GetDeepControlsByType<T>()) 
      { 
       yield return control; 
      } 
     } 
    } 
} 

Lưu ý rằng có một chi phí để đệ quy theo cách này - bạn sẽ kết thúc việc tạo ra nhiều lần lặp lại ators, có thể tạo ra một vấn đề hiệu suất nếu bạn có một cây điều khiển thực sự sâu sắc. Nếu bạn muốn tránh điều đó, về cơ bản bạn cần phải thực hiện đệ quy cho mình trong phương thức, để đảm bảo chỉ có một trình lặp (máy trạng thái) được tạo ra. Xem this question để biết thêm chi tiết và triển khai mẫu - nhưng điều này rõ ràng là cũng thêm một số lượng phức tạp nhất định.

+2

Tôi thấy thật ngạc nhiên rằng trong một chủ đề về năng suất Jon đã không đề cập đến 'c.Controls.Count> 0' so với' .Any() ':) – mayu

+0

@ Tymek thực sự nó được đề cập trong câu trả lời được liên kết. –

12

Bạn cần phải trả lại mục từ các điều tra viên, không phải là điều tra viên chính nó, trong thứ hai của bạn yield return

public static IEnumerable<Control> GetDeepControlsByType<T>(this Control control) 
{ 
    foreach (Control c in control.Controls) 
    { 
     if (c is T) 
     { 
      yield return c; 
     } 

     if (c.Controls.Count > 0) 
     { 
      foreach (Control ctrl in c.GetDeepControlsByType<T>()) 
      { 
       yield return ctrl; 
      } 
     } 
    } 
} 
9

Tôi nghĩ rằng bạn phải mang lại lợi nhuận mỗi của các điều khiển trong enumerables.

public static IEnumerable<Control> GetDeepControlsByType<T>(this Control control) 
    { 
     foreach (Control c in control.Controls) 
     { 
      if (c is T) 
      { 
       yield return c; 
      } 

      if (c.Controls.Count > 0) 
      { 
       foreach (Control childControl in c.GetDeepControlsByType<T>()) 
       { 
        yield return childControl; 
       } 
      } 
     } 
    } 
13

Những người khác cung cấp cho bạn câu trả lời đúng, nhưng tôi không nghĩ rằng trường hợp của bạn mang lại lợi ích.

Dưới đây là một đoạn trích đạt được điều tương tự mà không có năng suất.

public static IEnumerable<Control> GetDeepControlsByType<T>(this Control control) 
{ 
    return control.Controls 
       .Where(c => c is T) 
       .Concat(control.Controls 
           .SelectMany(c =>c.GetDeepControlsByType<T>())); 
} 
+2

Không sử dụng LINQ 'yield'as tốt? ;) –

+0

Điều này thật khéo léo. Tôi luôn bị làm phiền bởi vòng lặp 'foreach' bổ sung. Bây giờ tôi có thể làm điều này với lập trình hàm thuần túy! – jsuddsjr

+1

Tôi thích giải pháp này về khả năng đọc, nhưng nó phải đối mặt với cùng một vấn đề hiệu suất với các trình vòng lặp như sử dụng lợi nhuận. @PhilippM: Đã xác minh rằng LINQ sử dụng lợi nhuận https://referencesource.microsoft.com/System.Core/R/577032c8811e20d3.html – Herman

6

Seredynski's syntax là đúng, nhưng bạn nên cẩn thận để tránh yield return trong các chức năng đệ quy vì đó là một thảm họa cho sử dụng bộ nhớ. Xem https://stackoverflow.com/a/3970171/284795 nó vảy bùng nổ với độ sâu (một chức năng tương tự đã sử dụng 10% bộ nhớ trong ứng dụng của tôi).

Một giải pháp đơn giản là sử dụng một danh sách và vượt qua nó với đệ quy https://codereview.stackexchange.com/a/5651/754

/// <summary> 
/// Append the descendents of tree to the given list. 
/// </summary> 
private void AppendDescendents(Tree tree, List<Tree> descendents) 
{ 
    foreach (var child in tree.Children) 
    { 
     descendents.Add(child); 
     AppendDescendents(child, descendents); 
    } 
} 

Hoặc bạn có thể sử dụng một chồng và một vòng lặp while để loại bỏ các cuộc gọi đệ quy https://codereview.stackexchange.com/a/5661/754

15

Như Jon Skeet và Đại Tá Ghi chú hoảng loạn trong câu trả lời của họ, sử dụng yield return trong các phương pháp đệ quy có thể gây ra vấn đề hiệu suất nếu cây rất sâu.

Dưới đây là một không đệ quy phương pháp chung phần mở rộng mà thực hiện một traversal sâu-đầu tiên của một chuỗi các cây:

public static IEnumerable<TSource> RecursiveSelect<TSource>(
    this IEnumerable<TSource> source, Func<TSource, IEnumerable<TSource>> childSelector) 
{ 
    var stack = new Stack<IEnumerator<TSource>>(); 
    var enumerator = source.GetEnumerator(); 

    try 
    { 
     while (true) 
     { 
      if (enumerator.MoveNext()) 
      { 
       TSource element = enumerator.Current; 
       yield return element; 

       stack.Push(enumerator); 
       enumerator = childSelector(element).GetEnumerator(); 
      } 
      else if (stack.Count > 0) 
      { 
       enumerator.Dispose(); 
       enumerator = stack.Pop(); 
      } 
      else 
      { 
       yield break; 
      } 
     } 
    } 
    finally 
    { 
     enumerator.Dispose(); 

     while (stack.Count > 0) // Clean up in case of an exception. 
     { 
      enumerator = stack.Pop(); 
      enumerator.Dispose(); 
     } 
    } 
} 

Không giống như Eric Lippert's solution, RecursiveSelect làm việc trực tiếp với điều tra viên để nó không cần phải gọi Reverse (làm đệm toàn bộ chuỗi trong bộ nhớ).

Sử dụng RecursiveSelect, phương pháp ban đầu của OP có thể được viết lại đơn giản như thế này:

public static IEnumerable<Control> GetDeepControlsByType<T>(this Control control) 
{ 
    return control.Controls.RecursiveSelect(c => c.Controls).Where(c => c is T); 
} 
+0

Để có được mã (tuyệt vời) này để làm việc, tôi đã phải sử dụng 'OfType để có được ControlCollection thành dạng IEnumerable; trong Windows Forms, một ControlCollection không phải là enumerable: kiểm soát trở lại.Controls.OfType () .RecursiveSelect (c => c.Controls.OfType ()) .Where (c => c là T); – BillW

0

Trong khi có rất nhiều câu trả lời tốt ở ngoài kia, tôi vẫn muốn nói thêm rằng người ta có thể sử dụng phương pháp LINQ để thực hiện cùng Điều, .

Ví dụ, mã gốc của OP có thể được viết lại như sau:

public static IEnumerable<Control> 
          GetDeepControlsByType<T>(this Control control) 
{ 
    return control.Controls.OfType<T>() 
      .Union(control.Controls.SelectMany(c => c.GetDeepControlsByType<T>()));   
} 
+0

Một giải pháp sử dụng cùng cách tiếp cận đó đã được đăng * ba năm trước *. – Servy

+0

@Servy Mặc dù nó tương tự (BTW mà tôi đã bỏ lỡ giữa tất cả các câu trả lời ... trong khi viết câu trả lời này), nó vẫn khác nhau, vì nó sử dụng .OfType <> để lọc và .Union() –

+2

'OfType' thực sự không khác gì mấy. Ít nhất là một sự thay đổi nhỏ.Một điều khiển không thể là một con của nhiều điều khiển, vì vậy cây đi qua là * đã * unqiue. Sử dụng 'Union' thay vì' Concat' là không cần thiết xác minh tính duy nhất của một chuỗi đã được đảm bảo là duy nhất, và do đó là hạ cấp khách quan. – Servy

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