IEnumerable และ Recursion โดยใช้ผลตอบแทน


307

ฉันมีIEnumerable<T>วิธีที่ฉันใช้เพื่อค้นหาการควบคุมในหน้า WebForms

วิธีการแบบเรียกซ้ำและฉันมีปัญหาบางอย่างกลับประเภทที่ฉันต้องการเมื่อ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)
            {
                yield return c.GetDeepControlsByType<T>();
            }
        }
    }

ขณะนี้มีข้อผิดพลาด "ไม่สามารถแปลงประเภทการแสดงออก" หากอย่างไรก็ตามวิธีการนี้จะคืนค่าชนิดIEnumerable<Object>รหัสจะสร้างขึ้น แต่จะส่งคืนชนิดที่ไม่ถูกต้องในผลลัพธ์

มีวิธีใช้yield returnในขณะที่ใช้การเรียกซ้ำหรือไม่


1
stackoverflow.com/questions/1815497/ … : ลิงก์ไปยังคำตอบ "mrydengrens" บนเธรด "การระบุคอลเล็กชันที่ไม่ได้มีอยู่จริงนับ IEnumerable" โค้ดตัวอย่างของเขาอ้างอิงจากบทความบล็อกโดย Eric Lippert แสดงวิธีใช้สแต็คในการแจงนับซ้ำกับ Linq ดังนั้นจึงหลีกเลี่ยงการใช้หน่วยความจำแพงโดย iterators imho มีประโยชน์มาก!
BillW

BTW if(c.Controls.Count > 0)-> if(c.Controls.Any())โดยเฉพาะถ้าคุณยอม
แพ้

ฉันไม่คิดว่ากรณีนี้ได้ประโยชน์จากการยอม yieldเพื่อความสมบูรณ์ฉันมีให้ดำเนินการได้โดยไม่ต้อง โปรดดูที่ด้านล่าง :) และมันเป็นหนึ่งซับมากเกินไป :)
tymtam

คุณควรระมัดระวังในการหลีกเลี่ยงyield returnฟังก์ชั่นวนซ้ำการใช้งานหน่วยความจำจะเพิ่มขึ้นอย่างมาก ดูstackoverflow.com/a/30300257/284795
พันเอก Panic

คำตอบ:


485

ภายในวิธีการที่ผลตอบแทนIEnumerable<T>, yield returnมีการกลับไม่ใช่TIEnumerable<T>

แทนที่

yield return c.GetDeepControlsByType<T>();

ด้วย:

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

98

คุณต้องให้ผลแต่ละรายการที่ได้จากการเรียกซ้ำ:

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

โปรดทราบว่ามีค่าใช้จ่ายในการเรียกซ้ำด้วยวิธีนี้คุณจะต้องสร้างตัววนซ้ำจำนวนมากซึ่งสามารถสร้างปัญหาด้านประสิทธิภาพได้หากคุณมีโครงสร้างการควบคุมที่ลึกมาก หากคุณต้องการหลีกเลี่ยงปัญหานั้นโดยทั่วไปคุณจำเป็นต้องทำการเรียกซ้ำตัวเองภายในวิธีการเพื่อให้แน่ใจว่ามีตัววนซ้ำหนึ่งตัวเท่านั้น (เครื่องสถานะ) สร้างขึ้น ดูคำถามนี้สำหรับรายละเอียดเพิ่มเติมและตัวอย่างการใช้งาน - แต่นี่เป็นการเพิ่มความซับซ้อนจำนวนหนึ่งด้วย


2
ฉันคิดว่ามันน่าแปลกใจที่ในหัวข้อเกี่ยวกับผลผลิตจอนยังไม่ได้กล่าวถึงc.Controls.Count > 0กับ.Any():)
tymtam

@Tymek จริงๆแล้วมันถูกกล่าวถึงในคำตอบที่เชื่อมโยง

28

ในฐานะที่เป็น Jon Skeet และพันเอก Panic note ในคำตอบของพวกเขาใช้ yield returnวิธีการเรียกซ้ำอาจทำให้เกิดปัญหาด้านประสิทธิภาพหากต้นไม้ลึกมาก

ต่อไปนี้เป็นวิธีส่วนขยายแบบไม่เรียกซ้ำทั่วไปที่ดำเนินการสำรวจเส้นทางที่มีความลึกเป็นลำดับแรกของต้นไม้:

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

แตกต่างจากโซลูชันของ Eric Lippert , RecursiveSelect ทำงานโดยตรงกับตัวแจงนับเพื่อให้ไม่จำเป็นต้องเรียก Reverse (ซึ่งบัฟเฟอร์ลำดับทั้งหมดในหน่วยความจำ)

ใช้ RecursiveSelect วิธีดั้งเดิมของ OP สามารถเขียนใหม่ได้อย่างนี้

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

เพื่อให้ได้รหัส (ยอดเยี่ยม) ในการทำงานฉันต้องใช้ 'OfType เพื่อรับ ControlCollection ในรูปแบบ IEnumerable ใน Windows Forms ControlCollection ไม่สามารถนับได้: return control.Controls.OfType <Control> () .RecursiveSelect <Control> (c => c.Controls.OfType <Control> ()). ที่ไหน (c => c คือ T );
BillW

17

คนอื่นให้คำตอบที่ถูกต้องแก่คุณ แต่ฉันไม่คิดว่าคดีของคุณจะได้ประโยชน์จากการยอม

นี่เป็นตัวอย่างข้อมูลที่ประสบความสำเร็จโดยไม่ยอมแพ้

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
ไม่ใช้ LINQ yieldด้วยใช่ไหม ;)
Philipp M

นี่มันลื่น ฉันถูกforeachห่วงเพิ่มเติมเสมอ ตอนนี้ฉันสามารถทำได้ด้วยการเขียนโปรแกรมฟังก์ชั่นที่บริสุทธิ์!
jsuddsjr

1
ฉันชอบโซลูชันนี้ในแง่ของความสามารถในการอ่าน แต่มันก็ประสบปัญหาประสิทธิภาพเดียวกันกับตัววนซ้ำโดยใช้อัตราผลตอบแทน @PhilippM: ยืนยันว่า LINQ ใช้การอ้างอิง
Herman

ยกนิ้วให้ทางออกที่ดี
Tomer W

12

คุณต้องส่งคืนไอเท็มจากตัวแจงนับไม่ใช่ตัวแจงนับเองในวินาทีของคุณ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

ฉันคิดว่าคุณต้องยอมคืนการควบคุมแต่ละอย่างใน 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;
                }
            }
        }
    }

8

ไวยากรณ์ของ Seredynskiนั้นถูกต้อง แต่คุณควรระวังเพื่อหลีกเลี่ยงการyield returnใช้ฟังก์ชันเรียกซ้ำเนื่องจากเป็นหายนะสำหรับการใช้หน่วยความจำ ดูhttps://stackoverflow.com/a/3970171/284795มันปรับขนาดด้วยความลึก (ฟังก์ชันที่คล้ายกันใช้หน่วยความจำ 10% ในแอปของฉัน)

วิธีแก้ปัญหาง่ายๆคือใช้หนึ่งรายการและส่งต่อด้วยการเรียกซ้ำ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);
    }
}

อีกวิธีหนึ่งคือคุณสามารถใช้ stack และ a loop เพื่อกำจัดการโทรซ้ำโดยเรียก https://codereview.stackexchange.com/a/5661/754


0

ในขณะที่มีคำตอบที่ดีมากมายฉันยังคงเพิ่มว่าเป็นไปได้ที่จะใช้วิธี LINQ ในการทำสิ่งเดียวกันให้สำเร็จ

ตัวอย่างเช่นรหัสต้นฉบับของ OP สามารถเขียนใหม่เป็น:

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

วิธีการแก้ปัญหาโดยใช้วิธีการเดียวกับที่ถูกโพสต์เมื่อสามปีก่อน
Servy

@Servy แม้ว่าจะคล้ายกัน (ซึ่ง BTW ฉันพลาดระหว่างคำตอบทั้งหมด ... ในขณะที่เขียนคำตอบนี้) มันยังคงแตกต่างกันเนื่องจากมันใช้. OpenType <> เพื่อกรองและ. union ()
yoel halb

2
OfTypeไม่ได้จริงๆที่แตกต่างกัน meainingful อย่างน้อยที่สุดการเปลี่ยนแปลงของสไตล์ลิสต์เล็กน้อย การควบคุมไม่สามารถเป็นลูกของการควบคุมหลายเพื่อให้ต้นไม้เหี่ยวแห้งเป็นแล้ว unqiue การใช้UnionแทนConcatนั้นไม่จำเป็นต้องตรวจสอบความเป็นเอกลักษณ์ของลำดับที่ได้รับการรับรองแล้วว่าไม่ซ้ำกันและดังนั้นจึงเป็นการลดระดับวัตถุประสงค์
Servy
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.