Pregunta

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 creo que el consumidor querrá recorrer 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 es bueno / flexible. Así que estaba pensando que podría usar un & "; Rendimiento de retorno &"; escriba el diseño 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 el rendimiento (en sitios como este ... msdn no parece mencionar esto), aparentemente es un evaluador perezoso, que mantiene el estado del populador alrededor, anticipando de alguien que solicita el siguiente valor, y luego solo lo ejecuta hasta que devuelve el siguiente valor.

Esto parece estar bien en la mayoría de los casos, pero con una llamada DB, esto suena un poco incierto. Como un ejemplo un tanto artificial, si alguien solicita un IEnumerable, estoy completando una llamada de base de datos, obtiene la mitad y luego se queda atrapado en un bucle ... hasta donde puedo ver, mi conexión de base de datos está funcionando permanecer abierto para siempre.

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

¿Fue útil?

Solución

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

Desde mi punto de vista, esa decisión debería depender de la persona que llama, que sabe más sobre lo que quiere hacer. Si escribe el código utilizando un bloque iterador, la persona que llama puede muy convertir fácilmente ese formulario de transmisión en un formulario completamente protegido:

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

Si, por otro lado, hace el almacenamiento en búfer usted mismo, 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 lo que hace, y aconsejaría a la persona que llama que decida adecuadamente. Es posible que incluso desee proporcionar un método auxiliar para llamar básicamente a la versión transmitida y convertirla en una lista.

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

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

Otros consejos

No siempre eres inseguro con el IEnumerable. Si abandonas la llamada del marco GetEnumerator (que es lo que hará la mayoría de las personas), entonces estás a salvo. Básicamente, estás tan seguro como la minuciosidad del código usando 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 permitirse el lujo de dejar abierta la conexión de la base de datos o no, también depende de su arquitectura. Si la persona que llama participa en una transacción (y su conexión se inscribe automáticamente), el marco mantendrá la conexión abierta de todos modos.

Otra ventaja de yield es (cuando se usa un cursor del lado del servidor), su código no tiene que leer todos los datos (ejemplo: 1,000 artículos) de la base de datos, si su consumidor quiere salir del ciclo anterior (ejemplo: después del 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 común de recuperar datos.

No te falta nada. Su muestra muestra cómo NO usar el 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 un punto (¡muy sorprendido!): hay raras ocasiones en las que la transmisión es realmente lo mejor desde la perspectiva del rendimiento. Después de todo, si estamos hablando de 100,000 (1,000,000? 10,000,000?) Filas aquí, no querrás cargar todo eso primero en la memoria.

Como comentario aparte, tenga en cuenta que el enfoque IEnumerable < T > es esencialmente lo que hacen los proveedores de LINQ (LINQ-to-SQL, LINQ-to-Entities) una 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 aquí es que:

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

Esto se relaciona un poco con mis pensamientos aquí: LINK pragmático .

Pero debo enfatizar: definitivamente hay momentos en que la transmisión es altamente deseable. No es un simple "siempre vs nunca" cosa ...

Forma un poco más concisa de forzar la evaluación del iterador:

using System.Linq;

//...

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

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

La única forma en que esto podría causar 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 rendimiento retorno toma la llamada Dispose como una señal para ejecutar cualquier bloque finalmente abierto, que en su ejemplo llamará Dispose en los objetos que ha creado en las declaraciones usando .

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

Siempre puede usar un subproceso separado para almacenar los datos en el búfer (tal vez en una cola) mientras que también hace un esfuerzo para devolver los datos. Cuando el usuario solicita datos (devueltos a través de un servicio), un elemento se elimina de la cola. Los datos también se agregan continuamente a la cola a través del hilo separado. De esa manera, si el usuario solicita los datos lo suficientemente rápido, la cola nunca está muy llena y no tiene que preocuparse por problemas de memoria. Si no lo hacen, entonces 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 máximo de cola (en ese momento, el otro hilo esperaría a que se eliminen los elementos antes de agregar más a la cola). Naturalmente, querrá asegurarse de manejar los recursos (es decir, la cola) correctamente entre los dos hilos.

Como alternativa, puede obligar al usuario a pasar un booleano para indicar si los datos deben almacenarse o no en el búfer. Si es verdadero, los datos se almacenan en el 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 mientras el usuario lo necesite. Tener un parámetro booleano obliga al usuario a elegir, lo que garantiza que sepa sobre el problema.

Me he topado con este muro varias veces. Las consultas de bases de datos SQL no se pueden transmitir fácilmente como archivos. En su lugar, consulte solo lo que cree que necesitará y devuélvalo como el contenedor que desee ( IList < > , DataTable , etc.). IEnumerable no lo ayudará aquí.

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 manera, está consultando todo de una vez y cerrando la conexión de inmediato, pero aún está iterando perezosamente el resultado. Además, la persona que llama de este método no puede enviar el resultado a una Lista y hacer algo que no debería estar haciendo.

No use el rendimiento aquí. su muestra está bien.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top