Ce qui suit est correct:
try
{
Console.WriteLine("Before");
yield return 1;
Console.WriteLine("After");
}
finally
{
Console.WriteLine("Done");
}
Le bloc finally
s'exécute lorsque tout est terminé (IEnumerator<T>
prend en charge IDisposable
pour fournir un moyen de garantir cela même lorsque l'énumération est abandonnée avant la fin).
Mais ce n'est pas correct:
try
{
Console.WriteLine("Before");
yield return 1; // error CS1626: Cannot yield a value in the body of a try block with a catch clause
Console.WriteLine("After");
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
Supposons (pour les besoins de l'argument) qu'une exception est levée par l'un ou l'autre des appels WriteLine
à l'intérieur du bloc try. Quel est le problème avec la poursuite de l'exécution dans le bloc catch
?
Bien sûr, la partie de retour de rendement est (actuellement) incapable de lancer quoi que ce soit, mais pourquoi cela devrait-il nous empêcher d'avoir un try
/catch
englobant pour traiter les exceptions levées avant ou après un yield return
?
Mise à jour: Il y a un commentaire intéressant d'Eric Lippert ici - semble qu'ils ont déjà suffisamment de problèmes pour implémenter correctement le comportement try/finally !
EDIT: La page MSDN sur cette erreur est: http://msdn.Microsoft.com/en-us/library/cs1x15az.aspx . Cela n'explique cependant pas pourquoi.
Je soupçonne que c'est une question d'ordre pratique plutôt que de faisabilité. Je soupçonne qu'il y a très, très peu de fois où cette restriction est en fait un problème qui ne peut pas être résolu - mais la complexité supplémentaire dans le compilateur serait très importante.
Il y a quelques choses comme ça que j'ai déjà rencontrées:
Dans chacun de ces cas, il serait possible de gagner un peu plus de liberté, au prix d'une complexité supplémentaire dans le compilateur. L'équipe a fait le choix pragmatique, pour lequel je les applaudis - je préférerais un langage légèrement plus restrictif avec un compilateur précis à 99,9% (oui, il y a des bugs; j'en ai rencontré un sur SO = juste l'autre jour) qu'une langue plus flexible qui ne pouvait pas compiler correctement.
EDIT: Voici une pseudo-preuve de la raison pour laquelle c'est faisable.
Considérez que:
Transformez maintenant:
try
{
Console.WriteLine("a");
yield return 10;
Console.WriteLine("b");
}
catch (Something e)
{
Console.WriteLine("Catch block");
}
Console.WriteLine("Post");
en (sorte de pseudo-code):
case just_before_try_state:
try
{
Console.WriteLine("a");
}
catch (Something e)
{
CatchBlock();
goto case post;
}
__current = 10;
return true;
case just_after_yield_return:
try
{
Console.WriteLine("b");
}
catch (Something e)
{
CatchBlock();
}
goto case post;
case post;
Console.WriteLine("Post");
void CatchBlock()
{
Console.WriteLine("Catch block");
}
La seule duplication réside dans la configuration des blocs try/catch - mais c'est quelque chose que le compilateur peut certainement faire.
J'ai peut-être manqué quelque chose ici - si oui, faites-le moi savoir!
Toutes les instructions yield
dans une définition d'itérateur sont converties en un état dans une machine à états qui utilise efficacement une instruction switch
pour avancer les états. S'il faisait générer du code pour les instructions yield
dans un try/catch, il devrait dupliquer tout dans le bloc try
pour - chaqueyield
instruction en excluant toutes les autres instructions yield
pour ce bloc. Ce n'est pas toujours possible, en particulier si une instruction yield
dépend d'une précédente.
Je suppose que, en raison de la façon dont la pile d'appels est enroulée/déroulée lorsque vous retournez d'un énumérateur, il devient impossible pour un bloc try/catch de "capturer" réellement l'exception. (parce que le bloc de retour de rendement n'est pas sur la pile, même s'il est à l'origine du bloc d'itération)
Pour avoir une idée de ce dont je parle, configurez un bloc d'itérateur et un foreach à l'aide de cet itérateur. Vérifiez à quoi ressemble la pile d'appels à l'intérieur du bloc foreach, puis vérifiez-la à l'intérieur du bloc try/finally de l'itérateur.
J'ai accepté la réponse de THE INVINCIBLE SKEET jusqu'à ce que quelqu'un de Microsoft vienne verser de l'eau froide sur l'idée. Mais je ne suis pas d'accord avec la question de l'opinion - bien sûr, un compilateur correct est plus important qu'un compilateur complet, mais le compilateur C # est déjà très intelligent pour trier cette transformation pour nous dans la mesure où il le fait. Un peu plus d'exhaustivité dans ce cas rendrait la langue plus facile à utiliser, à enseigner, à expliquer, avec moins de cas Edge ou de pièges. Je pense donc que cela en vaudrait la peine. Quelques gars de Redmond se grattent la tête pendant quinze jours et, par conséquent, des millions de codeurs au cours de la prochaine décennie peuvent se détendre un peu plus.
(Je nourris également un désir sordide de trouver un moyen de faire yield return
lève une exception qui a été insérée dans la machine d'état "de l'extérieur", par le code entraînant l'itération. Mais mes raisons de vouloir cela sont assez obscures.)
En fait, une question que j'ai sur la réponse de Jon concerne le lancement de l'expression de retour de rendement.
De toute évidence, le rendement du rendement 10 n'est pas si mauvais. Mais ce serait mauvais:
yield return File.ReadAllText("c:\\missing.txt").Length;
Il ne serait donc pas plus logique d'évaluer cela à l'intérieur du bloc try/catch précédent:
case just_before_try_state:
try
{
Console.WriteLine("a");
__current = File.ReadAllText("c:\\missing.txt").Length;
}
catch (Something e)
{
CatchBlock();
goto case post;
}
return true;
Le prochain problème serait les blocs try/catch imbriqués et les exceptions renvoyées:
try
{
Console.WriteLine("x");
try
{
Console.WriteLine("a");
yield return 10;
Console.WriteLine("b");
}
catch (Something e)
{
Console.WriteLine("y");
if ((DateTime.Now.Second % 2) == 0)
throw;
}
}
catch (Something e)
{
Console.WriteLine("Catch block");
}
Console.WriteLine("Post");
Mais je suis sûr que c'est possible ...