C #에서 익명 메서드에 yield 문을 포함 할 수없는 이유는 무엇입니까?


87

나는 다음과 같은 일을하는 것이 좋을 것이라고 생각했습니다 (람다가 수익률을 반환하는 것과 함께).

public IList<T> Find<T>(Expression<Func<T, bool>> expression) where T : class, new()
{
    IList<T> list = GetList<T>();
    var fun = expression.Compile();

    var items = () => {
        foreach (var item in list)
            if (fun.Invoke(item))
                yield return item; // This is not allowed by C#
    }

    return items.ToList();
}

그러나 익명의 방법으로 yield를 사용할 수 없다는 것을 알았습니다. 이유가 궁금합니다. 항복 문서는 그냥이 허용되지 않습니다 말한다.

허용되지 않았기 때문에 List를 만들고 항목을 추가했습니다.


이제 C # 5.0에서 내부를 async허용하는 익명 람다 를 가질 수 있으므로 await내부에서 익명 반복기를 구현하지 않은 이유를 알고 싶습니다 yield. 다소간 동일한 상태 머신 생성기입니다.
noseratio

답변:


113

Eric Lippert는 최근에 어떤 경우에 수율이 허용되지 않는 이유에 대한 일련의 블로그 게시물을 작성했습니다.

EDIT2 :

  • 파트 7 (이 항목은 나중에 게시되었으며 특히이 질문에 대해 다룹니다)

아마 거기에서 답을 찾을 수있을 것입니다 ...


EDIT1 : 이것은 Abhijeet Patel의 의견에 대한 Eric의 답변에서 Part 5의 의견에 설명되어 있습니다.

Q :

에릭,

또한 익명 메서드 또는 람다 식 내에서 "수익"이 허용되지 않는 이유에 대한 통찰력을 제공 할 수 있습니까?

ㅏ :

좋은 질문. 익명의 반복자 블록을 갖고 싶습니다. 로컬 변수에 대해 닫히는 작은 시퀀스 생성기를 직접 만들 수 있다면 정말 멋질 것입니다. 그렇지 않은 이유는 간단합니다. 이점이 비용보다 중요하지 않습니다. 시퀀스 제너레이터를 제자리에 배치하는 것의 굉장함은 실제로 큰 계획에서 매우 작으며 명목상의 방법은 대부분의 시나리오에서 충분히 잘 작동합니다. 따라서 이점은 그다지 매력적이지 않습니다.

비용이 큽니다. 반복자 재 작성은 컴파일러에서 가장 복잡한 변환이고 익명 메서드 재 작성은 두 번째로 복잡합니다. 익명 메서드는 다른 익명 메서드 안에있을 수 있고 익명 메서드는 반복기 블록 안에있을 수 있습니다. 따라서 먼저 모든 익명 메서드를 다시 작성하여 클로저 클래스의 메서드가되도록합니다. 이것은 컴파일러가 메서드에 대해 IL을 방출하기 전에 수행하는 두 번째 마지막 작업입니다. 이 단계가 완료되면 반복기 재 작성기는 반복기 블록에 익명 메서드가 없다고 가정 할 수 있습니다. 그들은 모두 이미 다시 작성되었습니다. 따라서 반복기 재 작성자는 실현되지 않은 익명 메서드가있을 수 있다는 걱정없이 반복기 재 작성에만 집중할 수 있습니다.

또한 반복자 블록은 익명 메서드와 달리 "중첩"되지 않습니다. 반복기 재 작성기는 모든 반복기 블록이 "최상위 수준"이라고 가정 할 수 있습니다.

익명 메서드가 반복기 블록을 포함하도록 허용되면 두 가정이 모두 창 밖으로 나갑니다. 익명 메서드를 포함하는 반복기 블록을 포함하는 익명 메서드를 포함하는 익명 메서드를 포함하는 반복기 블록을 가질 수 있습니다. 이제 중첩 된 반복기 블록과 중첩 된 익명 메서드를 동시에 처리 할 수있는 재 작성 패스를 작성하여 가장 복잡한 두 알고리즘을 훨씬 더 복잡한 하나의 알고리즘으로 병합해야합니다. 디자인, 구현 및 테스트가 정말 어려울 것입니다. 우리는 그렇게 할만큼 똑똑합니다. 여기에 똑똑한 팀이 있습니다. 그러나 우리는 "갖는 것이 좋지만 필요하지 않은"기능에 대해 큰 부담을지고 싶지 않습니다. -에릭


2
특히 현재 로컬 기능이 있기 때문에 흥미 롭습니다.
Mafii

4
이 답변이 지역 함수에서 수익률을 반환하기 때문에 오래된 것인지 궁금합니다.
Joshua

2
@Joshua이지만 로컬 함수는 익명 메서드와 동일하지 않습니다 ... 수율 반환은 여전히 ​​익명 메서드에서 허용되지 않습니다.
Thomas Levesque

21

Eric Lippert는 반복기 블록 의 한계 (및 이러한 선택에 영향을 미치는 설계 결정)에 대한 훌륭한 기사 시리즈를 작성했습니다.

특히 반복기 블록은 일부 정교한 컴파일러 코드 변환으로 구현됩니다. 이러한 변환은 익명 함수 또는 람다 내부에서 발생하는 변환에 영향을 미쳐 특정 상황에서 둘 다 코드를 다른 구조와 호환되지 않는 다른 구조로 '변환'하려고합니다.

결과적으로 상호 작용이 금지됩니다.

반복자 블록이 내부에서 작동하는 방식은 여기서 잘 다루어 집니다 .

비 호환성의 간단한 예 :

public IList<T> GreaterThan<T>(T t)
{
    IList<T> list = GetList<T>();
    var items = () => {
        foreach (var item in list)
            if (fun.Invoke(item))
                yield return item; // This is not allowed by C#
    }

    return items.ToList();
}

컴파일러는 동시에 이것을 다음과 같이 변환하려고합니다.

// inner class
private class Magic
{
    private T t;
    private IList<T> list;
    private Magic(List<T> list, T t) { this.list = list; this.t = t;}

    public IEnumerable<T> DoIt()
    {
        var items = () => {
            foreach (var item in list)
                if (fun.Invoke(item))
                    yield return item;
        }
    }
}

public IList<T> GreaterThan<T>(T t)
{
    var magic = new Magic(GetList<T>(), t)
    var items = magic.DoIt();
    return items.ToList();
}

동시에 반복자 측면은 작은 상태 기계를 만드는 작업을 수행하려고합니다. 특정 간단한 예제는 상당한 양의 온 전성 검사 (먼저 (임의로) 중첩 된 클로저를 처리)와 함께 작동 한 다음 맨 아래 수준의 결과 클래스가 반복기 상태 머신으로 변환 될 수 있는지 확인합니다.

그러나 이것은

  1. 꽤 많은 일.
  2. 적어도 반복기 블록 측면이 효율성을 위해 특정 변환을 적용하는 것을 방지 할 수있는 반복기 블록 측면 없이는 모든 경우에서 작동 할 수 없습니다 (예 : 완전히 본격적인 폐쇄 클래스가 아닌 인스턴스 변수로 지역 변수를 승격).
    • 구현이 불가능하거나 충분히 어려운 부분에서 약간의 중복 가능성이있는 경우 미묘한 변경 사항이 많은 사용자에게 손실되므로 그로 인한 지원 문제의 수가 많을 수 있습니다.
  3. 매우 쉽게 해결할 수 있습니다.

귀하의 예에서 다음과 같습니다.

public IList<T> Find<T>(Expression<Func<T, bool>> expression) 
    where T : class, new()
{
    return FindInner(expression).ToList();
}

private IEnumerable<T> FindInner<T>(Expression<Func<T, bool>> expression) 
    where T : class, new()
{
    IList<T> list = GetList<T>();
    var fun = expression.Compile();
    foreach (var item in list)
        if (fun.Invoke(item))
            yield return item;
}

2
컴파일러가 모든 클로저를 해제 한 후에는 일반적인 반복자 변환을 수행 할 수없는 명확한 이유가 없습니다. 실제로 어려움을 겪는 사례를 알고 있습니까? Btw, Magic수업은 Magic<T>.
Qwertie 2013

3

불행히도 나는 그들이 이것을 허용하지 않은 이유를 모르겠습니다. 물론 이것이 어떻게 작동하는지 상상하는 것이 전적으로 가능하기 때문입니다.

그러나 익명 메서드는 로컬 변수를 처리하는지 여부에 따라 기존 클래스의 메서드 또는 완전히 새로운 클래스로 메서드가 추출된다는 점에서 이미 "컴파일러 마법"의 일부입니다.

또한 사용하는 반복기 메서드 yield도 컴파일러 매직을 사용하여 구현됩니다.

내 생각 엔이 두 가지 중 하나가 다른 마법의 코드를 식별 할 수 없게 만들고 현재 버전의 C # 컴파일러에서이 작업을 수행하는 데 시간을 소비하지 않기로 결정한 것입니다. 물론, 그것은 의식적인 선택이 아닐 수도 있고, 아무도 그것을 구현할 생각이 없기 때문에 작동하지 않을 수도 있습니다.

100 % 정확한 질문에 대해서는 Microsoft Connect 사이트 를 사용하고 질문을보고하는 것이 좋습니다 . 그 대가로 유용한 정보를 얻을 수있을 것입니다.


1

나는 이것을 할 것이다 :

IList<T> list = GetList<T>();
var fun = expression.Compile();

return list.Where(item => fun.Invoke(item)).ToList();

물론 Linq 메서드를 위해 .NET 3.5에서 참조 된 System.Core.dll이 필요합니다. 다음을 포함합니다.

using System.Linq;

건배,

교활한


0

아마도 구문 제한 일 수도 있습니다. C #과 매우 유사한 Visual Basic .NET에서는 작성하기 어색하면서도 완벽하게 가능합니다.

Sub Main()
    Console.Write("x: ")
    Dim x = CInt(Console.ReadLine())
    For Each elem In Iterator Function()
                         Dim i = x
                         Do
                             Yield i
                             i += 1
                             x -= 1
                         Loop Until i = x + 20
                     End Function()
        Console.WriteLine($"{elem} to {x}")
    Next
    Console.ReadKey()
End Sub

또한 괄호에 유의하십시오 ' here. 람다 함수는 Iterator Function... End Function 반환IEnumerable(Of Integer)하지만, 하지 않습니다 이러한 개체 자체. 해당 개체를 가져 오려면 호출해야합니다.

[1]에 의해 변환 된 코드는 C # 7.3 (CS0149)에서 오류를 발생시킵니다.

static void Main()
{
    Console.Write("x: ");
    var x = System.Convert.ToInt32(Console.ReadLine());
    // ERROR: CS0149 - Method name expected 
    foreach (var elem in () =>
    {
        var i = x;
        do
        {
            yield return i;
            i += 1;
            x -= 1;
        }
        while (!i == x + 20);
    }())
        Console.WriteLine($"{elem} to {x}");
    Console.ReadKey();
}

나는 컴파일러가 처리하기 어렵다는 다른 답변에 주어진 이유에 강력히 동의하지 않습니다. Iterator Function()당신은 VB.NET 예에서 볼 특히 람다 반복자 생성됩니다.

VB에는 Iterator키워드가 있습니다. C # 대응 물이 없습니다. IMHO, 이것이 C #의 기능이 아닌 실제 이유는 없습니다.

그래서 정말로 익명의 반복기 함수를 원한다면 @Thomas Levesque의 답변 (F #의 경우 Ctrl + F) 의 Part # 7 주석에 명시된대로 현재 Visual Basic 또는 (확인하지 않았습니다) F #을 사용 합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.