Vô số và đệ quy sử dụng lợi nhuận


307

Tôi có một IEnumerable<T>phương pháp mà tôi đang sử dụng để tìm các điều khiển trong trang WebForms.

Phương thức này là đệ quy và tôi gặp một số vấn đề khi trả về kiểu tôi muốn khi yield returntrả 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>();
            }
        }
    }

Điều này hiện gây ra lỗi "Không thể chuyển đổi loại biểu thức". Tuy nhiên, nếu phương thức này trả về kiểu IEnumerable<Object>, mã sẽ xây dựng, nhưng loại sai được trả về trong đầu ra.

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


1
stackoverflow.com/questions/1815497/ trên : liên kết đến câu trả lời "mrydengren" 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 anh ấy dựa trên một bài viết trên blog của Eric Lippert chỉ cho bạn cách sử dụng ngăn xếp trong phép liệt kê đệ quy với Linq, do đó tránh việc sử dụng bộ nhớ đắt tiền của các trình vòng lặp. Imho rất hữu ích!
BillW

BTW. if(c.Controls.Count > 0)-> if(c.Controls.Any()), đặc biệt nếu bạn cũng cho năng suất :)
tymtam

Tôi không nghĩ rằng trường hợp này được hưởng lợi từ năng suất. Để hoàn thiện tôi đã cung cấp một triển khai mà không có yield. Vui lòng xem bên dưới :) Và đó cũng là một lớp lót :)
tymtam

Bạn nên cẩn thận để tránh yield returntrong các chức năng đệ quy, bộ nhớ sử dụng quy mô bùng nổ. Xem stackoverflow.com/a/30300257/284795
Đại tá Panic

Câu trả lời:


485

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

Thay thế

yield return c.GetDeepControlsByType<T>();

với:

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

98

Bạn cần nhường từng mục được mang lại bởi lệnh 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 - cuối cùng bạn sẽ tạo ra rất nhiều trình lặp, điều này có thể tạo ra vấn đề về hiệu suất nếu bạn có một cây điều khiển thực sự sâu. Nếu bạn muốn tránh điều đó, về cơ bản bạn cần tự thực hiện đệ quy 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. Xem câu hỏi này để biết thêm chi tiết và triển khai mẫu - nhưng điều này rõ ràng cũng thêm một số độ phức tạp nhất định.


2
Tôi thấy ngạc nhiên khi trong một chủ đề về năng suất mà Jon đã không đề cập c.Controls.Count > 0so với .Any():)
tymtam

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

28

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

Đây là một phương thức mở rộng không đệ quy chung , thực hiện một giao dịch theo chiều sâu của chuỗi 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ư giải pháp của Eric Lippert , RecursiveSelect hoạt động trực tiếp với các điều tra viên để không cần gọi Reverse (bộ đệm toàn bộ chuỗi trong bộ nhớ).

Sử dụng RecursiveSelect, phương thức 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);
}

Để làm cho mã (xuất sắc) này hoạt động, tôi đã phải sử dụng 'OfType để đưa ControlCollection thành dạng IEnumerable; trong Windows Forms, ControlCollection không thể đếm được: return control.Controls.OfType <Control> (). RecursiveSelect <Control> (c => c.Controls.OfType <Control> ()) .Where (c => c là T );
BillW

17

Những người khác cung cấp cho bạn câu trả lời chính xác, nhưng tôi không nghĩ trường hợp của bạn được hưởng lợi từ năng suất.

Đây là một đoạn mà đạt được điều tương tự mà không mang lại.

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 yieldlà tốt? ;)
Philipp M

Đây là khéo léo. Tôi luôn bị làm phiền bởi foreachvòng lặp bổ sung . Bây giờ tôi có thể làm điều này với lập trình chức năng 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 vấn đề hiệu năng tương tự với các trình vòng lặp như sử dụng năng suất. @PhilippM: Đã xác minh rằng LINQ sử dụng tài liệu tham khảo
Herman

Thumb up cho một giải pháp tuyệt vời.
Tomer W

12

Bạn cần trả lại các mục từ điều tra viên, chứ không phải chính điều tra viên, trong lần thứ hai của bạnyield 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 cho mỗi điều khiển trong bảng liệt kê.

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

8

Cú pháp của Seredynski là chính xác, nhưng bạn nên cẩn thận để tránh yield returntrong các hàm đệ quy vì đó là một thảm họa cho việc sử dụng bộ nhớ. Xem https://stackoverflow.com/a/3970171/284795 nó có tỷ lệ 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à chuyển 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);
    }
}

Ngoài ra, bạn có thể sử dụng ngăn xếp và vòng lặp while để loại bỏ các cuộc gọi đệ quy https://codereview.stackexchange.com/a/5661/754


0

Mặc dù có nhiều câu trả lời hay ngoài kia, tôi vẫn sẽ nói thêm rằng có thể sử dụng các phương pháp LINQ để thực hiện điều tương tự ,.

Chẳng hạn, mã gốc của OP có thể được viết lại thành:

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

Một giải pháp sử dụng phương pháp tương tự đã được đăng ba năm trước .
Phục vụ

@Servy Mặc dù nó giống nhau (mà BTW tôi đã bỏ qua 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, vì nó sử dụng .OfType <> để lọc và
.Union

2
Đây OfTypekhông thực sự là một sự khác biệt ít ỏi. Nhiều nhất là một thay đổi styalistic nhỏ. Một điều khiển không thể là con của nhiều điều khiển, vì vậy cây đi qua đã không còn tồn tại . Sử dụng Unionthay vì Concatkhông cần thiết phải xác minh tính duy nhất của chuỗi đã được đảm bảo là duy nhất và do đó là một hạ cấp khách quan.
Phục vụ
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.