yield
キーワードの仕組みを理解しているときに、yield return
の使用を推奨するStackOverflowで link1 および link2 に出会いましたDataReaderを反復処理している間、それは私のニーズにも適しています。しかし、以下に示すようにyield return
を使用し、DataReader全体を反復処理しない場合、DB接続は永久に開いたままになるのでしょうか?
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;
}
}
サンプルコンソールアプリで同じ例を試しましたが、デバッグ中にGetRecords()
のfinallyブロックが実行されていないことに気付きました。 DB接続が確実に閉じられるようにするにはどうすればよいですか? yield
キーワードを使用するよりも良い方法はありますか?選択したSQLとDBでのストアドプロシージャの実行を担当し、結果を返すカスタムクラスを設計しようとしています。しかし、DataReaderを呼び出し元に返したくありません。また、すべてのシナリオで接続が閉じられることを確認したいと思います。
Editメソッドの呼び出し元がメソッドを正しく使用することを期待するのは正しくなく、DB接続に関してはコストが高くなるため、回答をBenの回答に変更しました。このメソッドは理由もなく何度も呼び出されます。
詳細については、JakobとBenに感謝します。
はい、あなたはあなたが説明する問題に直面します:結果を反復するのを終えるまで、あなたは接続を開いたままにします。これに対処するために考えられる一般的なアプローチが2つあります。
現在、IEnumerable<IDataRecord>
、つまりプル可能なデータ構造を返しています。代わりに、メソッドをPushに切り替えて結果を出力することができます。最も簡単な方法は、各反復で呼び出されるAction<IDataRecord>
を渡すことです。
void GetRecords(Action<IDataRecord> callback)
{
// ...
while (myReader.Read())
{
callback(myReader);
}
}
アイテムのコレクションを扱っている場合、IObservable
/IObserver
の方が少し適切なデータ構造ですが、必要でない限り、単純なAction
ははるかに簡単です。
別の方法は、戻る前に反復が完全に完了していることを確認することです。
通常、結果をリストに入れてそれを返すだけでこれを行うことができますが、この場合、各項目がリーダーへの同じ参照であるという追加の複雑さが存在します。したがって、読者から必要な結果を抽出するために何かが必要です。
IEnumerable<T> GetRecords<T>(Func<IDataRecord,T> extractor)
{
// ...
var result = new List<T>();
try
{
while (myReader.Read())
{
result.Add(extractor(myReader));
}
}
finally
{
myReader.Close();
}
return result;
}
finally
ブロックは常に実行されます。
_yield return
_を使用すると、コンパイラーは新しいネストされたクラスを作成して、状態マシンを実装します。
このクラスには、finally
ブロックのすべてのコードが個別のメソッドとして含まれます。状態に応じて実行する必要があるfinally
ブロックを追跡します。必要なすべてのfinally
ブロックはDispose
メソッドで実行されます。
C#言語仕様によれば、foreach (V v in x) embedded-statement
は
_{
E e = ((C)(x)).GetEnumerator();
try
{
while (e.MoveNext())
{
V v = (V)(T)e.Current;
embedded - statement
}
}
finally {
… // Dispose e
}
}
_
そのため、break
またはreturn
を使用してループを終了した場合でも、列挙子は破棄されます。
イテレータ実装の詳細については、こちらをご覧ください この記事はJon Skeet氏
編集
このようなアプローチの問題は、メソッドを正しく使用するためにクラスのクライアントに依存していることです。たとえば、列挙子を直接取得し、それをwhile
ループで破棄せずに反復できます。
@BenAaronsonによって提案されたソリューションの1つを検討する必要があります。
特定のyield
動作に関係なく、コードにはリソースを正しく破棄しない実行パスが含まれています。 2行目が例外をスローした場合、または3行目が例外をスローした場合はどうなりますか?それともあなたの4番目?非常に複雑なtry/finallyチェーンが必要になるか、using
ブロックを使用できます。
IEnumerable<IDataRecord> GetRecords()
{
using(var connection = new SqlConnection(@"..."))
{
connection.Open();
using(var command = new SqlCommand(@"...", connection);
{
using(var reader = command.ExecuteReader())
{
while(reader.Read())
{
// your code here.
yield return reader;
}
}
}
}
}
これは直感的ではなく、メソッドを呼び出す人は結果を2回列挙するとメソッドが2回呼び出されることを知らないかもしれないと人々は述べています。まあ、頑張ってね。それが言語が機能する方法です。それがIEnumerable<T>
送信します。これが返されない理由がありますList<T>
またはT[]
。これを知らない人は、教育を受ける必要があり、回避する必要はありません。
Visual Studioには、静的コード分析と呼ばれる機能があります。これを使用して、リソースを適切に破棄したかどうかを確認できます。
あなたがしたことは間違っているようですが、うまくいきます。
IEnumerator <>も使用する必要があります。これは、IDisposableも継承するためです。
ただし、foreachを使用しているため、コンパイラーはIDisposableを使用してIEnumerator <>foreachの場合。
実際、foreachは内部で多くのことを伴います。