web-dev-qa-db-fra.com

En C #, pourquoi une méthode anonyme ne peut-elle pas contenir une déclaration de rendement?

Je pensais que ce serait bien de faire quelque chose comme ça (avec le lambda faisant un 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 le rendement en méthode anonyme. Je me demande pourquoi. Les yield docs disent simplement que ce n'est pas autorisé.

Comme ce n'était pas autorisé, je viens de créer List et d'y ajouter les éléments.

87
Lance Fisher

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

EDIT2:

  • Partie 7 (celui-ci a été posté 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 nous expliquer pourquoi les "rendements" ne sont pas autorisés dans une méthode anonyme ou une expression lambda

UNE :

Bonne question. J'aimerais avoir des blocs d'itérateurs anonymes. Ce serait totalement génial de pouvoir vous construire un petit générateur de séquence en place qui se referme sur des variables locales. La raison en est simple: les avantages ne l'emportent pas sur les coûts. Le caractère génial de la mise en place de générateurs de séquences 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 importants. 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 des blocs d'itérateur. Par conséquent, ce que nous faisons, c'est d'abord que nous réécrivons 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 le compilateur fait avant d'émettre IL pour une méthode. Une fois cette étape terminée, le réécrivain 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éécriteur de l'itérateur peut simplement se concentrer sur la réécriture de l'itérateur, sans se soucier de la présence d'une méthode anonyme non réalisée.

De plus, les blocs d'itérateur ne se "nichent" jamais, contrairement aux méthodes anonymes. Le réécriteur 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 disparaissent. 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érateurs 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, à implémenter 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é "Nice to have but not necessary". - Eric

110
Thomas Levesque

Eric Lippert a écrit une excellente série d'articles sur les limitations (et les décisions de conception influençant ces choix) sur blocs 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 des lambdas, de sorte que dans certaines circonstances, elles essaieraient toutes les deux de "convertir" le code en une autre construction incompatible avec l'autre.

En conséquence, il leur est interdit d'interaction.

Le fonctionnement des blocs itérateurs sous le capot est bien géré ici .

Comme exemple simple d'une 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 faire une petite machine d'état. Certains exemples simples peuvent fonctionner avec une bonne quantité de vérification de la validité (d'abord traiter les fermetures imbriquées (éventuellement arbitrairement)), puis voir si les classes résultantes de niveau inférieur pourraient être transformées en machines d'état d'itérateur.

Mais ce serait

  1. Beaucoup de travail.
  2. Impossible dans tous les cas de fonctionner sans à tout le moins que l'aspect bloc d'itérateur puisse empêcher l'aspect de fermeture d'appliquer certaines transformations pour l'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 lorsqu'il était impossible ou suffisamment difficile de ne pas être mis en œuvre, le nombre de problèmes de support en résultant serait probablement élevé car le changement de rupture subtil serait perdu pour de nombreux utilisateurs.
  3. Il 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;
}
21
ShuggyCoUk

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

Cependant, les méthodes anonymes sont déjà un morceau de "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érateur utilisant yield sont é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 consacrer 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 concis, 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.

3
Lasse V. Karlsen

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 inclure:

using System.Linq;

À votre santé,

Sournois

1
Sly1024

C'est peut-être juste une limitation de syntaxe. Dans Visual Basic .NET, qui est très similaire à C #, il est parfaitement possible d'écrire maladroitement

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 une 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 suis fortement en désaccord avec la raison donnée dans les autres réponses qu'il est difficile à gérer pour le compilateur. La Iterator Function() que vous voyez dans l'exemple VB.NET est spécialement créée pour les itérateurs lambda.

Dans VB, il y a le mot clé Iterator; il n'a pas d'homologue C #. À mon humble avis, il n'y a aucune raison réelle 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 Partie # 7 dans la réponse de @Thomas Levesque (faites Ctrl + F pour F #).

0
Bolpat