Ao entender como a yield
palavra-chave funciona, deparei-me com o link1 e o link2 no StackOverflow, que defendem o uso de yield return
iterações no DataReader e também atendem às minhas necessidades. Mas isso me faz pensar no que acontece, se eu usar yield return
como mostrado abaixo e se eu não percorrer todo o DataReader, a conexão com o banco de dados permanecerá aberta para sempre?
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;
}
}
Tentei o mesmo exemplo em um exemplo de aplicativo de console e notei, durante a depuração, que o bloco finalmente GetRecords()
não é executado. Como posso garantir o fechamento do DB Connection? Existe uma maneira melhor do que usar yield
palavras-chave? Estou tentando criar uma classe personalizada que será responsável pela execução de SQLs selecionados e procedimentos armazenados no DB e retornará o resultado. Mas não quero devolver o DataReader ao chamador. Também quero ter certeza de que a conexão será fechada em todos os cenários.
Editar A resposta foi alterada para a resposta de Ben, pois é incorreto esperar que os chamadores do método usem o método corretamente e, com relação à conexão com o banco de dados, será mais caro se o método for chamado várias vezes sem motivo.
Obrigado Jakob e Ben pela explicação detalhada.
Respostas:
Sim, você enfrentará o problema que descreve: até concluir a iteração sobre o resultado, manterá a conexão aberta. Há duas abordagens gerais em que posso pensar para lidar com isso:
Empurre, não puxe
Atualmente você está retornando uma
IEnumerable<IDataRecord>
estrutura de dados da qual você pode extrair. Em vez disso, você poderia mudar o seu método para empurrar seus resultados fora. A maneira mais simples seria passar umaAction<IDataRecord>
chamada em cada iteração:Observe que, como você está lidando com uma coleção de itens,
IObservable
/IObserver
pode ser uma estrutura de dados um pouco mais apropriada, mas, a menos que você precise, um simplesAction
é muito mais direto.Avalie Ansiosamente
Uma alternativa é garantir que a iteração seja totalmente concluída antes de retornar.
Normalmente, você pode fazer isso colocando os resultados em uma lista e depois retornando, mas nesse caso há a complicação adicional de cada item sendo a mesma referência para o leitor. Então, você precisa de algo para extrair o resultado necessário do leitor:
fonte
Seu
finally
bloco sempre será executado.Quando você usa
yield return
o compilador, cria uma nova classe aninhada para implementar uma máquina de estado.Essa classe conterá todo o código dos
finally
blocos como métodos separados. Ele acompanhará osfinally
blocos que precisam ser executados dependendo do estado. Todos osfinally
blocos necessários serão executados noDispose
métodoDe acordo com a especificação da linguagem C #,
foreach (V v in x) embedded-statement
é expandido paraportanto, o enumerador será descartado mesmo se você sair do loop com
break
oureturn
.Para obter mais informações sobre a implementação do iterador, leia este artigo por Jon Skeet.
Editar
O problema dessa abordagem é que você confia nos clientes da sua classe para usar o método corretamente. Eles poderiam, por exemplo, obter o enumerador diretamente e iterá-lo em um
while
loop sem descartá-lo.Você deve considerar uma das soluções sugeridas por @BenAaronson.
fonte
var records = GetRecords(); var first = records.First(); var count = records.Count()
realmente executará esse método duas vezes, abrindo e fechando a conexão e o leitor a cada vez. Iterar duas vezes faz a mesma coisa. Você também pode passar o resultado em torno de um longo tempo antes de realmente Iterando sobre ele, etc. Nada na assinatura do método implica que tipo de comportamentoGetRecords
returnIEnumerable
, espero que carregue os registros na memória. Por outro lado, se fosse uma propriedadeRecords
, prefiro assumir que é algum tipo de abstração sobre o banco de dados.Independentemente do
yield
comportamento específico , seu código contém caminhos de execução que não descartarão seus recursos corretamente. E se sua segunda linha lançar uma exceção ou sua terceira? Ou até o seu quarto? Você precisará de uma cadeia try / finalmente muito complexa, ou poderá usarusing
blocos.As pessoas observaram que isso não é intuitivo, que as pessoas que chamam seu método podem não saber que enumerar o resultado duas vezes chamará o método duas vezes. Bem, azar. É assim que a linguagem funciona. Esse é o sinal que
IEnumerable<T>
envia. Há uma razão para isso não retornarList<T>
ouT[]
. As pessoas que não sabem disso precisam ser educadas, não contornadas.O Visual Studio possui um recurso chamado análise de código estático. Você pode usá-lo para descobrir se você descartou os recursos corretamente.
fonte
IEnumerable
faça todo o tipo de coisas, como lançar exceções totalmente não relacionadas ou gravar arquivos de 5 GB no disco. Só porque a linguagem permite, isso não significa que é aceitável retornarIEnumerable
um método que não fornece nenhuma indicação de que isso acontecerá.IEnumerable<T>
é a indicação de que enumerá-lo duas vezes resultará em duas chamadas. Você receberá avisos úteis em suas ferramentas se enumerar várias vezes várias vezes. O ponto sobre lançar exceções é igualmente válido, independentemente do tipo de retorno. Se esse método retornasse umList<T>
, lançaria as mesmas exceções.IEnumerable
emulador de advertência. Mas também acho que, como redator de métodos, você tem a responsabilidade de aderir ao que sua assinatura implica. O método é chamadoGetRecords
, portanto, quando retornar, você esperaria que ele obtivesse os registros . Se fosse chamadoGiveMeSomethingICanPullRecordsFrom
(ou, mais realistaGetRecordSource
ou similar), um preguiçosoIEnumerable
seria mais aceitável.File
, em ,ReadLines
eReadAllLines
pode ser facilmente diferenciado, e isso é uma coisa boa. (Embora seja mais pelo fato de uma sobrecarga de método não diferir apenas pelo tipo de retorno). Talvez ReadRecords e ReadAllRecords também se ajustem aqui como esquema de nomes.O que você fez parece errado, mas funcionará bem.
Você deve usar um IEnumerator <> , pois também herda IDisposable .
Mas como você está usando um foreach, o compilador ainda está usando um IDisposable para gerar um IEnumerator <> para o foreach.
de fato, o foreach envolve muita coisa internamente.
Verifique /programming/13459447/do-i-need-to-consider-disposing-of-any-ienumerablet-i-use
fonte