Usar o rendimento para iterar em um leitor de dados pode não fechar a conexão?
Pergunta
Aqui está um exemplo de código para recuperar dados de um banco de dados usando a palavra-chave yield que encontrei em alguns lugares enquanto pesquisava no Google:
public IEnumerable<object> ExecuteSelect(string commandText)
{
using (IDbConnection connection = CreateConnection())
{
using (IDbCommand cmd = CreateCommand(commandText, connection))
{
connection.Open();
using (IDbDataReader reader = cmd.ExecuteReader())
{
while(reader.Read())
{
yield return reader["SomeField"];
}
}
connection.Close();
}
}
}
Estou correto ao pensar que neste código de exemplo, a conexão não seria fechada se não iterássemos em todo o datareader?
Aqui está um exemplo que não fecharia a conexão, se bem entendi o rendimento.
foreach(object obj in ExecuteSelect(commandText))
{
break;
}
Para uma conexão de banco de dados que pode não ser catastrófica, suponho que o GC a limparia eventualmente, mas e se, em vez de uma conexão, fosse um recurso mais crítico?
Solução
O Iterador que o compilador sintetiza implementa IDisposable, que foreach chama quando o loop foreach é encerrado.
O método Dispose() do Iterator limpará as instruções using na saída antecipada.
Contanto que você use o iterador em um loop foreach, bloco using() ou chame o método Dispose() de alguma outra forma, a limpeza do Iterator acontecerá.
Outras dicas
A conexão será fechada automaticamente já que você a está usando dentro do bloco "using".
Pelo teste simples que tentei, aku está certo, o descarte é chamado assim que o bloco foreach sai.
@David:Porém a pilha de chamadas é mantida entre as chamadas, então a conexão não seria fechada porque na próxima chamada retornaríamos para a próxima instrução após o rendimento, que é o bloco while.
Meu entendimento é que quando o iterador for descartado, a conexão também será descartada com ele.Também acho que Connection.Close não seria necessário porque seria cuidado quando o objeto fosse descartado por causa da cláusula using.
Aqui está um programa simples que tentei testar o comportamento ...
class Program
{
static void Main(string[] args)
{
foreach (int v in getValues())
{
Console.WriteLine(v);
}
Console.ReadKey();
foreach (int v in getValues())
{
Console.WriteLine(v);
break;
}
Console.ReadKey();
}
public static IEnumerable<int> getValues()
{
using (TestDisposable t = new TestDisposable())
{
for(int i = 0; i<10; i++)
yield return t.GetValue();
}
}
}
public class TestDisposable : IDisposable
{
private int value;
public void Dispose()
{
Console.WriteLine("Disposed");
}
public int GetValue()
{
value += 1;
return value;
}
}
A julgar por esta explicação técnica, seu código não funcionará como esperado, mas abortará no segundo item, pois a conexão já estava fechada ao retornar o primeiro item.
@Joel Gauvreau:Sim, eu deveria ter continuado a ler. Parte 3 desta série explica que o compilador adiciona tratamento especial para que os blocos finalmente sejam acionados apenas no real fim.