Question

Contexte: une base de données contient une série de chaînes que je souhaite restituer. Traditionnellement, ce serait quelque chose comme ça:

public List<string> GetStuff(string connectionString)
{
    List<string> categoryList = new List<string>();
    using (SqlConnection sqlConnection = new SqlConnection(connectionString))
    {
        string commandText = "GetStuff";
        using (SqlCommand sqlCommand = new SqlCommand(commandText, sqlConnection))
        {
            sqlCommand.CommandType = CommandType.StoredProcedure;

            sqlConnection.Open();
            SqlDataReader sqlDataReader = sqlCommand.ExecuteReader();
            while (sqlDataReader.Read())
            {
                categoryList.Add(sqlDataReader["myImportantColumn"].ToString());
            }
        }
    }
    return categoryList;
}

Mais ensuite, je suppose que le consommateur voudra parcourir les articles sans se soucier de quoi que ce soit d'autre, et j'aimerais ne pas être classé dans une liste en tant que telle, donc si je retourne un IEnumerable tout est bon / flexible. Je pensais donc que je pourrais utiliser un & "Return return &"; tapez la conception pour gérer cela ... quelque chose comme ceci:

public IEnumerable<string> GetStuff(string connectionString)
{
    using (SqlConnection sqlConnection = new SqlConnection(connectionString))
    {
        string commandText = "GetStuff";
        using (SqlCommand sqlCommand = new SqlCommand(commandText, sqlConnection))
        {
            sqlCommand.CommandType = CommandType.StoredProcedure;

            sqlConnection.Open();
            SqlDataReader sqlDataReader = sqlCommand.ExecuteReader();
            while (sqlDataReader.Read())
            {
                yield return sqlDataReader["myImportantColumn"].ToString();
            }
        }
    }
}

Mais maintenant que je lis un peu plus sur le rendement (sur des sites comme celui-ci ... msdn n'a pas semblé le mentionner), c'est apparemment un évaluateur paresseux, qui maintient l'état du populator, par anticipation de quelqu'un demandant la valeur suivante, puis ne l'exécutant que jusqu'à ce qu'il renvoie la valeur suivante.

Cela semble bien dans la plupart des cas, mais avec un appel à une base de données, cela semble un peu risqué. Comme exemple quelque peu artificiel, si quelqu'un demande un IEnumerable à partir de celui que je renseigne à partir d'un appel de base de données, en traverse la moitié, puis reste bloqué dans une boucle ... autant que je peux voir que ma connexion à la base de données est en cours rester ouvert pour toujours.

On dirait que dans certains cas, si l’itérateur ne finit pas, il me manque quelque chose?

Était-ce utile?

La solution

C’est un exercice d’équilibrage: voulez-vous forcer immédiatement toutes les données en mémoire afin de libérer la connexion ou voulez-vous bénéficier du streaming des données, au prix de la perte de connexion pendant tout ce temps? ?

À mon avis, cette décision devrait incomber à l'appelant, qui en sait plus sur ce qu'il veut faire. Si vous écrivez le code à l'aide d'un bloc d'itérateur, l'appelant peut très convertir facilement ce formulaire en continu en un format entièrement tamponné:

List<string> stuff = new List<string>(GetStuff(connectionString));

Si, en revanche, vous vous tamponnez vous-même, l'appelant ne peut pas revenir à un modèle de diffusion en continu.

Donc, j'utiliserais probablement le modèle de diffusion en continu et dirais explicitement dans la documentation ce qu'il fait, et conseillerais à l'appelant de prendre une décision appropriée. Vous pouvez même vouloir fournir une méthode d'assistance pour appeler la version en streaming et la convertir en liste.

Bien sûr, si vous ne faites pas confiance à vos appelants pour prendre la décision appropriée et que vous avez de bonnes raisons de croire qu'ils ne voudront jamais vraiment diffuser les données en continu (par exemple, elles ne renverront jamais beaucoup). pour la liste approche. Dans tous les cas, documentez-le - cela pourrait très bien affecter l'utilisation de la valeur de retour.

Une autre option pour traiter de grandes quantités de données consiste à utiliser des lots, ce qui est assez éloigné de la question de départ, mais c’est une approche différente à prendre en compte dans le cas où la diffusion en continu serait normalement intéressante.

Autres conseils

Vous n'êtes pas toujours en sécurité avec IEnumerable. Si vous quittez la structure appelez GetEnumerator (ce que la plupart des gens vont faire), vous êtes en sécurité. En gros, vous êtes aussi sûr que la prudence du code en utilisant votre méthode:

class Program
{
    static void Main(string[] args)
    {
        // safe
        var firstOnly = GetList().First();

        // safe
        foreach (var item in GetList())
        {
            if(item == "2")
                break;
        }

        // safe
        using (var enumerator = GetList().GetEnumerator())
        {
            for (int i = 0; i < 2; i++)
            {
                enumerator.MoveNext();
            }
        }

        // unsafe
        var enumerator2 = GetList().GetEnumerator();

        for (int i = 0; i < 2; i++)
        {
            enumerator2.MoveNext();
        }
    }

    static IEnumerable<string> GetList()
    {
        using (new Test())
        {
            yield return "1";
            yield return "2";
            yield return "3";
        }
    }

}

class Test : IDisposable
{
    public void Dispose()
    {
        Console.WriteLine("dispose called");
    }
}

Votre choix de laisser la connexion à la base de données ouverte ou non dépend également de votre architecture. Si l'appelant participe à une transaction (et que votre connexion est automatiquement inscrite), la connexion restera néanmoins ouverte par le cadre.

Un autre avantage de yield est que (lorsque vous utilisez un curseur côté serveur), votre code ne doit pas lire toutes les données (exemple: 1 000 éléments) de la base de données, si votre consommateur souhaite sortir de la boucle. plus tôt (exemple: après le 10ème item). Cela peut accélérer l'interrogation des données. Surtout dans un environnement Oracle, où les curseurs côté serveur sont le moyen habituel de récupérer des données.

Il ne vous manque rien. Votre exemple montre comment NE PAS utiliser le rendement. Ajoutez les éléments à une liste, fermez la connexion et renvoyez la liste. La signature de votre méthode peut toujours renvoyer IEnumerable.

Modifier: Cela dit, Jon a un point (tellement surpris!): il existe de rares occasions où le streaming est en fait la meilleure chose à faire du point de vue des performances. Après tout, si nous parlons ici de 100 000 (1 000 000? 10 000 000?) Rangs, vous ne voulez pas tout d'abord le charger en mémoire.

De plus, notez que l'approche IEnumerable < T > est essentiellement ce que les fournisseurs LINQ (LINQ-to-SQL, LINQ-to-Entities) font pour une source de revenu. L'approche présente des avantages, comme le dit Jon. Cependant, il y a aussi des problèmes certains - en particulier (pour moi) en termes de (combinaison de) séparation | abstraction.

Ce que je veux dire ici est que:

  • dans un scénario MVC (par exemple), vous souhaitez que votre "obtention de données" étape pour obtenir réellement les données , afin que vous puissiez le tester fonctionne sur le contrôleur , et non dans la vue (sans avoir à vous rappeler d'appeler .ToList () etc)
  • vous ne pouvez pas garantir qu'une autre implémentation de DAL sera capable de diffuser des données en continu (par exemple, un appel POX / WSE / SOAP ne peut généralement pas transmettre des enregistrements); et vous ne voulez pas forcément rendre le comportement très confus (par exemple, la connexion reste ouverte pendant une itération avec une implémentation et fermée pour une autre)

Cela rejoint un peu ma pensée ici: LINQ pragmatique .

Mais je dois souligner - il y a certainement des moments où la diffusion en continu est hautement souhaitable. Ce n'est pas un simple "toujours contre jamais". chose ...

Manière légèrement plus concise de forcer l'évaluation de l'itérateur:

using System.Linq;

//...

var stuff = GetStuff(connectionString).ToList();

Non, vous êtes sur le bon chemin ... le rendement verrouille le lecteur ... vous pouvez le tester en effectuant un autre appel à la base de données tout en appelant IEnumerable

Cela ne poserait problème que si l'appelant abusait du protocole de IEnumerable < T > . La bonne façon de l’utiliser est d’appeler Dispose quand elle n’est plus nécessaire.

L'implémentation générée par return return prend l'appel Dispose en tant que signal pour exécuter tout bloc finally ouvert, qui dans votre exemple appelle Supprime sur les objets que vous avez créés dans les instructions à l'aide de .

Il existe un certain nombre de fonctionnalités linguistiques (en particulier foreach ) qui facilitent l'utilisation de IEnumerable < T / gt; très facilement.

Vous pouvez toujours utiliser un thread séparé pour mettre les données en mémoire tampon (peut-être dans une file d'attente) tout en effectuant une année pour renvoyer les données. Lorsque l'utilisateur demande des données (renvoyées via une année), un élément est supprimé de la file d'attente. Des données sont également ajoutées en permanence à la file d'attente via le thread séparé. De cette façon, si l'utilisateur demande les données assez rapidement, la file d'attente n'est jamais très pleine et vous n'avez pas à vous soucier des problèmes de mémoire. Si ce n'est pas le cas, la file d'attente se remplira, ce qui n'est peut-être pas si grave. Si vous souhaitez imposer une limite à la mémoire, vous pouvez imposer une taille de file d'attente maximale (à ce stade, l'autre thread attendra que des éléments soient supprimés avant d'en ajouter d'autres). Naturellement, vous voudrez vous assurer que vous gérez les ressources (c'est-à-dire la file d'attente) correctement entre les deux threads.

Vous pouvez également obliger l'utilisateur à transmettre un booléen pour indiquer si les données doivent ou non être mises en mémoire tampon. Si la valeur est true, les données sont mises en mémoire tampon et la connexion est fermée dès que possible. Si la valeur est false, les données ne sont pas mises en mémoire tampon et la connexion à la base de données reste ouverte tant que l'utilisateur en a besoin. Avoir un paramètre booléen oblige l'utilisateur à faire son choix, ce qui lui garantit de connaître le problème.

Je suis tombé sur ce mur plusieurs fois. Les requêtes de base de données SQL ne sont pas facilement lisibles en continu comme les fichiers. Interrogez uniquement le nombre de fois dont vous avez besoin et renvoyez-le sous la forme du conteneur de votre choix ( IList < > , DataTable , etc.). IEnumerable ne vous aidera pas ici.

Vous pouvez plutôt utiliser un SqlDataAdapter et remplir un DataTable. Quelque chose comme ça:

public IEnumerable<string> GetStuff(string connectionString)
{
    DataTable table = new DataTable();
    using (SqlConnection sqlConnection = new SqlConnection(connectionString))
    {
        string commandText = "GetStuff";
        using (SqlCommand sqlCommand = new SqlCommand(commandText, sqlConnection))
        {
            sqlCommand.CommandType = CommandType.StoredProcedure;
            SqlDataAdapter dataAdapter = new SqlDataAdapter(sqlCommand);
            dataAdapter.Fill(table);
        }

    }
    foreach(DataRow row in table.Rows)
    {
        yield return row["myImportantColumn"].ToString();
    }
}

De cette façon, vous interrogez tout d'un coup et fermez la connexion immédiatement, mais vous restez toujours en train d'itérer le résultat. De plus, l'appelant de cette méthode ne peut pas convertir le résultat en liste et faire quelque chose qu'il ne devrait pas faire.

N'utilisez pas le rendement ici. votre échantillon va bien.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top