Pergunta

Fundo: Eu tenho um monte de cordas que estou recebendo a partir de um banco de dados, e quero devolvê-los. Tradicionalmente, seria algo como isto:

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

Mas então eu acho que o consumidor vai querer percorrer os itens e não se preocupa muito mais, e eu gostaria de não caixa-me em um List, per se, por isso, se eu retornar um IEnumerable tudo é bom / flexível. Então, eu estava pensando que eu poderia usar um projeto de tipo "rendimento retorno" para lidar com isso ... algo como isto:

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

Mas agora que eu estou lendo um pouco mais sobre o rendimento (em sites como este ... MSDN não parecem mencionar isso), é aparentemente um avaliador preguiçoso, que mantém o estado da populator ao redor, em antecipação de alguém pedindo o valor seguinte, e depois executá-lo apenas até ele retorna o valor seguinte.

Este parece muito bem na maioria dos casos, mas com uma chamada DB, isso soa um pouco arriscada. Como um exemplo um tanto artificial, se alguém pede um IEnumerable de que eu estou preenchendo de uma chamada DB, fica com a metade dele, e depois fica preso em um loop ... tanto quanto eu posso ver minha conexão DB vai para ficar aberta para sempre.

Parece pedindo para ter problemas em alguns casos, se o iterador não terminar ... estou faltando alguma coisa?

Foi útil?

Solução

É um ato de equilíbrio: você quer forçar todos os dados na memória imediatamente assim que você pode liberar a conexão, ou você quer tirar proveito de streaming de dados, com o custo de amarrar a conexão durante todo esse tempo ?

A maneira que eu olhar para ele, essa decisão deve ser potencialmente até o chamador, quem sabe mais sobre o que eles querem fazer. Se você escrever o código usando um bloco iterador, o chamador pode muito facilmente transformado que streaming de forma para uma forma totalmente tamponada:

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

Se, por outro lado, você faz a si mesmo buffer, não há nenhuma maneira o chamador pode voltar para um modelo de streaming.

Então, eu provavelmente usar o modelo de streaming e dizer explicitamente na documentação que ele faz, e aconselhar o autor da chamada para decidir de forma adequada. Você pode até querer fornecer um método auxiliar para, basicamente, chamar a versão streaming e convertê-lo em uma lista.

Claro que, se você não confia em seu interlocutor para tomar a decisão apropriada, e você tem uma boa razão para acreditar que eles nunca realmente deseja transmitir os dados (por exemplo, ele nunca vai voltar muito de qualquer maneira), então vá para a abordagem lista. De qualquer forma, documento que -. Ele poderia muito bem afetar a forma como o valor de retorno é usado

Outra opção para lidar com grandes quantidades de dados é a utilização de lotes, é claro -. Que está pensando um pouco longe da pergunta original, mas é uma abordagem diferente a considerar na situação em que fluindo normalmente seria atraente

Outras dicas

Você nem sempre está inseguro com o IEnumerable. Se você deixar o GetEnumerator chamada estrutura (que é o que a maioria das pessoas vai fazer), então você está seguro. Basicamente, você está tão seguro quanto o carefullness do código usando seu 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");
    }
}

Se você pode affort para deixar a conexão banco de dados aberto ou não depende de sua arquitetura também. Se os participa de chamadas em uma transação (e sua conexão é auto alistados), então a conexão será mantida aberta pela estrutura de qualquer maneira.

Outra vantagem do yield é (ao usar um cursor do lado do servidor), seu código não tem que ler todos os dados (exemplo: 1.000 itens) do banco de dados, se o consumidor quer sair do ciclo anterior ( exemplo: após o item 10). Isso pode acelerar a consulta de dados. Especialmente em um ambiente Oracle, onde cursores do lado do servidor são o caminho comum para recuperar dados.

Você não está faltando alguma coisa. Seus programas de amostra como não usar yield return. Adicione os itens a uma lista, fechar a conexão, e retornar a lista. Sua assinatura método ainda pode retornar IEnumerable.

Editar: Dito isso, Jon tem um ponto (tão surpreso!): Existem raras ocasiões onde streaming é realmente a melhor coisa a fazer a partir de uma perspectiva de desempenho. Afinal, se é 100.000 (1.000.000? 10000000?) Linhas que estamos falando aqui, você não quer ser o carregamento que todos na memória em primeiro lugar.

Como um aparte - nota que a abordagem IEnumerable<T> é essencialmente o que os provedores de LINQ (LINQ to SQL, LINQ to Entities) faz para viver. A abordagem tem vantagens, como Jon diz. No entanto, existem problemas definidos também - em particular (para mim) em termos de (a combinação de) separação | abstração.

O que quero dizer aqui é que:

  • em um cenário MVC (por exemplo) que você quer que seu "obter dados" passo para realmente obter dados , para que você possa testá-lo trabalha no controller não, Ver (sem ter que lembrar de chamar .ToList() etc)
  • você não pode garantir que outra implementação DAL será pode para transmitir dados (por exemplo, uma chamada POX / WSE / sabão pode geralmente não registros de fluxo); e você não necessariamente quer fazer o comportamento confusamente diferentes (conexão ou seja, ainda em aberto durante iteração com uma implementação e fechado para outra)

Esta laços em um pouco com os meus pensamentos aqui: Pragmatic LINQ .

Mas devo sublinhar - há definitivamente momentos em que o streaming é altamente desejável. Não é um simples "sempre vs nunca mais" coisa ...

forma um pouco mais conciso para a avaliação de força de iterator:

using System.Linq;

//...

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

Não, você está no caminho certo ... o rendimento irá travar o leitor ... você pode testá-lo a fazer outra chamada de banco de dados ao chamar o IEnumerable

A única maneira que isso poderia causar problemas é se os abusos chamador o protocolo de IEnumerable<T>. A maneira correta de usá-lo é chamar Dispose nele quando já não é necessário.

A implementação gerada por yield return toma a chamada Dispose como um sinal para executar todos os blocos finally abertas, que no seu exemplo vai chamar Dispose sobre os objetos que você criou nas demonstrações using.

Há uma série de recursos de linguagem (em particular foreach) que tornam muito fácil de usar IEnumerable<T> corretamente.

Você pode sempre usar um segmento separado para amortecer os dados (talvez para uma fila) ao mesmo tempo, fazendo uma de rendimento que para retornar os dados. Quando o utilizador pede dados (devolvido através de um de rendimento que), um item é removido da fila. Os dados são também ser continuamente adicionado à fila através do segmento separado. Dessa forma, se o usuário solicita os dados rápido o suficiente, a fila não é muito completo e você não precisa se preocupar com problemas de memória. Se não o fizerem, então a fila vai encher-se, o que pode não ser tão ruim. Se houver algum tipo de limitação que você gostaria de impor a memória, você poderia impor o tamanho máximo da fila (altura em que o outro segmento iria esperar por itens a serem removidos antes de adicionar mais para a fila). Naturalmente, você vai querer certificar-se de que você lidar com recursos (isto é, a fila) corretamente entre as duas linhas.

Como alternativa, você pode forçar o usuário a passar em um booleano para indicar se os dados devem ser tamponado. Se for verdade, os dados são armazenados em buffer e a conexão é fechada o mais rápido possível. Se for falso, os dados não é tamponado e as estadias de conexão do banco de dados aberto enquanto o usuário precisa que ele seja. Ter um booleano parâmetro força o usuário a fazer a escolha, o que garante que eles sabem sobre o assunto.

Eu tenho batido para este parede algumas vezes. consultas de banco de dados SQL não são facilmente streamable como arquivos. Em vez disso, consulta somente tanto quanto você acha que vai precisar e devolvê-lo como qualquer recipiente que você quer (IList<>, DataTable, etc.). IEnumerable não irá ajudá-lo aqui.

O que você pode fazer é usar um SqlDataAdapter em vez disso e preencher uma DataTable. Algo parecido com isto:

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

Desta forma, você está consultando tudo em um único tiro, e fechar a conexão imediatamente, mas você ainda está preguiçosamente repetindo o resultado. Além disso, o autor da chamada desse método não pode converter o resultado para uma lista e fazer algo que não deveria estar fazendo.

rendimento uso

Não aqui. sua amostra é bom.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top