Tout en comprenant le fonctionnement du mot clé yield
, je suis tombé sur link1 et link2 sur StackOverflow qui préconise l'utilisation de yield return
Tout en itérant sur le DataReader et cela convient aussi à mes besoins. Mais cela me fait me demander ce qui se passe, si j'utilise yield return
Comme indiqué ci-dessous et si je n'itère pas à travers DataReader entier, la connexion DB restera-t-elle ouverte pour toujours?
IEnumerable<IDataRecord> GetRecords()
{
SqlConnection myConnection = new SqlConnection(@"...");
SqlCommand myCommand = new SqlCommand(@"...", myConnection);
myCommand.CommandType = System.Data.CommandType.Text;
myConnection.Open();
myReader = myCommand.ExecuteReader(CommandBehavior.CloseConnection);
try
{
while (myReader.Read())
{
yield return myReader;
}
}
finally
{
myReader.Close();
}
}
void AnotherMethod()
{
foreach(var rec in GetRecords())
{
i++;
System.Console.WriteLine(rec.GetString(1));
if (i == 5)
break;
}
}
J'ai essayé le même exemple dans un exemple d'application console et j'ai remarqué lors du débogage que le bloc finalement de GetRecords()
n'était pas exécuté. Comment puis-je assurer la fermeture de DB Connection? Existe-t-il un meilleur moyen que d'utiliser le mot clé yield
? J'essaie de concevoir une classe personnalisée qui sera responsable de l'exécution de certains SQL et procédures stockées sur DB et retournera le résultat. Mais je ne veux pas retourner le DataReader à l'appelant. Je veux également m'assurer que la connexion sera fermée dans tous les scénarios.
Modifier Modification de la réponse à la réponse de Ben car il est incorrect d'attendre des appelants de la méthode qu'ils utilisent correctement la méthode et en ce qui concerne la connexion DB, cela coûtera plus cher si la méthode est appelée plusieurs fois sans raison.
Merci Jakob et Ben pour l'explication détaillée.
Oui, vous serez confronté au problème que vous décrivez: jusqu'à ce que vous ayez fini d'itérer le résultat, vous garderez la connexion ouverte. Il y a deux approches générales auxquelles je peux penser pour traiter ceci:
Vous retournez actuellement un IEnumerable<IDataRecord>
, une structure de données que vous pouvez extraire. Au lieu de cela, vous pouvez basculer votre méthode sur Push ses résultats. Le moyen le plus simple serait de passer un Action<IDataRecord>
qui est appelé à chaque itération:
void GetRecords(Action<IDataRecord> callback)
{
// ...
while (myReader.Read())
{
callback(myReader);
}
}
Notez que étant donné que vous avez affaire à une collection d'éléments, IObservable
/IObserver
peut être une structure de données légèrement plus appropriée, mais à moins que vous n'en ayez besoin, un simple Action
est beaucoup plus simple.
Une alternative consiste simplement à s'assurer que l'itération se termine entièrement avant de revenir.
Vous pouvez normalement le faire en plaçant simplement les résultats dans une liste puis en les renvoyant, mais dans ce cas, il y a la complication supplémentaire de chaque élément étant la même référence pour le lecteur. Vous avez donc besoin de quelque chose pour extraire le résultat dont vous avez besoin du lecteur:
IEnumerable<T> GetRecords<T>(Func<IDataRecord,T> extractor)
{
// ...
var result = new List<T>();
try
{
while (myReader.Read())
{
result.Add(extractor(myReader));
}
}
finally
{
myReader.Close();
}
return result;
}
Votre bloc finally
s'exécutera toujours.
Lorsque vous utilisez yield return
, Le compilateur créera une nouvelle classe imbriquée pour implémenter une machine d'état.
Cette classe contiendra tout le code des blocs finally
en tant que méthodes distinctes. Il gardera une trace des blocs finally
qui doivent être exécutés selon l'état. Tous les blocs finally
nécessaires seront exécutés dans la méthode Dispose
.
Selon la spécification du langage C #, foreach (V v in x) embedded-statement
est étendu à
{
E e = ((C)(x)).GetEnumerator();
try
{
while (e.MoveNext())
{
V v = (V)(T)e.Current;
embedded - statement
}
}
finally {
… // Dispose e
}
}
ainsi l'énumérateur sera supprimé même si vous quittez la boucle avec break
ou return
.
Pour obtenir plus d'informations sur la mise en œuvre de l'itérateur, vous pouvez lire ceci cet article de Jon Skeet
Modifier
Le problème avec une telle approche est que vous comptez sur les clients de votre classe pour utiliser correctement la méthode. Ils pourraient par exemple récupérer directement l'énumérateur et le parcourir dans une boucle while
sans le supprimer.
Vous devriez considérer l'une des solutions suggérées par @BenAaronson.
Quel que soit le comportement spécifique de yield
, votre code contient des chemins d'exécution qui ne disposeront pas correctement vos ressources. Et si votre deuxième ligne lève une exception ou votre troisième? Ou même votre quatrième? Vous aurez besoin d'une chaîne try/finally très complexe, ou vous pouvez utiliser des blocs using
.
IEnumerable<IDataRecord> GetRecords()
{
using(var connection = new SqlConnection(@"..."))
{
connection.Open();
using(var command = new SqlCommand(@"...", connection);
{
using(var reader = command.ExecuteReader())
{
while(reader.Read())
{
// your code here.
yield return reader;
}
}
}
}
}
Les gens ont remarqué que ce n'est pas intuitif, que les personnes qui appellent votre méthode peuvent ne pas savoir qu'énumérer le résultat deux fois appellera la méthode deux fois. Eh bien, bonne chance. C'est ainsi que la langue fonctionne. C'est le signal que IEnumerable<T>
envoie. Il y a une raison pour laquelle cela ne renvoie pas List<T>
ou T[]
. Les gens qui ne le savent pas doivent être éduqués, pas contournés.
Visual Studio possède une fonctionnalité appelée analyse de code statique. Vous pouvez l'utiliser pour savoir si vous avez correctement éliminé les ressources.
Ce que vous avez fait semble mal, mais fonctionnera bien.
Vous devez utiliser un IEnumerator <> , car il hérite également IDisposable .
Mais comme vous utilisez un foreach, le compilateur utilise toujours un IDisposable pour générer un IEnumerator <> pour le foreach.
en fait, le foreach implique beaucoup de choses en interne.