remarks example cref c# .net database resources yield

example - params comments c#



C#IEnumerador/estructura de rendimiento potencialmente malo? (11)

Antecedentes: tengo un montón de cadenas que obtengo de una base de datos, y quiero devolverlas. Tradicionalmente, sería algo como esto:

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; }

Pero luego me imagino que el consumidor va a querer repetir los elementos y no le importa mucho más, y me gustaría no incluirme en una lista, per se, así que si devuelvo un IEnumerable todo está bien. /flexible. Así que pensé que podría usar un diseño de tipo "retorno de rendimiento" para manejar esto ... algo como esto:

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(); } } } }

Pero ahora que estoy leyendo un poco más sobre rendimiento (en sitios como este ... msdn no parece mencionar esto), aparentemente es un evaluador perezoso, que mantiene el estado del populator, anticipándose a que alguien pregunte para el siguiente valor, y luego solo ejecutarlo hasta que devuelva el siguiente valor.

Esto parece estar bien en la mayoría de los casos, pero con una llamada a DB, esto suena un poco incierto. Como un ejemplo artificial, si alguien solicita un IEnumerable del que estoy completando una llamada de base de datos, atraviesa la mitad y luego se queda atrapado en un bucle ... por lo que puedo ver, mi conexión de base de datos va a funcionar permanecer abierto para siempre

Suena como pedir problemas en algunos casos si el iterador no termina ... ¿me estoy perdiendo algo?


Como nota aparte, el IEnumerable<T> es esencialmente lo que hacen los proveedores de LINQ (LINQ-to-SQL, LINQ-to-Entities) para ganarse la vida. El enfoque tiene ventajas, como dice Jon. Sin embargo, también hay problemas definidos, en particular (para mí) en términos de (la combinación de) separación | abstracción.

Lo que quiero decir es que:

  • en un escenario MVC (por ejemplo) desea que su paso de "obtener datos" obtenga realmente datos , para que pueda probar que funciona en el controlador , no en la vista (sin tener que recordar llamar a .ToList() etc.)
  • no se puede garantizar que otra implementación DAL pueda transmitir datos (por ejemplo, una llamada POX / WSE / SOAP no suele transmitir registros); y no necesariamente quiere que el comportamiento sea confusamente diferente (es decir, la conexión sigue abierta durante la iteración con una implementación, y se cierra por otra)

Esto se relaciona un poco con mis pensamientos aquí: Pragmatic LINQ .

Pero debo enfatizar que definitivamente hay momentos en los que la transmisión es altamente deseable. No es una simple cosa de "siempre contra nunca" ...


Es un acto de equilibrio: ¿quiere forzar todos los datos en la memoria de inmediato para que pueda liberar la conexión, o desea beneficiarse de la transmisión de datos, a costa de inmovilizar la conexión todo ese tiempo?

La forma en que lo veo, esa decisión debería ser potencialmente de la persona que llama, que sabe más acerca de lo que quieren hacer. Si escribe el código usando un bloque de iterador, la persona que llama puede convertir fácilmente ese flujo de forma en un búfer completo:

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

Si, por otro lado, usted mismo realiza el almacenamiento en búfer, no hay forma de que la persona que llama pueda volver a un modelo de transmisión.

Así que probablemente usaría el modelo de transmisión y diría explícitamente en la documentación qué hace, y le aconsejaría a la persona que llama que decida de manera apropiada. Es posible que incluso desee proporcionar un método de ayuda para básicamente llamar a la versión transmitida y convertirla en una lista.

Por supuesto, si no confía en las personas que llaman para tomar la decisión adecuada, y tiene buenas razones para creer que nunca querrán transmitir los datos (por ejemplo, nunca va a regresar mucho de todos modos), vaya a la lista. enfoque. De cualquier manera, documentarlo: podría afectar muy bien cómo se usa el valor de retorno.

Otra opción para lidiar con grandes cantidades de datos es usar lotes, por supuesto, eso es pensar algo lejos de la pregunta original, pero es un enfoque diferente a considerar en una situación en la que la transmisión normalmente sería atractiva.


La única forma en que esto causaría problemas es si la persona que llama abusa del protocolo de IEnumerable<T> . La forma correcta de usarlo es llamar a Dispose cuando ya no sea necesario.

La implementación generada por yield return toma la llamada Dispose como una señal para ejecutar cualquier bloque finally , que en su ejemplo llamará a Dispose en los objetos que ha creado en las instrucciones de using .

Hay una serie de características de lenguaje (en particular foreach ) que hacen que sea muy fácil utilizar IEnumerable<T> correctamente.


Lo que puede hacer es usar un SqlDataAdapter en su lugar y llenar una DataTable. Algo como esto:

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 esta forma, consultas todo de una sola vez y cierras la conexión de inmediato, pero todavía estás iterando perezosamente el resultado. Además, la persona que llama de este método no puede convertir el resultado en una lista y hacer algo que no deberían hacer.


Me he topado con esta pared varias veces. Las consultas de la base de datos SQL no son fácilmente transmisibles como los archivos. En su lugar, busque solo la cantidad que crea que necesitará y devuélvala como el contenedor que desee ( IList<> , DataTable , etc.). IEnumerable no te ayudará aquí.


No siempre eres inseguro con IEnumerable. Si abandonas el framework, llama a GetEnumerator (que es lo que la mayoría de la gente hará), entonces estás a salvo. Básicamente, eres tan seguro como la precisión del código con tu método:

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"); } }

Si puede dejar la conexión de base de datos abierta o no, también depende de su arquitectura. Si la persona que llama participa en una transacción (y su conexión se alista automáticamente), la conexión se mantendrá abierta por el marco de todos modos.

Otra ventaja del yield es (al usar un cursor del lado del servidor), su código no tiene que leer todos los datos (ejemplo: 1.000 elementos) de la base de datos, si su consumidor desea salir del ciclo antes (por ejemplo: después el décimo elemento). Esto puede acelerar la consulta de datos. Especialmente en un entorno Oracle, donde los cursores del lado del servidor son la forma más común de recuperar datos.


No utilices el rendimiento aquí. tu muestra está bien.


No, estás en el camino correcto ... el rendimiento bloqueará al lector ... puedes probarlo haciendo otra llamada a la base de datos mientras llamas al IEnumerable


Siempre se puede usar un hilo separado para almacenar los datos (quizás en una cola) mientras se hace un informe para devolver los datos. Cuando el usuario solicita datos (devueltos a través de un yeild), se elimina un elemento de la cola. Los datos también se agregan continuamente a la cola a través del hilo separado. De esta forma, si el usuario solicita los datos lo suficientemente rápido, la cola nunca estará muy llena y no tendrá que preocuparse por problemas de memoria. Si no lo hacen, la cola se llenará, lo que puede no ser tan malo. Si hay algún tipo de limitación que le gustaría imponer en la memoria, podría imponer un tamaño de cola máximo (en ese punto el otro subproceso esperaría a que se eliminen los elementos antes de agregar más a la cola). Naturalmente, querrás asegurarte de manejar los recursos (es decir, la cola) correctamente entre los dos hilos.

Como alternativa, puede forzar al usuario a pasar un booleano para indicar si los datos deben almacenarse en búfer o no. Si es verdadero, los datos se almacenan en un búfer y la conexión se cierra lo antes posible. Si es falso, los datos no se almacenan en el búfer y la conexión de la base de datos permanece abierta el tiempo que el usuario lo necesite. Tener un parámetro booleano obliga al usuario a hacer la elección, lo que asegura que ellos sepan sobre el problema.


Tu no te estas perdiendo nada. Su muestra muestra cómo NO usar el retorno de rendimiento. Agregue los elementos a una lista, cierre la conexión y devuelva la lista. La firma de su método aún puede devolver IEnumerable.

Editar: Dicho esto, Jon tiene razón (¡muy sorprendido!): Hay raras ocasiones en que la transmisión es en realidad lo mejor que se puede hacer desde la perspectiva del rendimiento. Después de todo, si estamos hablando de 100.000 (1,000,000? 10,000,000?) Filas, no queremos cargar todo eso en la memoria primero.


Una forma un poco más concisa para forzar la evaluación del iterador:

using System.Linq; //... var stuff = GetStuff(connectionString).ToList();