IEnumerable dan Rekursi menggunakan imbal hasil

307

Saya memiliki IEnumerable<T>metode yang saya gunakan untuk menemukan kontrol di halaman WebForms.

Metode ini rekursif dan saya mengalami beberapa masalah mengembalikan tipe yang saya inginkan ketika yield returnkembali nilai nilai panggilan rekursif.

Kode saya terlihat sebagai berikut:

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

Ini saat ini melontarkan kesalahan "Tidak dapat mengonversi tipe ekspresi". Namun jika metode ini mengembalikan tipe IEnumerable<Object>, kode dibuat, tetapi tipe yang salah dikembalikan dalam output.

Apakah ada cara menggunakan yield returnsementara juga menggunakan rekursi?

Jamie Dixon
sumber
1
stackoverflow.com/questions/1815497/… : tautan ke "mrydengrens" jawab di utas "Enumerating Collections yang tidak dapat dipisahkan secara bawaan IEnumerable?" Kode sampelnya didasarkan pada artikel blog oleh Eric Lippert yang menunjukkan kepada Anda bagaimana menggunakan tumpukan dalam enumerasi rekursif dengan Linq, sehingga menghindari kemungkinan penggunaan memori yang mahal oleh iterator. saya sangat berguna!
BillW
BTW. if(c.Controls.Count > 0)-> if(c.Controls.Any()), terutama jika Anda menghasilkan juga :)
tymtam
Saya tidak berpikir kasus ini mendapat manfaat dari menghasilkan. Untuk kelengkapan saya memberikan implementasi tanpa yield. Silakan lihat di bawah ini :) Dan itu juga satu-liner :)
tymtam
Anda harus berhati-hati untuk menghindari yield returnfungsi rekursif, skala penggunaan memori eksplosif. Lihat stackoverflow.com/a/30300257/284795
Kolonel Panic

Jawaban:

485

Di dalam metode yang mengembalikan IEnumerable<T>, yield returnharus kembali T, bukan IEnumerable<T>.

Menggantikan

yield return c.GetDeepControlsByType<T>();

dengan:

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

Anda harus menghasilkan setiap item yang dihasilkan oleh panggilan rekursif:

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

Perhatikan bahwa ada biaya untuk berulang dengan cara ini - Anda akhirnya akan menciptakan banyak iterator, yang dapat membuat masalah kinerja jika Anda memiliki pohon kontrol yang sangat dalam. Jika Anda ingin menghindari itu, pada dasarnya Anda perlu melakukan rekursi sendiri dalam metode ini, untuk memastikan hanya ada satu iterator (mesin negara) yang dibuat. Lihat pertanyaan ini untuk detail lebih lanjut dan contoh implementasi - tetapi ini jelas menambahkan sejumlah kompleksitas juga.

Jon Skeet
sumber
2
Saya merasa mengejutkan bahwa di utas tentang menghasilkan Jon belum disebutkan c.Controls.Count > 0vs. .Any():)
tymtam
@Tymek sebenarnya disebutkan dalam jawaban yang tertaut.
28

Seperti yang dicatat oleh Jon Skeet dan Kolonel Panic dalam jawaban mereka, menggunakan yield returnmetode rekursif dapat menyebabkan masalah kinerja jika pohonnya sangat dalam.

Berikut adalah metode ekstensi non-rekursif generik yang melakukan traversal kedalaman-pertama dari urutan pohon:

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

Tidak seperti solusi Eric Lippert , RecursiveSelect bekerja langsung dengan enumerator sehingga tidak perlu memanggil Reverse (yang mendukung seluruh urutan dalam memori).

Menggunakan RecursiveSelect, metode asli OP dapat ditulis ulang seperti ini:

public static IEnumerable<Control> GetDeepControlsByType<T>(this Control control)
{
    return control.Controls.RecursiveSelect(c => c.Controls).Where(c => c is T);
}
Michael Liu
sumber
Untuk mendapatkan kode (luar biasa) ini bekerja, saya harus menggunakan 'OfType untuk mendapatkan ControlCollection ke dalam bentuk IEnumerable; di Windows Forms, ControlCollection tidak dapat dihitung: return control.Controls.OfType <Control> () .RecursiveSelect <Control> (c => c.Controls.OfType <Control> ()). Dimana (c => c adalah T );
BillW
17

Yang lain memberi Anda jawaban yang benar, tetapi saya pikir kasus Anda tidak akan diuntungkan.

Berikut cuplikan yang mencapai hal yang sama tanpa menghasilkan.

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>()));
}
tymtam
sumber
2
Tidak menggunakan LINQ yieldjuga? ;)
Philipp M
Ini licin. Saya selalu terganggu oleh foreachloop tambahan . Sekarang saya bisa melakukan ini dengan pemrograman fungsional murni!
jsuddsjr
1
Saya suka solusi ini dalam hal keterbacaan, tetapi menghadapi masalah kinerja yang sama dengan iterator seperti menggunakan hasil. @PhilippM: Diverifikasi bahwa LINQ menggunakan referensikan
Herman
Jempol untuk solusi hebat.
Tomer W
12

Anda harus mengembalikan barang dari pencacah, bukan pencacah itu sendiri, pada detik Andayield 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;
            }
        }
    }
}
Rob Levine
sumber
9

Saya pikir Anda harus menghasilkan pengembalian setiap kontrol di 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;
                }
            }
        }
    }
Torbjörn Hansson
sumber
8

Sintaksis Seredynski benar, tetapi Anda harus berhati-hati untuk menghindari yield returnfungsi rekursif karena itu adalah bencana untuk penggunaan memori. Lihat https://stackoverflow.com/a/3970171/284795 skala itu eksplosif dengan kedalaman (fungsi serupa menggunakan 10% memori di aplikasi saya).

Solusi sederhana adalah menggunakan satu daftar dan memberikannya dengan rekursi 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);
    }
}

Atau Anda dapat menggunakan tumpukan dan loop sementara untuk menghilangkan panggilan rekursif https://codereview.stackexchange.com/a/5661/754

Kolonel Panic
sumber
0

Meskipun ada banyak jawaban bagus di luar sana, saya masih akan menambahkan bahwa adalah mungkin untuk menggunakan metode LINQ untuk mencapai hal yang sama,.

Misalnya, kode asli OP dapat ditulis ulang sebagai:

public static IEnumerable<Control> 
                           GetDeepControlsByType<T>(this Control control)
{
   return control.Controls.OfType<T>()
          .Union(control.Controls.SelectMany(c => c.GetDeepControlsByType<T>()));        
}
halo yoel
sumber
Solusi menggunakan pendekatan yang sama telah diposting tiga tahun lalu .
Servy
@Servy Meskipun mirip (yang BTW saya lewatkan di antara semua jawaban ... saat menulis jawaban ini), masih berbeda, karena menggunakan .OfType <> untuk memfilter, dan .Union ()
yoel halb
2
Ini OfTypesebenarnya bukan perbedaan yang berarti. Paling banter perubahan styalistic minor. Kontrol tidak bisa menjadi anak dari banyak kontrol, jadi pohon yang dilintasi sudah unqiue. Menggunakan Unionalih-alih Concattidak perlu memverifikasi keunikan urutan yang sudah dijamin unik, dan karenanya merupakan penurunan peringkat obyektif.
Servy