C#IEnumerator / yield構造は潜在的に悪いですか?
質問
背景:データベースから取得する文字列がたくさんあるので、それらを返したい。従来は、次のようなものでした:
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;
}
しかし、消費者はアイテムを繰り返し処理することを望み、他のことはあまり気にしません。そして、リスト自体にボックスを入れたくないので、IEnumerableを返す場合すべてが良い/柔軟です。だから、私は<!> quot; yield return <!> quot;を使用できると考えていました。これを処理するデザインを入力します...次のようなものです:
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();
}
}
}
}
しかし、私は収量についてもう少し読んでいます(このようなサイトでは... msdnはこれに言及していないようです)、それは明らかに怠慢な評価者であり、予想の中で人口の状態を維持します次の値を要求し、次の値を返すまでそれを実行するだけの誰かの。
これはほとんどの場合問題ないように見えますが、DBコールでは少し危険に聞こえます。少し不自然な例として、誰かがDB呼び出しからデータを取得しているIEnumerableを要求し、その半分を通過してから、ループに陥った場合、DB接続が進行していることがわかりますいつまでも開いたままになります。
イテレータが終了しない場合、場合によっては問題を尋ねるように聞こえます...何かが足りませんか?
解決
それはバランスのとれた行為です:接続を解放できるようにすべてのデータをすぐにメモリに強制しますか、それともデータをストリーミングすることで利益を得たいですか?
私が見ているように、その決定は呼び出し側に任せられるべきです。イテレータブロックを使用してコードを記述する場合、呼び出し元はそのストリーミングフォームを完全にバッファリングされたフォームに簡単に変換できます:
List<string> stuff = new List<string>(GetStuff(connectionString));
一方、自分でバッファリングを行う場合、発信者がストリーミングモデルに戻る方法はありません。
だから、おそらくストリーミングモデルを使用し、ドキュメントで明示的にと言って、呼び出し元に適切な決定をするようアドバイスします。基本的に、ストリームバージョンを呼び出してリストに変換するヘルパーメソッドを提供することもできます。
もちろん、適切な決定を下すために呼び出し元を信頼せず、データを実際にストリーミングしたくないと信じる正当な理由がある場合(たとえば、とにかく多くを返すことはない)リストアプローチの場合。いずれにせよ、それを文書化してください-戻り値がどのように使用されるかに非常によく影響する可能性があります。
大量のデータを扱うための別のオプションは、もちろんバッチを使用することです-それは元の質問からいくらか離れていると考えていますが、ストリーミングが通常魅力的である状況で考慮するための異なるアプローチです。
他のヒント
IEnumerableを使用することは必ずしも安全ではありません。フレームワークの呼び出しGetEnumerator
(ほとんどの人が行うことです)を離れると、安全です。基本的に、メソッドを使用したコードの注意と同じくらい安全です:
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");
}
}
データベース接続を開いたままにすることができるかどうかは、アーキテクチャにも依存します。呼び出し元がトランザクションに参加する場合(および接続が自動的に登録される場合)、とにかくフレームワークによって接続が開かれたままになります。
yield
のもう1つの利点は、(サーバー側カーソルを使用する場合)消費者がループから抜け出すためにデータベースからすべてのデータ(例:1,000アイテム)を読み取る必要がないことです。以前(例:10番目のアイテムの後)。これにより、データのクエリを高速化できます。特に、サーバー側カーソルがデータを取得する一般的な方法であるOracle環境では。
何も欠落していません。サンプルは、利回り収益を使用しない方法を示しています。リストにアイテムを追加し、接続を閉じて、リストを返します。メソッドシグネチャは引き続きIEnumerableを返すことができます。
編集:とはいえ、Jonにはポイントがあります(驚いた!)。パフォーマンスの観点から、ストリーミングが実際に行うのに最適な場合はまれです。結局のところ、ここで話している100,000(1,000,000?10,000,000?)行であれば、最初にすべてをメモリにロードする必要はありません。
余談ですが、 IEnumerable&lt; T&gt;
アプローチは、LINQプロバイダー(LINQ-to-SQL、LINQ-to-Entities)の目的である本質的にであることに注意してください生活。ジョンが言うように、このアプローチには利点があります。しかし、明確な問題もあります-特に(私にとって)分離(の組み合わせ)の観点から|抽象化。
ここで私が意味するのは:
- (たとえば)MVCシナリオでは、「データを取得」したい view ではなく controller で動作することをテストできるように、実際にデータを取得するステップ(
> .ToList()
など) - 別のDAL実装がデータをストリーミングできることを保証できません(たとえば、POX / WSE / SOAP呼び出しは通常レコードをストリーミングできません)。また、動作を混乱させるほど異なるものにする必要はありません(つまり、1つの実装での反復中に接続を開いたまま、別の実装で閉じます)
これは、私の考えと少し関係しています: Pragmatic LINQ 。
しかし、強調する必要があります-ストリーミングが非常に望ましい場合があります。これは単純な「常に対決して」ではありません。事...
イテレータの評価をやや簡潔にする方法:
using System.Linq;
//...
var stuff = GetStuff(connectionString).ToList();
いいえ、あなたは正しい道を歩んでいます... yieldはリーダーをロックします... IEnumerableを呼び出している間に別のデータベース呼び出しを行ってテストすることができます
これが問題を引き起こす唯一の方法は、呼び出し元が IEnumerable&lt; T&gt;
のプロトコルを悪用した場合です。正しい使用方法は、不要になったときに Dispose
を呼び出すことです。
yield return
によって生成された実装は、 Dispose
呼び出しをシグナルとして受け取り、開いている finally
ブロックを実行します。 using
ステートメントで作成したオブジェクトを Dispose
します。
IEnumerable&lt; T&gt;
を非常に簡単に正しく使用できるようにする言語機能(特に foreach
)が多数あります。
常に別のスレッドを使用してデータをバッファリングし(おそらくキューに)、同時にデータを返すためにyeildを実行することもできます。ユーザーがデータを要求すると(yeildを介して返される)、アイテムがキューから削除されます。データは、個別のスレッドを介してキューに継続的に追加されています。そうすれば、ユーザーが十分な速さでデータを要求した場合、キューが非常にいっぱいになることはなく、メモリの問題を心配する必要はありません。そうでない場合、キューはいっぱいになりますが、それほど悪くないかもしれません。メモリに課す何らかの制限がある場合、最大キューサイズを強制できます(この時点で、他のスレッドは、キューに追加する前にアイテムが削除されるのを待ちます)。当然、2つのスレッド間でリソース(つまり、キュー)を正しく処理することを確認する必要があります。
別の方法として、データをバッファリングするかどうかを示すブール値をユーザーに強制的に渡すことができます。 trueの場合、データはバッファリングされ、接続はできるだけ早く閉じられます。 falseの場合、データはバッファリングされず、ユーザーが必要とする限りデータベース接続は開いたままになります。ブール値のパラメーターを使用すると、ユーザーに選択を強制するため、問題について確実に知ることができます。
この壁に何度かぶつかりました。 SQLデータベースクエリは、ファイルのように簡単にストリーミングできません。代わりに、必要と思われる範囲でクエリを実行し、必要なコンテナ( IList&lt;&gt;
、 DataTable
など)として返します。 IEnumerable
はここでは役に立ちません。
できることは、代わりにSqlDataAdapterを使用して、DataTableを埋めることです。このようなもの:
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();
}
}
この方法では、すべてを一度にクエリし、すぐに接続を閉じますが、結果を遅延的に繰り返します。さらに、このメソッドの呼び出し元は、結果をリストにキャストできず、実行すべきでないことを実行できません。
ここでは収量を使用しないでください。サンプルは問題ありません。