En C #, pourquoi une méthode anonyme ne peut-elle pas contenir une instruction yield?

87

J'ai pensé que ce serait bien de faire quelque chose comme ça (avec le lambda faisant un retour de rendement):

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

Cependant, j'ai découvert que je ne peux pas utiliser yield en méthode anonyme. Je me demande pourquoi. Les documents de rendement disent simplement que ce n'est pas autorisé.

Comme ce n'était pas autorisé, je viens de créer une liste et j'y ai ajouté les éléments.

Lance Fisher
la source
Maintenant que nous pouvons avoir des asynclambdas anonymes autorisant l' awaitintérieur en C # 5.0, je serais intéressé de savoir pourquoi ils n'ont toujours pas implémenté d'itérateurs anonymes avec yieldinside. Plus ou moins, c'est le même générateur de machine d'état.
noseratio

Réponses:

113

Eric Lippert a récemment écrit une série d'articles de blog expliquant pourquoi le rendement n'est pas autorisé dans certains cas.

EDIT2:

  • Partie 7 (celle-ci a été publiée plus tard et répond spécifiquement à cette question)

Vous y trouverez probablement la réponse ...


EDIT1: cela est expliqué dans les commentaires de la partie 5, dans la réponse d'Eric au commentaire d'Abhijeet Patel:

Q:

Eric,

Pouvez-vous également expliquer pourquoi les "yields" ne sont pas autorisés dans une méthode anonyme ou une expression lambda

UNE :

Bonne question. J'adorerais avoir des blocs d'itérateurs anonymes. Ce serait totalement génial de pouvoir construire soi-même un petit générateur de séquence en place qui se ferme sur les variables locales. La raison pour laquelle ce n'est pas le cas est simple: les avantages ne l'emportent pas sur les coûts. Le fait de créer des générateurs de séquences en place est en fait assez petit dans le grand schéma des choses et les méthodes nominales font assez bien le travail dans la plupart des scénarios. Les avantages ne sont donc pas si convaincants.

Les coûts sont élevés. La réécriture d'itérateur est la transformation la plus compliquée du compilateur, et la réécriture de méthode anonyme est la deuxième plus compliquée. Les méthodes anonymes peuvent être à l'intérieur d'autres méthodes anonymes, et les méthodes anonymes peuvent être à l'intérieur de blocs d'itérateur. Par conséquent, ce que nous faisons, c'est d'abord réécrire toutes les méthodes anonymes afin qu'elles deviennent des méthodes d'une classe de fermeture. C'est l'avant-dernière chose que fait le compilateur avant d'émettre IL pour une méthode. Une fois cette étape terminée, le réécriveur d'itérateur peut supposer qu'il n'y a pas de méthodes anonymes dans le bloc d'itérateur; ils ont tous déjà été réécrits. Par conséquent, le réécriveur d'itérateur peut simplement se concentrer sur la réécriture de l'itérateur, sans se soucier qu'il pourrait y avoir une méthode anonyme non réalisée.

De plus, les blocs d'itérateur ne «s'imbriquent» jamais, contrairement aux méthodes anonymes. Le réécriveur d'itérateur peut supposer que tous les blocs d'itérateur sont de "niveau supérieur".

Si les méthodes anonymes sont autorisées à contenir des blocs d'itérateur, ces deux hypothèses sortent de la fenêtre. Vous pouvez avoir un bloc d'itérateur qui contient une méthode anonyme qui contient une méthode anonyme qui contient un bloc d'itérateur qui contient une méthode anonyme, et ... beurk. Maintenant, nous devons écrire une passe de réécriture qui peut gérer les blocs d'itérateur imbriqués et les méthodes anonymes imbriquées en même temps, fusionnant nos deux algorithmes les plus compliqués en un algorithme beaucoup plus compliqué. Ce serait vraiment difficile à concevoir, à mettre en œuvre et à tester. Nous sommes assez intelligents pour le faire, j'en suis sûr. Nous avons une équipe intelligente ici. Mais nous ne voulons pas assumer ce lourd fardeau pour une fonctionnalité «agréable à avoir mais pas nécessaire». - Éric

Thomas Levesque
la source
2
Intéressant, d'autant plus qu'il y a des fonctions locales maintenant.
Mafii
4
Je me demande si cette réponse n'est pas à jour car il faudra un rendement de rendement dans une fonction locale.
Joshua
2
@Joshua mais une fonction locale n'est pas la même chose qu'une méthode anonyme ... le retour de rendement n'est toujours pas autorisé dans les méthodes anonymes.
Thomas Levesque
21

Eric Lippert a écrit une excellente série d'articles sur les limitations (et les décisions de conception influençant ces choix) sur les blocs d'itérateurs

En particulier, les blocs d'itérateur sont implémentés par des transformations de code de compilateur sophistiquées. Ces transformations auraient un impact sur les transformations qui se produisent à l'intérieur des fonctions anonymes ou lambdas de telle sorte que dans certaines circonstances, ils essaieraient tous les deux de «convertir» le code en une autre construction incompatible avec l'autre.

En conséquence, ils sont interdits d'interaction.

Le fonctionnement des blocs d'itérateur sous le capot est bien traité ici .

A titre d'exemple simple d'incompatibilité:

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

Le compilateur souhaite simultanément convertir ceci en quelque chose comme:

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

et en même temps, l'aspect itérateur essaie de faire son travail pour créer une petite machine à états. Certains exemples simples peuvent fonctionner avec une bonne quantité de vérification de la cohérence (en traitant d'abord les fermetures imbriquées (éventuellement arbitrairement)) puis en vérifiant si les classes résultantes de niveau inférieur pourraient être transformées en machines à états d'itération.

Cependant ce serait

  1. Beaucoup de travail.
  2. Cela ne pourrait pas fonctionner dans tous les cas sans au moins l'aspect bloc d'itérateur pouvant empêcher l'aspect fermeture d'appliquer certaines transformations pour plus d'efficacité (comme la promotion de variables locales en variables d'instance plutôt qu'une classe de fermeture à part entière).
    • S'il y avait même un léger risque de chevauchement là où il était impossible ou suffisamment difficile de ne pas être mis en œuvre, le nombre de problèmes de support qui en résulterait serait probablement élevé car le changement subtil serait perdu pour de nombreux utilisateurs.
  3. Cela peut être très facilement contourné.

Dans votre exemple comme ceci:

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
la source
2
Il n'y a aucune raison claire pour laquelle le compilateur ne peut pas, une fois qu'il a levé toutes les fermetures, effectuer la transformation d'itérateur habituelle. Connaissez-vous un cas qui présenterait en fait des difficultés? Btw, votre Magicclasse devrait l'être Magic<T>.
Qwertie
3

Malheureusement, je ne sais pas pourquoi ils n'ont pas permis cela, car il est bien sûr tout à fait possible d'envisager comment cela fonctionnerait.

Cependant, les méthodes anonymes font déjà partie de la "magie du compilateur" dans le sens où la méthode sera extraite soit vers une méthode de la classe existante, soit même vers une toute nouvelle classe, selon qu'elle traite ou non des variables locales.

De plus, les méthodes d'itération utilisant yieldsont également implémentées à l'aide de la magie du compilateur.

Je suppose que l'un de ces deux rend le code non identifiable à l'autre morceau de magie, et qu'il a été décidé de ne pas passer de temps à faire ce travail pour les versions actuelles du compilateur C #. Bien sûr, ce n'est peut-être pas du tout un choix conscient et cela ne fonctionne tout simplement pas parce que personne n'a pensé à le mettre en œuvre.

Pour une question précise à 100%, je vous suggère d'utiliser le site Microsoft Connect et de signaler une question, je suis sûr que vous obtiendrez quelque chose utilisable en retour.

Lasse V. Karlsen
la source
1

Je ferais ceci:

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

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

Bien sûr, vous avez besoin du System.Core.dll référencé à partir de .NET 3.5 pour la méthode Linq. Et incluez:

using System.Linq;

À votre santé,

Sournois


la source
0

Peut-être que c'est juste une limitation de syntaxe. Dans Visual Basic .NET, qui est très similaire à C #, il est parfaitement possible, bien que difficile à écrire

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

Notez également les parenthèses ' here; la fonction lambda Iterator Function... End Function renvoie un IEnumerable(Of Integer)mais n'est pas un tel objet lui-même. Il doit être appelé pour obtenir cet objet.

Le code converti par [1] génère des erreurs dans 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();
}

Je ne suis pas du tout d'accord avec la raison donnée dans les autres réponses qu'il est difficile pour le compilateur de gérer. Le que Iterator Function()vous voyez dans l'exemple VB.NET est spécialement créé pour les itérateurs lambda.

En VB, il y a le Iteratormot - clé; il n'a pas d'équivalent C #. À mon humble avis, il n'y a aucune vraie raison pour laquelle ce n'est pas une fonctionnalité de C #.

Donc, si vous voulez vraiment, vraiment des fonctions d'itérateur anonymes, utilisez actuellement Visual Basic ou (je ne l'ai pas vérifié) F #, comme indiqué dans un commentaire de la partie 7 dans la réponse de @Thomas Levesque (faites Ctrl + F pour F #).

Bolpat
la source