Dlaczego w języku C # anonimowa metoda nie może zawierać instrukcji yield?

87

Pomyślałem, że fajnie byłoby zrobić coś takiego (z lambdą zwracającą wydajność):

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

Okazało się jednak, że nie mogę używać wydajności w metodzie anonimowej. Zastanawiam się, dlaczego. W docs plon tylko powiedzieć, że nie jest dozwolone.

Ponieważ nie było to dozwolone, po prostu utworzyłem Listę i dodałem do niej elementy.

Lance Fisher
źródło
Teraz, gdy możemy mieć anonimowe asynclambdy pozwalające awaitwewnątrz w C # 5.0, chciałbym wiedzieć, dlaczego nadal nie zaimplementowano anonimowych iteratorów z yieldinside. Mniej więcej jest to ten sam generator maszyny stanów.
noseratio

Odpowiedzi:

113

Eric Lippert napisał niedawno serię postów na blogu o tym, dlaczego w niektórych przypadkach zbiory są niedozwolone.

EDYCJA2:

  • Część 7 (ta została opublikowana później i konkretnie dotyczy tego pytania)

Prawdopodobnie znajdziesz tam odpowiedź ...


EDIT1: wyjaśniono to w komentarzach do części 5, w odpowiedzi Erica na komentarz Abhijeeta Patela:

P:

Eric,

Czy możesz też wyjaśnić, dlaczego „zyski” nie są dozwolone w anonimowych metodach lub wyrażeniach lambda

A:

Dobre pytanie. Chciałbym mieć anonimowe bloki iteratorów. Byłoby całkowicie niesamowite móc zbudować sobie mały generator sekwencji na miejscu, który zamyka się na zmiennych lokalnych. Powód jest prosty: korzyści nie przewyższają kosztów. Niesamowitość tworzenia generatorów sekwencji na miejscu jest w rzeczywistości dość niewielka w ogólnym schemacie, a nominalne metody działają wystarczająco dobrze w większości scenariuszy. Więc korzyści nie są tak przekonujące.

Koszty są duże. Przepisywanie iteratora jest najbardziej skomplikowaną transformacją w kompilatorze, a przepisywanie anonimowych metod jest drugim najbardziej skomplikowanym. Metody anonimowe mogą znajdować się w innych metodach anonimowych, a metody anonimowe mogą znajdować się w blokach iteratorów. Dlatego najpierw przepisujemy wszystkie metody anonimowe, aby stały się metodami klasy zamknięcia. Jest to przedostatnia rzecz, którą kompilator wykonuje przed wyemitowaniem IL dla metody. Po wykonaniu tego kroku program przepisujący iterator może założyć, że w bloku iteratora nie ma żadnych anonimowych metod; wszystkie zostały już przepisane. Dlatego narzędzie do ponownego pisania iteratora może po prostu skoncentrować się na przepisaniu iteratora, nie martwiąc się, że może tam być niezrealizowana anonimowa metoda.

Ponadto bloki iteratora nigdy nie „zagnieżdżają się”, w przeciwieństwie do metod anonimowych. Edytor iteratora może założyć, że wszystkie bloki iteratora są „najwyższego poziomu”.

Jeśli metody anonimowe mogą zawierać bloki iteratora, oba te założenia znikają. Możesz mieć blok iteratora, który zawiera metodę anonimową, która zawiera metodę anonimową, która zawiera blok iteratora, który zawiera metodę anonimową, i ... fuj. Teraz musimy napisać przepis przepisujący, który może obsługiwać zagnieżdżone bloki iteratora i zagnieżdżone metody anonimowe w tym samym czasie, łącząc nasze dwa najbardziej skomplikowane algorytmy w jeden znacznie bardziej skomplikowany algorytm. Byłoby naprawdę trudno zaprojektować, wdrożyć i przetestować. Jestem pewien, że jesteśmy na to wystarczająco sprytni. Mamy tu sprytny zespół. Ale nie chcemy brać na siebie tak dużego ciężaru, by mieć „fajną, ale niekonieczną” funkcję. - Eric

Thomas Levesque
źródło
2
Ciekawe, zwłaszcza, że ​​teraz są lokalne funkcje.
Mafii
4
Zastanawiam się, czy ta odpowiedź jest nieaktualna, ponieważ przyniesie zwrot zysku w funkcji lokalnej.
Joshua
2
@Joshua, ale funkcja lokalna to nie to samo, co metoda anonimowa ... zwracanie zysku jest nadal niedozwolone w metodach anonimowych.
Thomas Levesque
21

Eric Lippert napisał doskonałą serię artykułów na temat ograniczeń (i decyzji projektowych wpływających na te wybory) w blokach iteratorów

W szczególności bloki iteratora są implementowane przez pewne wyrafinowane transformacje kodu kompilatora. Przekształcenia te miałyby wpływ na transformacje, które mają miejsce w anonimowych funkcjach lub lambdach, tak że w pewnych okolicznościach obie próbowałyby „przekonwertować” kod na inną konstrukcję, która byłaby niezgodna z drugą.

W rezultacie zabrania się im interakcji.

Sposób działania bloków iteratora pod maską jest tutaj dobrze rozwiązany .

Jako prosty przykład niezgodności:

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

Kompilator jednocześnie chce przekonwertować to na coś takiego:

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

a jednocześnie aspekt iteratora próbuje wykonać swoją pracę, aby stworzyć małą maszynę stanów. Niektóre proste przykłady mogą działać z dużą ilością sprawdzania poprawności (najpierw zajmując się (prawdopodobnie arbitralnie) zagnieżdżonymi zamknięciami), a następnie sprawdzaniem, czy klasy wynikowe z najniższego poziomu można przekształcić w iteratorowe maszyny stanu.

Jednak tak by się stało

  1. Sporo pracy.
  2. Nie mógłby działać we wszystkich przypadkach bez przynajmniej aspektu bloku iteratora, który byłby w stanie uniemożliwić aspektowi zamknięcia stosowanie pewnych transformacji w celu zwiększenia wydajności (takich jak promowanie zmiennych lokalnych do zmiennych instancji, a nie w pełni rozwiniętej klasy zamknięcia).
    • Gdyby istniała choćby niewielka szansa na nakładanie się, gdy nie było to możliwe lub wystarczająco trudne, aby nie zostać wdrożone, liczba wynikających z tego problemów ze wsparciem byłaby prawdopodobnie wysoka, ponieważ wielu użytkowników utraciłoby subtelną zmianę przełomową.
  3. Można to bardzo łatwo obejść.

W twoim przykładzie tak:

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;
}
ShuggyCoUk
źródło
2
Nie ma wyraźnego powodu, dla którego kompilator nie może, po usunięciu wszystkich domknięć, wykonać zwykłej transformacji iteratora. Czy znasz przypadek, który faktycznie stwarzałby pewne trudności? Przy okazji, twoja Magicklasa powinna być Magic<T>.
Qwertie
3

Niestety nie wiem, dlaczego na to nie pozwolili, ponieważ oczywiście można sobie wyobrazić, jak to zadziała.

Jednak metody anonimowe są już kawałkiem „magii kompilatora” w tym sensie, że metoda zostanie wyodrębniona albo do metody w istniejącej klasie, albo nawet do całej nowej klasy, w zależności od tego, czy ma do czynienia ze zmiennymi lokalnymi, czy nie.

Ponadto metody iteratora używające yieldsą również implementowane przy użyciu magii kompilatora.

Domyślam się, że jeden z tych dwóch sprawia, że ​​kod nie jest identyfikowalny dla drugiego elementu magii i że postanowiono nie tracić czasu na tworzenie tej pracy dla bieżących wersji kompilatora C #. Oczywiście może to wcale nie być świadomy wybór i po prostu nie działa, ponieważ nikt nie pomyślał o jego wdrożeniu.

Aby uzyskać w 100% dokładne pytanie, sugerowałbym skorzystanie z witryny Microsoft Connect i zgłoszenie pytania, jestem pewien, że w zamian otrzymasz coś użytecznego.

Lasse V. Karlsen
źródło
1

Zrobiłbym to:

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

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

Oczywiście do metody Linq potrzebny jest plik System.Core.dll, do którego odwołuje się .NET 3.5. I obejmują:

using System.Linq;

Twoje zdrowie,

Chytry


źródło
0

Może to tylko ograniczenie składni. W Visual Basic .NET, który jest bardzo podobny do C #, jest to całkowicie możliwe, a jednocześnie niewygodne do pisania

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

Zwróć także uwagę na nawiasy ' here; funkcja lambda Iterator Function... End Function zwraca obiekt , IEnumerable(Of Integer)ale sam nie jest takim obiektem. Musi zostać wywołany, aby uzyskać ten obiekt.

Przekonwertowany kod przez [1] wywołuje błędy w 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();
}

Zdecydowanie nie zgadzam się z powodem podanym w innych odpowiedziach, że kompilator ma trudności z obsługą. Element, Iterator Function()który widzisz w przykładzie VB.NET, jest specjalnie utworzony dla iteratorów lambda.

W VB jest Iteratorsłowo kluczowe; nie ma odpowiednika w C #. IMHO, nie ma prawdziwego powodu, dla którego nie jest to funkcja C #.

Więc jeśli naprawdę, naprawdę chcesz anonimowych funkcji iteratora, obecnie używaj Visual Basic lub (nie sprawdzałem tego) F #, jak stwierdzono w komentarzu do części 7 w odpowiedzi @Thomas Levesque (wykonaj Ctrl + F dla F #).

Bolpat
źródło