En C #, pourquoi une méthode anonyme ne peut-elle pas contenir une déclaration de rendement?
-
10-07-2019 - |
Question
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 pouvais pas utiliser le rendement en méthode anonyme. Je me demande pourquoi Les documents de rendement indiquent simplement que ce n'est pas autorisé.
Comme cela n’était pas autorisé, je viens de créer une liste et d’y ajouter les éléments.
La solution
Eric Lippert a récemment écrit une série d'articles sur pourquoi le rendement n'est pas autorisé dans certains cas.
EDIT2:
- Partie 7 (celui-ci a été posté ultérieurement et traite spécifiquement de 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 donner un aperçu de pourquoi " rendements " ne sont pas autorisés à l'intérieur d'un méthode anonyme ou expression lambda
A:
Bonne question. J'aimerais avoir blocs itérateurs anonymes. Ce serait totalement génial de pouvoir construire vous-même un petit générateur de séquence en place qui a fermé sur local variables. La raison pour laquelle c'est simple: les avantages ne sont pas l'emportent sur les coûts. La génialité de faire des générateurs de séquence en place est en fait assez petit dans le grand schéma de choses et méthodes nominales faire le travail assez bien dans la plupart scénarios. Donc, les avantages ne sont pas cela convaincant.
Les coûts sont élevés. Itérateur la réécriture est la plus compliquée transformation dans le compilateur, et méthode anonyme de réécriture est la deuxième plus compliqué. Anonyme les méthodes peuvent être à l'intérieur d'autres anonymes méthodes, et les méthodes anonymes peuvent être à l'intérieur des blocs d'itérateur. Donc, ce que nous faisons est d'abord nous réécrivons tous méthodes anonymes pour qu'ils deviennent méthodes d'une classe de fermeture. C'est l'avant-dernière chose du compilateur fait avant d’émettre IL pour une méthode. Une fois cette étape effectuée, l’itérateur rewriter peut supposer qu'il n'y a pas méthodes anonymes dans l'itérateur bloc; ils ont tous être réécrits déjà. Par conséquent, l'itérateur le rédacteur peut simplement se concentrer sur réécrire l'itérateur, sans craignant qu'il pourrait y avoir un méthode anonyme non réalisée ici.
De plus, les blocs d'itérateur ne "nichent" jamais, contrairement aux méthodes anonymes. L'itérateur réécrivain peut supposer que tout itérateur les blocs sont "de premier niveau".
Si les méthodes anonymes sont autorisées à contient des blocs d'itérateur, puis les deux ces hypothèses vont par la fenêtre. Vous pouvez avoir un bloc itérateur qui contient une méthode anonyme qui contient une méthode anonyme qui contient un bloc itérateur contient une méthode anonyme, et ... beurk. Maintenant, nous devons écrire une réécriture passe qui peut gérer un itérateur imbriqué blocs et méthodes anonymes imbriquées à en même temps, fusionnant nos deux plus algorithmes compliqués dans un loin algorithme plus compliqué. Il serait être vraiment difficile à concevoir, mettre en œuvre, et test. Nous sommes assez intelligents pour faire alors j'en suis sûr. Nous avons une équipe intelligente ici. Mais nous ne voulons pas prendre ce grand fardeau pour un & quo
Autres conseils
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 d'itérateur
En particulier, les blocs itérateurs sont implémentés par certaines transformations sophistiquées du code du compilateur. Ces transformations auraient un impact sur les transformations qui se produisent à l'intérieur de fonctions anonymes ou de lambdas, de sorte que, dans certaines circonstances, ils essaient tous deux de "convertir" le code en une autre construction incompatible avec l'autre.
En conséquence, toute interaction leur est interdite.
Le fonctionnement des blocs d'itérateurs sous le capot est traité correctement ici .
Comme 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 veut 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 à états. Certains exemples simples peuvent fonctionner avec une bonne quantité de contrôle de cohérence (d'abord avec les fermetures imbriquées (éventuellement arbitrairement)), puis de voir si les classes résultantes de très bas niveau peuvent être transformées en machines d'état d'itérateurs.
Cependant, ce serait
- Beaucoup de travail.
- Impossible de travailler dans tous les cas sans au moins que l'aspect bloc itérateur puisse 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'en tant que classe de clôture à part entière).
- S'il existait une faible probabilité de chevauchement là où il était impossible ou suffisamment difficile de ne pas être mis en œuvre, le nombre de problèmes d'assistance en résultant serait probablement élevé, car de nombreux utilisateurs perdraient le changement subtil.
- Cela peut être très facilement corrigé.
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;
}
Malheureusement, je ne sais pas pourquoi ils ne l'ont pas autorisé, car il est tout à fait possible d'envisager comment cela fonctionnerait.
Cependant, les méthodes anonymes font déjà partie de la "magie du compilateur". en ce sens que la méthode sera extraite soit vers une méthode de la classe existante, soit même vers une toute nouvelle classe, selon qu’il s’agisse ou non de variables locales.
De plus, les méthodes itérateurs utilisant yield
sont également implémentées à l'aide de la magie du compilateur.
Je suppose que l'un de ces deux codes rend le code impossible à identifier par l'autre pièce de magie et qu'il a été décidé de ne pas perdre de temps à le faire pour les versions actuelles du compilateur C #. Bien sûr, ce n'est peut-être pas un choix conscient, et cela ne fonctionne tout simplement pas car 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 d’utilisable en retour.
Je voudrais faire ceci:
IList<T> list = GetList<T>();
var fun = expression.Compile();
return list.Where(item => fun.Invoke(item)).ToList();
Bien entendu, vous avez besoin de System.Core.dll référencé à partir de .NET 3.5 pour la méthode Linq. Et inclure:
using System.Linq;
A bientôt,
Sly
Peut-être que c'est juste une limitation de syntaxe. Dans Visual Basic .NET, qui est très similaire à C #, il est parfaitement possible d’écrire de manière maladroite
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(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(<*>quot;{elem} to {x}");
Console.ReadKey();
}
quot;{elem} to {x}")
Next
Console.ReadKey()
End Sub
Notez également les parenthèses 'here
; la fonction lambda Fonction itératrice
... Fonction de fin
renvoie un IEnumerable (Of Integer)
mais est pas un tel objet lui-même. Il doit être appelé pour obtenir cet objet.
Le code converti par [1] soulève des erreurs en C # 7.3 (CS0149):
<*> Je ne suis absolument pas d'accord avec la raison donnée dans les autres réponses qu'il est difficile pour le compilateur de gérer. La Iterator Function ()
que vous voyez dans l'exemple VB.NET est spécialement créée pour les itérateurs lambda.
En VB, il existe le mot clé Iterator
; il n'a pas d'équivalent C #. IMHO, il n'y a pas de vraie raison pour laquelle ce n'est pas une fonctionnalité de C #.
Donc, si vous voulez vraiment, vraiment, des fonctions d'itérateurs 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 (do Ctrl + F pour F #).