Verim iadesi kullanılarak sayısız ve özyineleme


307

IEnumerable<T>Bir WebForms sayfasında denetimleri bulmak için kullandığım bir yöntem var.

Yöntem özyinelemeli ve yield returnözyinelemeli çağrının değeri returnig olduğunda istediğim türü döndüren bazı sorunlar yaşıyorum .

Kodum aşağıdaki gibi görünüyor:

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

Bu, şu anda bir "İfade türü dönüştürülemiyor" hatası veriyor. Ancak bu yöntem tür döndürürse IEnumerable<Object>, kod oluşturulur, ancak çıkışta yanlış tür döndürülür.

yield returnÖzyineleme yaparken de kullanmanın bir yolu var mı ?


1
stackoverflow.com/questions/1815497/… : "Doğası gereği IEnumerable olmayan Koleksiyonları Numaralandırma?" iş parçacığında "mrydengrens" cevabına bağlantı Örnek kodu, Eric Lippert'in Linq ile özyinelemeli numaralandırmada yığınların nasıl kullanılacağını gösteren, böylece yineleyiciler tarafından muhtemelen pahalı bellek kullanımını önleyen bir blog makalesine dayanmaktadır. imho çok yararlı!
BillW

BTW. if(c.Controls.Count > 0)-> if(c.Controls.Any()), özellikle de siz de
şanslıysanız

Bu davanın getiriden fayda sağlayacağını sanmıyorum. Bütünlük için olmadan bir uygulama sağladım yield. Lütfen aşağıya bakın :) Ve bu da tek astarlı :)
timtam

yield returnÖzyinelemeli işlevlerden kaçınmaya dikkat etmelisiniz , bellek kullanımı patlayıcı bir şekilde ölçeklenir. Bkz. Stackoverflow.com/a/30300257/284795
Albay Panik

Yanıtlar:


485

Donduğu bir yöntem İçinde IEnumerable<T>, yield returndönmek zorundadır T, bir değil IEnumerable<T>.

değiştirmek

yield return c.GetDeepControlsByType<T>();

ile:

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

98

Özyinelemeli çağrı ile verilen öğelerin her birini vermeniz gerekir :

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

Bu şekilde tekrarlamanın bir maliyeti olduğunu unutmayın - çok fazla yineleyici oluşturursunuz, bu da gerçekten derin bir kontrol ağacınız varsa bir performans sorunu oluşturabilir. Bundan kaçınmak istiyorsanız, temel olarak tek bir yineleyici (durum makinesi) oluşturulduğundan emin olmak için özyinelemeyi yöntem içinde kendiniz yapmanız gerekir. Daha fazla ayrıntı ve örnek bir uygulama için bu soruya bakın - ancak bu belli bir karmaşıklık da getirir.


2
Jon c.Controls.Count > 0.Any()
vermekle

@Tymek aslında bağlantılı cevapta bahsedildi.

28

Jon Skeet ve Albay Panic'in yanıtlarında belirttiği gibi, yield returnözyinelemeli yöntemlerde kullanmak ağaç çok derinse performans sorunlarına neden olabilir.

İşte bir ağaç dizisinin derinlik-ilk geçişini gerçekleştiren genel özyinelemeli olmayan bir uzatma yöntemi:

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'in çözümünün aksine , RecursiveSelect doğrudan numaralandırıcılar ile çalışır, böylece Reverse (bellekteki tüm diziyi tamponlar) çağırması gerekmez.

RecursiveSelect kullanarak, OP'nin orijinal yöntemi aşağıdaki gibi yeniden yazılabilir:

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

Çalışmak için bu (mükemmel) kodu almak için, 'OfType ı ControlCollection IEnumerable forma almak için kullanmak zorunda kaldı; Windows Forms'da, bir ControlCollection numaralandırılamaz: return control.Controls.OfType <Control> () .RecursiveSelect <Control> (c => c.Controls.OfType <Control> ()) .Where (c => c T );
BillW

17

Diğerleri size doğru cevabı verdi, ancak davanızın getiriden fayda sağladığını düşünmüyorum.

İşte aynı şeyi başaramayan bir pasaj.

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 kullanmıyor mu yield? ;)
Philipp M

Bu kaygan. Ek foreachdöngü her zaman rahatsız oldum . Şimdi bunu saf fonksiyonel programlama ile yapabilirim!
jsuddsjr

1
Bu çözümü okunabilirlik açısından seviyorum, ancak yineleyicilerde verim kullanmakla aynı performans sorunuyla karşı karşıya. @PhilippM: LINQ'nun verim referanslarını
Herman

Mükemmel bir çözüm için başparmak yukarı.
Tomer W

12

İkincinizde öğeleri numaralandırıcıdan değil, numaralandırıcıdan iade etmeniz gerekir.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

Bence numaralandırılabilir her kontrol dönüş vermek zorunda.

    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'nin sözdizimi doğrudur, ancak yield returnyinelemeli işlevlerden kaçınmaya dikkat etmelisiniz, çünkü bu bellek kullanımı için bir felakettir. Bkz. Https://stackoverflow.com/a/3970171/284795 derinlikle patlayıcı bir şekilde ölçekleniyor (benzer bir işlev, uygulamamda belleğin% 10'unu kullanıyordu).

Basit bir çözüm, bir listeyi kullanmak ve listeyi yineleme ile iletmektir 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);
    }
}

Alternatif olarak, yinelenen çağrıları ortadan kaldırmak için bir yığın ve bir while döngüsü kullanabilirsiniz https://codereview.stackexchange.com/a/5661/754


0

Orada çok iyi cevaplar olsa da, yine de aynı şeyi gerçekleştirmek için LINQ yöntemlerini kullanmak mümkün olduğunu ekleyebilirim.

Örneğin, OP'nin orijinal kodu şu şekilde yeniden yazılabilir:

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

Aynı yaklaşımı kullanan bir çözüm üç yıl önce yayınlandı .
Servy

(Bu cevabı yazarken hangi BTW ... tüm cevaplar arasında cevapsız) benzer olsa da bu filtreye .OfType <> kullanır ve .Union () olarak @Servy, hala farklıdır
yoel halb

2
OfTypeGerçekten meainingful farklı değildir. En fazla küçük bir styalistik değişiklik. Bir denetim, birden çok denetimin alt öğesi olamaz, bu nedenle çapraz ağaç zaten benzersizdir. UnionBunun yerine kullanmak Concat, benzersiz olması garanti edilen bir dizinin benzersizliğini gereksiz yere doğrulamaktır ve bu nedenle nesnel bir düşüştür.
Servy
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.