수율 반환을 사용한 IEnumerable 및 Recursion


307

IEnumerable<T>WebForms 페이지에서 컨트롤을 찾는 데 사용 하는 방법이 있습니다.

이 방법은 재귀 적이며 yield return재귀 호출의 값을 returnig 할 때 원하는 유형을 반환하는 데 문제 가 있습니다.

내 코드는 다음과 같습니다.

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

현재 "표현 유형을 변환 할 수 없습니다"오류가 발생합니다. 그러나이 메소드가 type을 리턴 IEnumerable<Object>하면 코드가 빌드되지만 출력에 잘못된 유형이 리턴됩니다.

yield return재귀도 사용 하는 방법이 있습니까?


1
stackoverflow.com/questions/1815497/… : "내재적으로 IEnumerable이 아닌 컬렉션 열거?"스레드의 "mrydengrens"답변 링크 그의 샘플 코드는 Eric Lippert의 블로그 기사를 기반으로 Linq와 함께 재귀 열거에서 스택을 사용하는 방법을 보여 주므로 반복자가 비싼 메모리 사용을 피할 수 있습니다. 매우 유용한 imho!
BillW

BTW. if(c.Controls.Count > 0)-> if(c.Controls.Any()), 특히 양보하는 경우 :)
tymtam

나는이 사건이 양보로부터 이익을 얻는다고 생각하지 않는다. 완전성을 위해 나는없이 구현을 제공했습니다 yield. 아래를 참조하십시오 :) 그리고 그것은 하나의 라이너이기도합니다 :)
tymtam

yield return재귀 함수에서는 메모리 사용량이 폭발적으로 증가 하지 않도록주의해야합니다 . 참조 stackoverflow.com/a/30300257/284795을
대령 패닉

답변:


485

반환하는 방법 내부 IEnumerable<T>, yield return반환하는 T,을하지 IEnumerable<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 대령이 답에 언급했듯이, 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);
}

이 (우수한) 코드를 작동 시키려면 ControlOf를 IEnumerable 형식으로 가져 오려면 'OfType'을 사용해야했습니다. Windows Forms에서는 ControlCollection을 열거 할 수 없습니다. return control.Controls.OfType <Control> (). RecursiveSelect <Control> (c => c.Controls.OfType <Control> ()) .Where (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 2016 년

1
가독성 측면 에서이 솔루션을 좋아하지만 반복자를 사용할 때와 동일한 성능 문제에 직면합니다. @PhilippM : LINQ가 yield referencesource.microsoft.com/System.Core/R/…을
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

열거 형의 각 컨트롤을 반환해야한다고 생각합니다.

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

또는 스택과 while 루프를 사용하여 재귀 호출을 제거 할 수 있습니다 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>()));        
}

같은 접근법을 사용하는 솔루션이 3 년 전에 게시 되었습니다 .
Servy

@Servy 비슷하지만 (이 답변을 쓰는 ​​동안 BTW에서 모든 답변 사이에 놓쳤습니다 ...) .OfType <>을 사용하여 필터링하고 .Union ()을 사용하므로 여전히 다릅니다.
yoel halb

2
OfType정말 meainingful 다르지 않다. 기껏해야 사소한 변화가 있습니다. 컨트롤은 여러 컨트롤의 자식이 될 수 없으므로 트래버스 된 트리는 이미 unqiue입니다. Union대신에 사용 하는 Concat것은 이미 고유 한 것으로 보장 된 시퀀스의 고유성을 불필요하게 검증하므로 객관적인 다운 그레이드입니다.
Servy
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.